Function composition is a fundamental part of functional programming. This chapter is concerned with exploring the composition characteristics of the fundamental type-classes: Functor
, Applicative Functor
, Arrow
, and Monad
. We will see that Functor
embeds into Applicative Functor
, which embeds into Arrow
, which embeds into Monad
. After exploring Monad
composition, we'll also look into Monad
transformers (a technique for composing different types of Monad
). As we move through the successive types, from the most general (Functor
) to the most powerful (Monad
), we will see how they differ in the ways they can be composed.
Note that this chapter does not have the last word on composition patterns, for example, in the following chapter we will explore another pattern for composition: functional lenses.
The Functor type-class gives us a way to generalize function application to arbitrary types. Let's first look at regular function application. Suppose we defined a function of primitive types:
f :: Num a => a -> a f = (^2)
We can apply it directly to the types it was intended for:
f 5 f 5.0 –- etc
To apply the f
function to a richer type, we need to make that type an instance of Functor
and then use the fmap
function:
-- fmap function Functor fmap f (Just 5) fmap (f . read) getLine
The Functor
type-class defines fmap
:
class Functor f where fmap :: (a -> b) -> f a -> f b
Let's create our own Maybe'
type and make it an instance of Functor
:
data Maybe' a = Just' a | Nothing' deriving (Show) instance Functor Maybe' where fmap _ Nothing' = Nothing' fmap f (Just' x) = Just' (f x)
By making Maybe'
a Functor
type-class, we are describing how single-parameter functions may be applied to our type, assuming the function types align with our Functor
class, for example:
-- we can do this fmap f (Just' 7) fmap show (Just' 7) -– but still not this fmap f (Just' "7")
The fmap function lifts our function into the realm of Functor; fmap
also lifts function composition to the level of Functor
. This is described by the functor laws:
-- law of composition fmap (f . g) == fmap f . fmap g -- e.g. fmap (f . read) getLine -- is the same as (fmap f) . (fmap read) $ getLine -- identity law fmap id == id -- e.g. fmap id (Just 1) = id (Just 1)
The fmap
function is to Functor
what map
is to the List
type, as shown in the following code:
ns = map (^2) [1, 2, 3, 5, 7] -- 'map' lifts f to operate on List type
We can also write the preceding code as follows:
ns' = fmap (^2) [1, 2, 3, 5, 7]
because List is a Functor
:
instance Functor List where fmap = map
The Functor
typeclass abstracts the idea of function application to a single argument and thereby gives us a way to apply our functions to arbitrary types.