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.