Profunctors are bifunctors that are contravariant in their first type argument and covariant in their second one. Make sure that you understand contravariance first. Then we just need to talk about bifunctors, and finally we will get to profunctors.

Bifunctor Bifunctor , which is available in base , Data.Bifunctor is a lot like Functor . It offers a nice solution for those times when you don’t want to ignore the leftmost type argument of a binary type constructor, such as Either or (,) . Its core operation, bimap , closely resembles fmap , except it lifts two functions into the new context, allowing you to apply one or both. class Bifunctor ( f :: * -> * -> * ) where bimap :: (a0 -> z0) (a0z0) -> (a1 -> z1) (a1z1) -> f a0 a1 f a0 a1 -> f z0 z1 f z0 z1 Ye gods that’s a lot of variables! Let’s clean that up a bit. We’ll be talking about the bimap for Either and tuples so let’s go ahead and see what those look like: Each @ symbol in these examples is a visible type application, using the TypeApplications GHC language extension. @ Either :: (a0 -> z0) bimap(a0z0) -> (a1 -> z1) (a1z1) -> Either a0 a1 a0 a1 -> Either z0 z1 z0 z1 @ (,) :: (a0 -> z0) bimap(,)(a0z0) -> (a1 -> z1) (a1z1) -> (a0, a1) (a0, a1) -> (z0, z1) (z0, z1) bimap takes two unary functions as arguments along with a value, such as (1, 3) or Left 5 , and applies whichever function it can – both if it can! We’ll partially apply bimap here so that we can reuse it: greet :: Bifunctor p => p String String -> p String String = bimap ( "hello " ++ ) ( "goodbye " ++ ) greetbimap () ( That p is going to be something like Either or (,) , taking two type arguments, although in this case we already know both those type arguments have to be strings. Let’s try it out. λ> greet (Left "Julie") greet (Left "Julie") Left "hello Julie" λ> greet (Right "March") greet (Right "March") Right "goodbye March" We can use the function on Either values, even though only one “side” is present at the value level at a time. We can also use it on a two-tuple and use both functions at once: λ> greet ("Julie", "to all that") greet ("Julie", "to all that") ("hello Julie","goodbye to all that") So, bimap is fmap but for binary type constructors where you want the ability to lift two functions at once.

Profunctor Profunctors are bifunctors that are contravariant in the first argument and covariant in the second one. While people do incredibly magical looking things with profunctors, if you’ve understood fmap , contramap , and bimap , then you’re ready for dimap . (That’s di as in “dioxide”; bi was already in use for bimap , so we had to switch from Latin to Greek.) Bifunctor bimap “bi” as in bicycle “2” in Latin Profunctor dimap “di” as in dioxide “2” in Greek The core operation of the Profunctor class is dimap – get ready for some type variable soup. class Profunctor ( f :: * -> * -> * ) where dimap :: (a1 -> a0) (a1a0) -> (z0 -> z1) (z0z1) -> f a0 z0 f a0 z0 -> f a1 z1 f a1 z1 We can start by looking at where it differs from the Bifunctor definition: We have renamed all of the type variables for the moment, just to highlight this particular comparison. bimap :: (a -> b) -> (c -> d) -> f a c -> f b d (ab)(cd)f a cf b d -- ^^^^^^ dimap :: (b -> a) -> (c -> d) -> f a c -> f b d (ba)(cd)f a cf b d -- ^^^^^^ Earlier we saw what bimap looks like with Either and tuple types, but we cannot also implement dimap for these types. That’s because of the contravariance in the first argument; as we saw with Contravariant , basically everything in Haskell that are contravariant functors are function types. Informally, what we’re going to have is a bifunctor that acts like fmap on the z of f a z and like contramap on the a of f a z . It’s worth pointing out here that the Profunctor class also has methods called lmap and rmap (for left and right map, respectively), and their implementations for the function type are flip (.) and (.) . There’s a lot of function composition going on under the hood here.

The (->) profunctor Since we’ve been talking about about functors of functions, we’re going to continue to do so as that is the simplest profunctor example we could start with. What dimap f g does is take h and squeeze it in between f and g . = g . h . f dimap f g h Or, to put it another way, dimap f g starts with f a0 z0 and Extends it on the input side by applying f :: a1 -> a0 to change the “argument” type variable from a0 to a1 (this is the contravariant part). Extends it on the output side by applying g :: z0 -> z1 to change the “result” type variable from z0 to z1 (this is the covariant part). Thus giving a result of f a1 z1 .