Having read the book Learn you a Haskell For Great Good, and the very helpful wiki book article Haskell Category Theory which helped me overcome the common category mistake of confusing category objects with the programming objects, I still have the following question:
Why must fmap
map over every elements of a List?
I like that it does, I just want to understand how this is justified category theoretically. ( or perhaps it is easier to justify using HoTT? )
In Scala notation, List
is a functor that takes any type and maps that into an type from the set of all list types, eg it maps the type Int
to the type List[Int]
and it maps the functions on Int
eg
Int.successor: Int => Int
to Functor[List].fmap(successor) : List[Int] => List[Int]
Int.toString: Int => String
to Functor[List].fmap(toString): List[Int] => List[String]
Now every instance of List[X]
is a monoid with a empty
function (mempty
in Haskell) and combine
function (mappend
in Haskell). My guess is that one can use the fact that Lists are Monoids, to show that map
has to map all elements of a list. My feeling here is that if one adds the pure
function from Applicative, this gives us a list with just one element of some other type. e.g Applicative[List[Int]].pure(1) == List(1)
. Since map(succ)
on those elements gives us the singleton list with the next element, this covers all those subsets. Then I suppose the combine
function on all those singletons gives us all the other elements of the lists. Somehow I suppose that constrains the way map has to work.
Another suggestive argument is that map
has to map functions between lists. Since every element in a List[Int]
is of type Int, and if one maps to List[String]
one has to map every element of it, or one would not the right type.
So both of those arguments seem to point in the right direction. But I was wondering what was needed to get the rest of the way.
Counterexample?
Why is this not a counterexample map function?
def map[X,Y](f: X=>Y)(l: List[X]): List[Y] = l match {
case Nil => Nil
case head::tail=> List(f(head))
}
It seems to follow the rules
val l1 = List(3,2,1)
val l2 = List(2,10,100)
val plus2 = (x: Int) => x+ 2
val plus5 = (x: Int) => x+5
map(plus2)(List()) == List()
map(plus2)(l1) == List(5)
map(plus5)(l1) == List(8)
map(plus2 compose plus5)(l1) == List(10)
(map(plus2)_ compose map(plus5)_)(l1) == List(10)
Ahh. But it does not fit the id law.
def id[X](x: X): X = x
map(id[Int] _)(l1) == List(3)
id(l1) == List(3,2,1)
This relies on a theoretical result called "parametricity", first defined by Reynolds and then developed by Wadler (among others). Perhaps the most famous paper on this topic is "Theorems for free!" by Wadler.
The key idea is that from the polymorphic type of a function only, we can get some information about the semantics of the function. For instance:
foo :: a -> a
From this type alone, we can see that, if foo
terminates, it is the identity function. Intuitively, foo
can not distinguish between different a
s since in Haskell we do not have e.g. Java's instanceof
which can inspect the actual runtime type. Similarly,
bar :: a -> b -> a
must return the first argument. And baz :: a -> a -> a
must return either the first or the second. And quz :: a -> (a -> a) -> a
must apply some fixed number of times the function to the first argument. You probably get the idea now.
The general property which can be inferred from a type is quite complex, but luckily it can be computed mechanically. In category-theory, this is related to the notion of natural transformation.
For the map
type, we get the following scary property:
forall t1,t2 in TYPES, f :: t1 -> t2.
forall t3,t4 in TYPES, g :: t3 -> t4.
forall p :: t1 -> t3.
forall q :: t2 -> t4.
(forall x :: t1. g (p x) = q (f x))
==> (forall y :: [t1].
map_{t3}_{t4} g (map2_{t1}_{t3} p y) =
map2_{t2}_{t4} q (map_{t1}_{t2} f y))
Above, map
is the well-known map function, while map2
is any arbitrary function which has type (a -> b) -> [a] -> [b]
.
Now, further assume that map2
satisfies the functor laws, in particular map2 id = id
. We then can choose p = id
and t3 = t1
. We get
forall t1,t2 in TYPES, f :: t1 -> t2.
forall t4 in TYPES, g :: t1 -> t4.
forall q :: t2 -> t4.
(forall x :: t1. g x = q (f x))
==> (forall y :: [t1].
map_{t1}_{t4} g (map2_{t1}_{t1} id y) =
map2_{t2}_{t4} q (map_{t1}_{t2} f y))
Applying the functor law on map2
:
forall t1,t2 in TYPES, f :: t1 -> t2.
forall t4 in TYPES, g :: t1 -> t4.
forall q :: t2 -> t4.
(forall x :: t1. g x = q (f x))
==> (forall y :: [t1].
map_{t1}_{t4} g y =
map2_{t2}_{t4} q (map_{t1}_{t2} f y))
Now, let's choose t2 = t1
and f = id
:
forall t1 in TYPES.
forall t4 in TYPES, g :: t1 -> t4.
forall q :: t1 -> t4.
(forall x :: t1. g x = q x)
==> (forall y :: [t1].
map_{t1}_{t4} g y =
map2_{t1}_{t4} q (map_{t1}_{t1} id y))
By the functor law of map
:
forall t1, t4 in TYPES.
forall g :: t1 -> t4, q :: t1 -> t4.
g = q
==> (forall y :: [t1].
map_{t1}_{t4} g y =
map2_{t1}_{t4} q y)
which means
forall t1, t4 in TYPES.
forall g :: t1 -> t4.
(forall y :: [t1].
map_{t1}_{t4} g y =
map2_{t1}_{t4} g y)
which means
forall t1, t4 in TYPES.
map_{t1}_{t4} = map2_{t1}_{t4}
Summing up:
If map2
is any function with polymorphic type (a -> b) -> [a] -> [b]
, and such that it satisfies the first functor law map2 id = id
, then map2
must be equivalent to the standard map
function.
Also see a related blog post by Edward Kmett.
Note that in Scala the above holds only if you do not use x.isInstanceOf[]
, and other reflection tools, which can break parametricity.