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…
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.
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.
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.