In FP we love to compose things.
When we have two functions:
f :: a -> b
g :: b -> c
then we can compose them into one function:
h :: a -> c
h = g . f -- "dot" is Scala's "compose"
But what if f
returns a "contextful" value?
f :: a -> F b
g :: b -> c
Where F
can be anything, like Option
or Future
or IO
, etc.? We can't just use "compose" to get a final result.
But we can "map over" F
to "transform value within it", so we can have a -> F c
:
h :: a -> F c
h a = fmap g (f a) -- f(a).map(g) in Scala
When our context F
(Option
, Future
, IO
, List
, etc.) allows this "map over" operation we call this context a Functor
.
fmap
must obey some intuitive laws which I'll skip for now, but now we know what Functor
is: a "context" that we can "map over" to transform its value inside.
fmap
therefore looks like:
fmap :: Functor f => (a -> b) -> f a -> f b
But what if both f
and g
return "contextful" values?
f :: a -> M b
b :: b -> M c
How do we compose these? If M
is a functor, we can't just fmap g (f x)
because it doesn't give us M c
.
Therefore we need another operation that takes f
and g
and gives us our final M c
.
If such an operation exists for a given context M
then we call M
a Monad
. This operation is usually called bind
:
h :: a -> M c
h a = bind g (f a) -- in Scala: f(a).flatMap(g)
In Haskell bind
is an operator:
(>>=) :: Monad m => m a -> (a -> M b) -> M b
so we can do:
h :: f a >>= g
So a Monad
is a context that allows us to "chain" contextful actions. It enforces sequence naturally (nothing to do with IO specifically) because in order to call the 2nd action (g :: b -> M c
) it needs to calculate b
first (to pass it into g
).
A Monad
also defines an operation to "construct" the context (to wrap the pure value into a context).
This operation is called return
:
return :: Monad m => a -> M a
so, for example:
return 2 :: Maybe Int -- result: Just 2
return 2 :: [Int] -- result: [2]
return 2 :: IO Int -- result: IO 2
Maybe
is a monad, so we can compose:
boss :: Department -> Maybe User
userEmail :: User -> Maybe Email
departmentEmail :: Department -> Maybe Email
departmentEmail dep = boss dep >>= userEmail
Future
is a monad, so we can compose:
def createEC2Instance(ec2: EC2Params): Future[EC2] = ???
def startEC2Instance(ec2: EC2): Future[EC2] = ???
def upEC2(ec2: EC2Params): Future[EC2] =
createEC2Instance(ec2).flatMap(startEC2Instance)
We can briefly talk about monad laws now, which are mostly intuitive and trivial, like "if I don't do anything with the value, then nothing changes", etc.