🎉 End-of-year Sale! Save 25% when you subscribe today.

Some news about contramap

Monday October 29, 2018
Note

We’ve seen that contramap is a powerful operation, but the name isn’t fantastic. We propose a much more intuitive name for this operation, and in doing so make our code much easier to read.


A few months ago we introduced the idea of contravariance, and showed that it’s a very natural idea hidden in a very counterintuitive package. It’s like the map we all know and love on arrays and optionals, but it goes in the opposite direction. We applied it to the idea of predicate sets, and showed that it helps us see a form of composition that we may not have looked for otherwise.

Then, last week, in a very unexpected way, we showed that contramap surfaced when discussing how to convert protocols into concrete datatypes. That was very surprising, and powerful, because it allowed us to transform witnesses to a protocol into all new witnesses, which is something completely hidden from us when dealing with only protocols.

We hope that we have convinced you that contramap is a very powerful tool for composition, even though it seems counterintuitive and can be hard to grasp at first. So that’s why it might seem surprising that we are…

Saying goodbye to contramap, hello pullback!

However, the name contramap isn’t fantastic. In one way it’s nice because it is indeed the contravariant version of map. It has basically the same shape as map, it’s just that the arrow flips the other direction. Even so, the term may seem a little overly-jargony and may turn people off to the idea entirely, and that would be a real shame.

Luckily, there’s a concept in math that is far more general than the idea of contravariance, and in the case of functions is precisely contramap. And even better it has a great name. It’s called the pullback. Intuitively it expresses the idea of pulling a structure back along a function to another structure. Let’s see why this is a really great name for this operation.

Taking pullback for a spin

Recall that we previously defined a PredicateSet type that simply wrapped a function that returns boolean values.

struct Predicate<A> {
  let contains: (A) -> Bool
}

This allows us to express sets that potentially hold infinitely many values, which Swift’s Set is not capable of.

And we could create predicate sets easily enough. For example, one that holds all integers less than 10:

let isLessThan10 = PredicateSet { $0 < 10 }

isLessThan10.contains(5)   // true
isLessThan10.contains(11)  // false

This is neat, but not particularly interesting. But then we discovered that PredicateSet supports a contramap operation, which is precisely what you need to transform predicate sets. We were able to define it like so:

extension PredicateSet {
  func contramap<B>(_ f: @escaping (B) -> A) -> Predicate<B> {
    Predicate<B> { self.contains(f($0)) }
  }
}

We could then use this operation to transform our isLessThan10 predicate into a predicate on strings:

let shortStrings = isLessThan10.contramap { (s: String) in s.count }

shortStrings.contains("Blob")           // true
shortStrings.contains("Blobby McBlob")  // false

Take careful note that there is no “less than 10” logic in the body of the contramap transformation. All of that is inside the isLessThan10 predicate. Instead, we are transforming a predicate set of integers into a predicate set of strings by simply plucking out the character count of a string. This is what allows you to build lots of small units and piece them together to create more complex units.

Even better, if you use our open source library of function composition helpers, Overture, you can write this in a truly short and expressive manner:

import Overture

let shortStrings = isLessThan10.contramap(get(\\String.count))

shortStrings.contains("Blob")           // true
shortStrings.contains("Blobby McBlob")  // false

Now let’s rename contramap to pullback:

extension PredicateSet {
  func pullback<B>(_ f: @escaping (B) -> A) -> Predicate<B> {
    return Predicate<B> { self.contains(f($0)) }
  }
}

import Overture

let shortStrings = isLessThan10.pullback(get(\\String.count))

shortStrings.contains("Blob")           // true
shortStrings.contains("Blobby McBlob")  // false

Simple enough. But now when we read this code it is far more intuitive. We take our isLessThan10 predicate and “pull it back” to work on strings by simply getting the string’s character count.

Let’s look at another example. In this week’s episode we showed how to convert the Equatable protocol into a concrete datatype, and one can define a pullback operation on it:

struct Equating<A> {
  let equals: (A, A) -> Bool

  func pullback<B>(_ f: @escaping (B) -> A) -> Equating<B> {
    return Predicate<B> { self.equals(f($0), f($1)) }
  }
}

Using the pullback operation we can induce a notion of equating on, say, a User value by only knowing how to equate integers:

import Overture

struct User { let id: Int, name: String }

let int = Equating<Int> { $0 == $1 }
let user = int.pullback(get(\\User.id))

This shows just how flexible and transformable concrete types with pullback are. Types can only conform to a protocol in a single way, but often it is completely valid to conform in multiple ways, as seen above. But when working with concrete datatypes we get to pullback conformances on one type to conformances on completely unrelated types.

Naming is hard

Although it’s unfortunate to rename such a fundamental concept after having learned it many months ago, we think it’s worth it. This name reads well and has a lot of great intuition, and we’re going to use it going forward on this series. We still think the contramap name is still important, mostly because the contra- prefix allows us to transform any concept into its contravariant dual concept, and it will be creeping into some future episodes, but from now we will be mostly using pullback.

Get started with our free plan

Our free plan includes 1 subscriber-only episode of your choice, access to 64 free episodes with transcripts and code samples, and weekly updates from our newsletter.

View plans and pricing