What Does fmap Preserve?
June 2, 2014
A common way of introducing
fmap is saying that it only changes the values in a container, and not its structure. Leaving behind the the functors-as-containers metaphor, we can convey the same idea by saying that
fmap leaves the context of the values in a
Functor unchanged. But what, exactly, is the “context” or “structure” being preserved? “It depends on the functor”, though correct, is not an entirely satisfactory answer. The functor laws, after all, are highly abstract, and make no mention of anything a programmer would be inclined to call “structure” (say, the skeleton of a list); and yet the preservation we alluded to follows from them. After struggling a bit with this question, I realised that the incompatibility is only apparent. This post shows how the tension can be resolved through the mediation of parametricity and naturality, two concepts from different domains that are intertwined in Haskell.
Categorical Cautionary Comment
A correct, if rather cruel, answer to “Why does
fmap preserve structure?” would be “By definition, you silly!” To see what would be meant by that, let’s have a look at the functor laws.
fmap id = id -- 1st functor law fmap (g . f) = fmap g . fmap f -- 2nd functor law
fmap is a mapping of functions that takes identity to identity, and composed functions to the corresponding composed functions. Identity and composition make up the structure, in the mathematical sense, of a category. In category theory, a functor is a mapping between categories that preserves category structure. Therefore, the functor laws ensure that Haskell
Functors are indeed functors; more precisely, functors from Hask to Hask, Hask being the category with Haskell types as objects and Haskell functions as arrows.1
That functors preserve category structure is evident. However, our question is not directly about “structure” in the mathematical sense, but with the looser acception it has in programmer parlance. In what follows, our goal will be clarifying this casual meaning.
What Can You Do With a Function?
As an intial, fuzzy characterisation, we can say that, given a functorial value, the
Functor context is everything in it other than the wrapped values. Starting from that, a straightforward way of showing why
fmap preserves context involves parametric polymorphism; more specifically, the preservation is ensured by the wild generality of the types in the signature of
fmap :: (Functor t) => (a -> b) -> (t a -> t b)
We will look at
fmap as a function of one argument which converts a plain
a -> b function into a function which operates on functorial values. The key fact is that there is very little we can do with the
a -> b function when defining
fmap. Composition is not an option, as choosing a function other than
id to compose it with would require knowledge about the
b types. The only thing that can be done is applying the function to any
a values we can retrieve from the
t a functorial value. Since the context of a
t a value, whatever it is, does not include the
a values, it follows that changes to the context cannot depend on the
a -> b function. Given that
fmap takes no other arguments, any changes in the context must happen for any
a -> b arguments uniformly. The first functor law, however, says that
fmap id = id, and so there is one argument,
id, which leads to no changes in the context. Therefore,
fmap never changes the context.
The informal argument above can be made precise through a proper type theory treatment of parametricity. Philip Wadler’s Theorems for free! is a well-known example of such work. However, a type theory approach, while entirely appropriate, would have us taking concrete Haksell types for granted and only incidentally concluding they are functors; in contrast, our problem begins with functors. For that reason, we will follow a different path and look at the issue from a primarily categorical point of view.
What Is a Context, After All?
In the spirit of category theory, we will now focus not on the types but on the functions between them. After all, given functional purity any interesting properties of a Haskell value can be verified with suitable functions. Let’s start with a few concrete examples of how the context of a
Functor can be probed with functions.
length :: [a] -> Int
The length of a list is perhaps the most obvious example of a structural property. It depends only on the list skeleton, and not at all on the values in it. The type of
length, with a fully polymorphic element type which is not mentioned by the result type, reflects such an independence. An obvious consequence is that
fmap, which only affects the list elements, cannot change the length of a list. We can state that like this:
length xs = length (fmap f xs)
Or, in a more categorical fashion:
length = length . fmap f
Our second example of a structure-probing function will be
reverse :: [a] -> [a]
While the result value of
reverse obviously depends on the list elements,
reverse cannot actually modify the elements, given that the function is fully polymorphic on the element type.
fmap applied to a list after reversing it will thus affect the same element values there were before the reversal; they will only have been rearranged. In other words,
fmap commutes with
fmap f . reverse = reverse . fmap f
Our final example will be
listToMaybe :: [a] -> Maybe a
listToMaybe is a safe version of
head, which returns
Nothing when given an empty list. Again, the function is fully polymorphic in the element type, and so the value of the first element cannot be affected by it. The scenario is very similar to what we have seen for
reverse, and an analogous property holds, with the only difference being that
fmap is instantiated at a different
Functor at each side of the equation:
-- Maybe-fmap on the left, -fmap on the right. fmap f . listToMaybe = listToMaybe . fmap f
Earlier we said that the
Functor context consists of everything but the wrapped values. Our examples illustrate how parametric polymorphism makes it possible to keep that general idea while putting functions rather than values under the spotlight. The context is all that can be probed with functions fully polymorphic on the type parameter of the
Functor; or, taking the abstraction further, the context is the collection of functions fully polymorphic on the type parameter of the
Functor. We now have done away with the fuzziness of our preliminary, valure-centric definition. The next step is clarifying how that definition relates to
Your Freedom Comes Naturally
By identifying the
Functor context with polymorphic functions, we can also state the context-preserving trait of
fmap through commutativity equations like those shown in the above examples. For an arbitrary context-probing function
r, the equation is:
-- f is arbitrary, and so are the involved functors. fmap f . r = r . fmap f
The equations for
listToMaybe clearly have that shape.
length does not seem to fit at first sight, but that can be easily solved by lifting it to a constant functor such as the one provided by
lengthC :: [a] -> Const Int a = Const . length lengthC -- length = getConst . lengthC -- For constant functors, fmap f = id regardless of f. fmap f . lengthC = lengthC . fmap f
A similar trick can be done with the
Identity functor to make functions in which the type parameter of the
Functor appears bare, such as
Just :: a -> Maybe a, fit our scheme.
It turns out that there is a category theory concept that captures the commutativity property we are interested in. A natural transformation is a translation between functors which preserves arrows being mapped through them. For Haskell
Functors, that amounts to preserving functions being mapped via
fmap. We can display the relation through a diagram:
The naturality condition matches our commuativity property. Indeed, polymorphic functions are natural transformations between Haskell
Functors. The proof of this appealing result is not trivial, and requires some theoretical work, just like in the case of the closely related results about parametricity we alluded to earlier. In any case, all it takes to go from “natural transformations preserve
fmap” to “
fmap preserves natural transformations” is tilting our heads while looking at the diagram above!
Given how we identified
Functor contexts, polymorphic functions and natural transformations, we can finally give a precise answer to our question. The context consists of natural transformations between functors, and therefore
fmap preserves it.
Structures and Structures
Earlier on, we have said that we would not be directly concerned with structure in the sense mathematicians use the word, but only with the fuzzy Haskell concept that sometimes goes by the same name. To wrap things up, we will now illustrate the fact that both acceptions are not worlds apart. Let’s have another look at the second functor law, which states that
fmap preserves composition:
fmap (g . f) = fmap g . fmap f
Structure, in the mathematical sense, refers to some collection of interesting operations and distinguished elements. In this example, the relevant operation is function composition, which is part of the structure of the Hask category. Besides that, however, we are now able to note the uncanny resemblance between the shapes of the law, which says that it does not matter whether we compose
g before applying
fmap, and of the commutativity properties we used to characterise functorial contexts. The upshot is that by identifying context and structure of a
Functor with polymorphic functions, we retain much of the spirit of the mathematical usage of structure. The interesting operations, in our case, are the polymorphic functions with which the context is probed. Perhaps it even makes sense to keep talking of structure of a
Functor even after dropping the container metaphor.
fmap Preserves fmap
Speaking of the second law, we will, just for kicks, use it to show how to turn things around and look at
fmap as a natural transformation between
Functors. In order to do so, we have to recall that
fmap for the function functor:
-- First, we rewrite the second law in a more suggestive form: fmap (g . f) = fmap g . fmap f fmap (((.) g) f) = (.) (fmap g) (fmap f) fmap . (.) g = ((.) . fmap) g . fmap -- Next, some synonyms to indicate the Functors fmap leads to. -- fmap from identity to t fmap_t :: (Functor t) => (->) a b -> (->) (t a) (t b) = fmap fmap_t -- fmap from identity to ((->) a) fmap_fun :: (b -> c) -> ((->) a b -> (->) a c) = (.) fmap_fun -- fmap from identity to the composite functor ((->) (t a)) . t fmap_fun_t :: (Functor t) => (b -> c) -> ((->) (t a) (t b) -> (->) (t a) (t c)) = fmap_fun . fmap_t fmap_fun_t -- The second law then becomes: . fmap_fun g = fmap_fun_t g . fmap_t fmap_t -- That, however, shows fmap_t is a natural transformation: fmap . fmap g = fmap g . fmap
a in the signature of
fmap_t above, we get one functor on either side of the outer function arrow:
((->) a) on the left and
((->) (t a)) . t on the right.
fmap is a natural transformation between these two functors.
In The Holy Trinity, Robert Harper comments on the deep connection between logic, type theory and category theory that allows us to shift seamlessly between the categorical and the type theoretical perspectives, as we have done here.
You Could Have Defined Natural Transformations by Dan Piponi is a very clear introduction to natural transformations in a Haskell context.
We have already mentioned Philip Wadler’s Theorems for free!, which is a reasonably accessible introduction to the free theorems. Free theorems are results about functions that, thanks to parametric polymorphism, can be deduced from the type of the function alone. Given suitable generalisations, free theorems and naturality conditions provide two parallel ways of reaching the same results about Haskell functions.
Free Theorems Involving Type Constructor Classes, a functional pearl by Janis Voigtländer that illustrates how free theorem generation can be generalised to types parametric on type constructors and type classes.
For an explicitly categorical perspective on parametricity, a good place to start if you are willing to dig into theory is the section on parametricity in Some Aspects of Categories in Computer Science by Philip J. Scott.
A category theory primer would be too big a detour for this post. If the category theory concepts I just mentioned are new to you, I suggest the following gentle introductions for Haskellers, which have very different approaches: Haskell Wikibook chapter on category theory, and Gabriella Gonzalez’s posts The category design pattern and The functor design pattern.↩︎
Post licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.