This is one of those neat cases where I think it's simpler to grasp the more general case first, and then think about the specific case. So let's think about functors. We know that functors provide a way to map functions over a structure --
class Functor f where
fmap :: (a -> b) -> f a -> f b
But what if we have two layers of the functor? For example, a list of lists? In that case we can use two layers of fmap
>>> let xs = [[1,2,3], [4,5,6]]
>>> fmap (fmap (+10)) xs
[[11,12,13],[14,15,16]]
But the pattern f (g x)
is exactly the same as (f . g) x
so we could write
>>> (fmap . fmap) (+10) xs
[[11,12,13],[14,15,16]]
What is the type of fmap . fmap
?
>>> :t fmap.fmap
:: (Functor g, Functor f) => (a -> b) -> f (g a) -> f (g b)
We see that it maps over two layers of functor, as we wanted. But now remember that (->) r
is a functor (the type of functions from r
, which you might prefer to read as (r ->)
) and its functor instance is
instance Functor ((->) r) where
fmap f g = f . g
For a function, fmap
is just function composition! When we compose two fmap
s we map over two levels of the function functor. We initially have something of type (->) s ((->) r a)
, which is equivalent to s -> r -> a
, and we end up with something of type s -> r -> b
, so the type of (.).(.)
must be
(.).(.) :: (a -> b) -> (s -> r -> a) -> (s -> r -> b)
which takes its first function, and uses it to transform the output of the second (two-argument) function. So for example, the function ((.).(.)) show (+)
is a function of two arguments, that first adds its arguments together and then transforms the result to a String
using show
:
>>> ((.).(.)) show (+) 11 22
"33"
There is then a natural generalization to thinking about longer chains of fmap
, for example
fmap.fmap.fmap ::
(Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))
which maps over three layers of functor, which is equivalent to composing with a function of three arguments:
(.).(.).(.) :: (a -> b) -> (r -> s -> t -> a) -> (r -> s -> t -> b)
for example
>>> import Data.Map
>>> ((.).(.).(.)) show insert 1 True empty
"fromList [(1,True)]"
which inserts the value True
into an empty map with key 1
, and then converts the output to a string with show
.
These functions can be generally useful, so you sometimes see them defined as
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(.:) = (.).(.)
so that you can write
>>> let f = show .: (+)
>>> f 10 20
"30"
Of course, a simpler, pointful definition of (.:)
can be given
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(f .: g) x y = f (g x y)
which may help to demystify (.).(.)
somewhat.