Functor Composition
Composition is the beauty of functional programming & haskell with it's typeclasses excels at this.
Let's see what Functors
are and how they compose.
Almost all languages have a way to apply function on all elements of a data structure.
eg. Map in ruby & javascript
Haskell goes beyond just mapping over array-like objects & allows defining an abstract method for multiple structures.
Functors
If we consider the example of a Maybe
data-type (which may
hold a value), a common
use-case is to apply function to inner value if it is present:
x = Just 1
fmap (+1) x -- # => Just 2
y = Nothing
fmap (+1) y -- # => Nothing
One can think of structures as boxes. fmap
applies function to element inside that box, wraps it in the same box
and returns it. link
fmap
is a function of Functor
typeclass & structures like List & Maybe implement Functor typeclass:
class Functor f where
fmap :: (a -> b) -> f a -> f b -- f is abstract structure here
Well-defined functors are structure-preserving - ie. they don't modify the structure.
fmap (+2) [1,2,3] -- # => [3,4,5]
Composing Functors
Now suppose we want to apply function to nested lists
fmap (fmap (+1)) [[1,2,3],[4,5]] -- # => [[2,3,4],[5,6]]
Can we compose these two fmaps
?
fmap1 :: (a -> b) -> (f1 a -> f1 b)
fmap2 :: (x -> y) -> (f2 x -> f2 y)
In haskell, all functions are curried - which means fmap1
when supplied with
one argument (a -> b
) returns another function which takes f1 a
and returns f1 b
.
Substitute x -> y
with f1 a -> f1 b
: (replace x
with f1 a
& y
with f1 b
)
fmap2 :: (f1 a -> f1 b) -> (f2 (f1 a) -> f2 (f1 b))
See f2 (f1 a)
there? Nested structure! We just got fmap
for a nested structure!
We took the output of fmap1
- (f1 a -> f1 b)
and passed it as first argument to fmap2
. Which is nothing but function composition:
(.) :: (b -> c) -> (a -> b) -> a -> c
Output of function a -> b
(b) is passed to b -> c
.
Above example becomes:
(fmap2 . fmap1) :: (a -> b) -> (f2 (f1 a) -> f2 (f1 b))
Cleaning it up a bit & adding typeclass constraints:
(fmap . fmap) :: (Functor f1, Functor f2) => (a -> b) -> f1 (f2 a) -> f1 (f2 b)
Note the entire structure f1 (f2)
is preserved in the output - only a
s are transformed to b
s.
Now we can do this:
(fmap . fmap) (+1) [[1,2,3],[4,5]] -- # => [[2,3,4],[5,6]]
We jumped inside two lists & applied the function (+1)
. But note those two structures(functors) need not be lists or rather need not even be of the same type.
They can be different (f1
& f2
in above definition), although both must have instances of Functor.
(fmap . fmap) (+1) [Just 1, Nothing, Just 2]
-- # => [Just 2, Nothing, Just 3]
But why limit to just two?
(fmap . fmap . fmap) (+1) [Just [1], Nothing, Just [2]]
-- # => [Just [2],Nothing,Just [3]]
Being pure & structure preserving allows Functors to compose in such a neat way. Just beautiful!