What does a functor actually look like in various programming languages? We already said it’s something you can use to map, so let’s take a look at some language’s mapping functions:
- Clojure has
map
,mapv
andfnil
- Haskell has
map
andfmap
. (It also has<$>
, which is the same asfmap
) - C# has
Select
- Java’s streams library has
map
andmapToInt
So, are any of these functor mapping operations? Well, it won’t take a genius to guess that fmap
does the right thing. map
in Haskell does the same thing, but only works for List
s. The definition of fmap
for list is map
, so it’s pretty much a wash. (*)
Land of Compromises
The others? Well, kind of, but you tend to need to squint a bit. The problem is that if you map over an identity function, (e.g. x => x
in C#) you should get the same type back as you put in. And actually, that’s very rarely true. map
in Clojure can be called on a vector and will return a lazy list. mapv
can be called on a lazy list and get back a vector. map
in Java and Select
in C# send an interface type to the same interface type, but rarely return the exact same type as you were expecting.
Moreover, there isn’t a general mapping interface that lots of functor-like things implement and there isn’t any way to make one. This isn’t a problem for list comprehension, but it horribly breaks functors as a general model of computation. (#) You can still use the concepts, but you’ll end up with a lot of code duplication. Indeed, you’ll probably already have this code duplication and be thinking of it as a pattern. As is all to often the case, low level programming patterns reveal deficiencies in your programming language.
There’s good reasons for these mapping functions not behaving exactly like a functor, though: performance. The haskell compiler has everything, not just lists, as lazy and can optimize types away. Clojure, C# and Java can’t and treat them as hard optimization boundaries.
Haskell ain’t perfect either
We already established in the previous article that there are plenty of functors that have nothing to do with types. Haskell’s Functor type class is therefore only a functor on the category of Haskell types (usually referred to as Hask). This seems good enough, but actually it isn’t.
Consider a set of values. You can easily define a mapping function that satisfies the functor rules. Sadly, Set
in Haskell isn’t a Haskell Functor. This is because Set
imposes a condition on its values that they be sortable. Whilst this isn’t a problem for real Functor’s, it’s a problem for Haskell functors because type classes don’t admit restrictions on their type parameters. To put it another way, Functor
in Haskell is a functor over the whole of Hask, never a subcategory. For that matter, you can’t do the (*2) functor that I described last time in any sensible way because you can’t restrict its action to integers.
It turns out this problem is fixable, with Rank-2 typeclasses, but don’t hold your breath for that to land in Prelude any time soon. In the meantime, you can’t use Functor
to represent functors with domain type restrictions.
(*) Many smart Haskellers believe (and I agree with them(+)) map
should work on all functors and fmap
should be retired. There’s a general theme here: the standard libraries are showing their age and need work to simplify them.
(+) If you’ve never seen Alan Rickman play Obadiah Slope, you’re missing out.
(#) If you’re prepared to lose information, all you really need is reduce/Foldable anyway.