Pairs as Functors

Two-ish weeks ago, we talked about the wonderful flexibility of Function when you start treating it as a Functor. We started off with composition, then branching composition, and then finally environment-aware composition. We also gave our humble function a new name: Reader. Today, we’re going to walk the same path for Pair, and build up a closely-related idea.

Functor

It’s pretty simple to write an implementation of Functor for a Pair a b structure:

const Pair = daggy.tagged('_1', '_2')

// We just transform the second value!
// map :: Pair a b ~> (b -> c) -> Pair a c
Pair.prototype.map = function (f) {
  return Pair(this._1, f(this._2))
}

We transform the second half, but leave the first alone. I’m going to suggest that we treat the Pair functor as a way of modelling values with metadata - some extra information about the value. I’ll admit that this, in isolation, is not particularly useful. However, we’ll see that, with some extra functionality provided by ap and chain, it’ll start to make sense.

Applicative

Now, let’s make something interesting. Turns out that one useful Applicative implementation requires the left-hand side to be a Monoid. We can implement this like so:

const Pair = T => {
  const Pair_ = daggy.tagged('_1', '_2')

  Pair_.prototype.map = function (f) {
    return Pair_(this._1, f(this._2))
  }

  Pair_.prototype.ap = function (fs) {
    return Pair_(fs._1.concat(this._1),
                 fs._2(this._2))
  }

  Pair_.of = x => Pair_(T.empty(), x)

  return Pair_
}

As usual, note that only a Semigroup is required for Apply, but of needs empty to produce a valid left-hand value.

Turns out this is a pretty useful implementation, which is definitely made more readable with some help from lift2. Readers of the Fantasy Land series will remember the post on Apply, where we discussed lift2 as a way of “combining contexts” with a given function. Let’s imagine we have a set of actions that have “costs”. We can write them as a Pair with Sum as the left-side monoid:

const CostPair = Pair(Sum)

//- Database lookups are pretty costly...
//+ userFromDB :: Int
//+            -> Pair (Sum Int) String
const nameFromDB = id => CostPair(
  Sum(100), getUserById(id))

//- ... but ordering/counting is harder.
//+ rankFromDB :: Int
//+            -> Pair (Sum Int) Int
const rankFromDB = id => CostPair(
  Sum(500), getUserRankById(id))

//- Do both jobs, end up with Sum(600)!
//+ getUserData :: Int
//+             -> Pair (Sum Int) User
const getUserData = id => lift2(
  x => y => ({ name: x, rank: y }),
  nameFromDB(id), rankFromDB(id))

// ===================== //

//- By the way, we can use `Function` as an
//- `Applicative` as we did last time, and
//- that lets us write this with a couple
//- of `lift2` calls! Beautiful point-free!
//+ getUserData_ :: Int
//+              -> Pair (Sum Int) User
const getUserData_ = lift2(
  lift2(x => y => ({ name: x, rank: y }),
  nameFromDB, rankFromDB)

So, we can collect the “cost” of our computation as we go, which we could then analyse at the end. As we said with Functor, the Pair allows for metadata, which, in this case, is cost of computation. We could also swap out Sum for Max to find the most expensive operation in our app, or Average to… well, find the average!

Monad

const Pair = T => {
  ...

  Pair_.prototype.chain = function (f) {
    const that = f(this._2)

    return Pair_(this._1.concat(that._1),
                 that._2)
  }

  ...
}

Again, Monad requires of (Applicative) and thus needs a Monoid for the left side. Chain doesn’t require of, and so we can get away with a Semigroup.

This chain function allows us to string actions together, while also collecting a value in the monoid, as we did before. Of course, this will work with the CostPair we defined above, but let’s try something a bit different. Let’s instead use [String] as our monoid, and play some Guess Who?

// Sneaky monkey patch!
Array.empty = () => []

LogPair = Pair(Array)

// Lift the users into the LogPair type.
// (The left side at this point is [])
LogPair.of(users)

// Keep only the users with brown hair
.chain(users => LogPair(
  ['Brown hair'],
  users.filter(user =>
    user.hair === 'brown')))

// Keep only the users over 180cm tall
.chain(users => LogPair(
  ['Tall'],
  users.filter(user =>
    user.height > 180)))

// Count the remaining users
.map(users => users.length)

// e.g.: Pair(['Brown Hair', 'Tall'], 36)

We can see that our final result is another Pair, whose right side holds the number of remaining users, as we’d expect. However, the left side is now a list of all the actions that got us to that number! If you built your whole app inside the Pair monad, you would have a purely-functional logger, to which you could write any number of messages at any time. On top of that, Array’s Monoid implementation doesn’t care about the inner type, so you can log any data structure you want!

This nifty little pattern - writing something to a log (or really just appending to a monoid) while transforming the other value - is what gives this type the common name Writer. It is, in essence, the opposite of Reader: Reader can read from some “global” state, Writer can write to some “global” state.


So, we’ve seen that, through a different lens, s -> a is a Reader, and (s, a) is a Writer. We have readable or writable state, and they’re purely functional! There’s only one downside, though: what if we want both?

Think about what state is in stateful languages. It’s a thing that can change as a result of any instruction. This means, really, our stateful languages execute functions kinda like this:

execute :: (State, Action) -> State

We take an instruction (Action) and the current state, and we get back a potentially new state. Maybe something has been written the console, maybe a variable has been updated, etc. That’s really the most abstract that we can make it, and will look familiar to users of packages like Redux. Now, take a closer look at execute:

--                FUNCTION v
execute :: (State, Action) -> State
--               ^ PAIR

OMGWTF. Turns out that execute is a combination of Reader and Writer! Well, if we can make read-only state purely functional, and write-only state purely functional, and we can see that State is really just a combination of the two… there has to be a way to build purely functional read and write state, right?

Right. Next time, we’ll look at the State type, and see how we can combine these two principles to give us everything we need to combat those pesky naysayers who think functional programming is impractical. Get excited!

PS: if you want to play with the code in this post, I’ve put all the code in a Gist, so why not mess around with it?