Fantas, Eel, and Specification 18: Bifunctor and Profunctor

The worst is behind us. We’ve mastered the Monad, conquered the Comonad, and surmounted the Semigroup. Consider these last two posts to be a cool-down, because the end is in sight. Today, to enjoy our first week of rest, we’re going to revise functors. A number of times, we’ve seen types with two inner types: Either, Pair, Task, Function, and so on. However, in all cases, we’ve only been able to map over the right-hand side. Well, Fantasists, the reason has something to do with a concept called kinds that we won’t go into. Instead, let’s look at solutions.

We’ll take a type like Either or Pair. These types have two inner types (left and right!) that we could map over. The Bifunctor class allows us to deal with both:

bimap :: Bifunctor f
      => f a c
      ~> (a -> b, c -> d)
      -> f b d

It’s pretty much exactly like Functor, except we are mapping two at a time! What does this look like in actual code?

Either.prototype.bimap =
  function (f, g) {
    return this.cata({
      Left: f,
      Right: g
    })
  }

Pair.prototype.bimap =
  function (f, g) {
    return Pair(f(this._1),
                g(this._2))
  }

Task.prototype.bimap =
  function (f, g) {
    return Task((rej, res) =>
      this.fork(e => rej(f(e)),
                x => res(g(e))))
  }

Hopefully, no huge surprises. We apply the left function to any mention of the left value, and the same for the right. Even the laws are just doubled-up versions of the Functor laws:

// For some functor U:

// Identity
U.map(x => x) === U

// Composition
U.map(f).map(g) ===
  U.map(x => g(f(x)))

// For some bifunctor C:

// Identity
C.bimap(x => x, x => x) === C

// Composition
C.bimap(f, g).bimap(h, i) ===
  C.bimap(l => h(f(l)),
          r => i(g(r)))

A lot of brackets, but look closely: the laws are the same, but we have two independent “channels” on the go!

Fun fact: if you have a Bifunctor instance for some type T, you can automatically derive a fully-lawful Functor instance for T a with f => bimap(x => x, f). Hooray, Free upgrades!

So, is this useful? Yes! Let’s look at a neat little example using Either for a second:

//- Where everything changes...
const login = user =>
  isValid(user) ? Right(user)
                : Left('Boo')

//- Function map === "piping".
//+ failureStream :: String
//+               -> HTML
const failureStream =
  (x => x.toUpperCase())
  .map(x => x + '!')
  .map(x => '<em>' + x + '</em>')

//+ successStream :: User
//+               -> HTML
const successStream =
  (x => x.name)
  .map(x => 'Hey, ' + x + '!')
  .map(x => '<h1>' + x + '</h1>')

//- We can now pass in our two
//- possible application flows
//- using `bimap`!
login(user).bimap(
  failureStream,
  successStream)

Note that this would also work with Task! We’re in a situation where we want to transform a potential success or failure, and bimap lets us supply both at once. Cool, right? Straightforward, too!


Now, Function is a slightly different story. Effectively, we can contramap over its left-hand type (the input) and map over its right-hand type (the output). It turns out there’s a fancy name for this sort of thing, too: Profunctor.

promap :: Profunctor p
       => p b c
       ~> (a -> b, c -> d)
       -> p a d

You can think of it as adding a before and after step to some process. Naturally, the laws look like a mooshmash of Contravariant and Functor:

// For some profunctor p:

// Identity...
P.promap(a => a, b => b) === P

// Composition...
p.promap(f, i).promap(g, h) ===
  p.promap(a => f(g(a)),
           b => h(i(b)))

Guess what? You can build a functor P a out of any profunctor P: f => promap(x => x, f) is all it takes. So many free upgrades!

The left-hand side looks like Contravariant, and the right-hand side like Functor. Of course, we’ve seen a Profunctor already: Function! However, to give a slightly more exciting example, let’s look at one of my favourites: Costar.

//- Fancy wrapping around a specific type
//- of function: an "f a" to a "b"
//+ Costar f a b = f a -> b
const Costar = daggy.tagged('Costar', ['run'])

//- Contramap with the "before" function,
//- fold, then apply the "after" function.
//+ promap :: Functor f
//+        => Costar f b c
//+        -> (b -> a, c -> d)
//+        -> Costar f a d
Costar.prototype.promap = function (f, g) {
  return Costar(x => g(this.run(x.map(f))))
}

//- Takes a list of ints to the sum
//+ sum :: Costar Array Int Int
const sum = Costar(xs =>
  xs.reduce((x, y) => x + y, 0))

//- Make every element 1, then sum them!
const length = sum.promap(_ => 1, x => x)

//- Is the result over 5?
const isOk = sum.promap(x => x, x => x > 5)

//- Why not both? Is the length over 5?
const longEnough = sum.promap(
  _ => 1, x => x > 5)

// Returns false!
longEnough.run([1, 3, 5, 7])

Costar allows us to take the idea of reduce and wrap it in a Profunctor. We can use promap to prepare different inputs for the reduction and manipulate the result. You may have heard of this idea before: map/reduce. No matter how complex the process, there’s a good chance that you can express it in a Profunctor!


These are, of course, very quick overviews of Bifunctor and Profunctor. That said, if you’re comfortable now with Functor and you remember the post on Contravariant, there’s nothing new to learn! We’re really just building on ideas we’ve already had. Bifunctor might seem a little underwhelming now that we’ve seen the power of Monad and Comonad, but it’s twice as powerful as Functor: we can define a flow for success and error, for left and right, for… well, any two things!

As for Profunctor, it’s a pretty massive topic once you start digging. Costar is the opposite of Star, which is an a -> f b function; why not think about how to implement that? Would you need any special conditions to make it a Profunctor?

Take the article’s gist, and bimap and promap until the cows come home, Fantasists, for there is only one article left: Semigroupoid and Category. Expect high drama, hard maths, and herds of monoids. Well, maybe not the second thing…

Until then!