Consider a list of natural numbers and a list of strings. Their element types differ, but the list structure and its structural operations remain the same. Let denote the element type: the constructor takes it and produces the concrete type . Therefore, the kind of is .
A C++ programmer may reasonably point out that the templates and already abstract over the element type, while iterators and ranges let the same algorithms work with different structures, such as lists and trees. Those algorithms, however, abstract over the traversal interface of an existing object rather than over a type constructor that preserves the shape of a context.
Haskell takes the next step. Both and have kind , so the type class can abstract over the constructor itself. The operation takes a function , transforms values inside , and returns while preserving the shape of the context. This is polymorphism not only over the element type but also over the context constructor. C++ can emulate it with templates and overloads, but the language has no direct standard counterpart to type classes and uniform higher-kinded polymorphism. The type class itself is declared as follows:
class Functor f where
fmap :: (a -> b) -> f a -> f b -- apply a function inside context fIn this declaration, is a variable for a type constructor of kind , not for a concrete type. The class specifies the common shape of , while each instance defines how its context is preserved. The standard instances for lists and work as follows:
instance Functor [] where
fmap _ [] = []
fmap g (x : xs) = g x : fmap g xs
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap g (Just x) = Just (g x)As we can see, a implementation depends on the context constructor but not on the concrete type of its elements. Now let us place the number 2 in a context. We begin with the key idea: a value in a context.
A context lets us define the rules for exceptional cases once and then apply ordinary functions without scattering checks throughout an algorithm. More powerful abstractions built on this idea can chain computations and specify execution strategies, but itself guarantees only transformation through . Suppose a JavaScript computation returns instead of a value. We then have to write an ; if the result is passed onward, such checks begin to spread through the algorithm. Haskell represents absence explicitly with : its instance leaves unchanged and applies the function to the value inside . Mapping over an empty list likewise returns an empty list. An ordinary function can be applied directly to a plain value; a value in a context would first have to be unpacked by hand. takes care of that:
fmap (+3) (Just 2) -- Just 5
fmap (+3) Nothing -- Nothinghas an infix synonym: .
Every instance must obey two laws: identity and composition.
fmap id == id -- identity
fmap (g . h) == fmap g . fmap h -- compositionThese laws mean that changes values but not the shape of the context: it neither rearranges a list nor turns into . They also state that mapping two functions in sequence is equivalent to mapping their composition.
The main instances:
-- Maybe: the value may be absent
fmap (+3) (Just 2) -- Just 5
-- A list: zero or more values; fmap = map
fmap (+3) [1, 2, 3] -- [4, 5, 6]
-- Either e: only Right is mapped
fmap (+3) (Right 2) -- Right 5
fmap (+3) (Left "oops") -- Left "oops"A functor is a mapping between categories that preserves and composition. In the usual model of Haskell, functors are endofunctors on the category of types and functions, and the laws correspond exactly to the functor axioms.