A Monad
is an Applicative
(i.e. something that you can lift binary -- hence, "n-ary" -- functions to,(1) and inject pure values into(2)) Functor
(i.e. something that you can map over,(3) i.e. lift unary functions to(3)) with the added ability to flatten the nested datatype (with each of the three notions following its corresponding set of laws). In Haskell, this flattening operation is called join
.
The general (generic, parametric) type of this "join
" operation is:
join :: Monad m => m (m a) -> m a
for any monad m
(NB all m
s in the type are the same!).
A specific m
monad defines its specific version of join
working for any value type a
"carried" by the monadic values of type m a
. Some specific types are:
join :: [[a]] -> [a] -- for lists, or nondeterministic values
join :: Maybe (Maybe a) -> Maybe a -- for Maybe, or optional values
join :: IO (IO a) -> IO a -- for I/O-produced values
The join
operation converts an m
-computation producing an m
-computation of a
-type values into one combined m
-computation of a
-type values. This allows for combination of computation steps into one larger computation.
This computation steps-combining "bind" (>>=
) operator simply uses fmap
and join
together, i.e.
(ma >>= k) == join (fmap k ma)
{-
ma :: m a -- `m`-computation which produces `a`-type values
k :: a -> m b -- create new `m`-computation from an `a`-type value
fmap k ma :: m ( m b ) -- `m`-computation of `m`-computation of `b`-type values
(m >>= k) :: m b -- `m`-computation which produces `b`-type values
-}
Conversely, join
can be defined via bind, join mma == join (fmap id mma) == mma >>= id
where id ma = ma
-- whichever is more convenient for a given type m
.
For monads, both the do
-notation and its equivalent bind-using code,
do { x <- mx ; y <- my ; return (f x y) } -- x :: a , mx :: m a
-- y :: b , my :: m b
mx >>= (\x -> -- nested
my >>= (\y -> -- lambda
return (f x y) )) -- functions
can be read as
first "do"
mx
, and when it's done, get its "result" asx
and let me use it to "do" something else.
In a given do
block, each of the values to the right of the binding arrow <-
is of type m a
for some type a
and the same monad m
throughout the do
block.
return x
is a neutral m
-computation which just produces the pure value x
it is given, such that binding any m
-computation with return
does not change that computation at all.
(1) with liftA2 :: Applicative m => (a -> b -> c) -> m a -> m b -> m c
(2) with pure :: Applicative m => a -> m a
(3) with fmap :: Functor m => (a -> b) -> m a -> m b
There's also the equivalent Monad methods,
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
return :: Monad m => a -> m a
liftM :: Monad m => (a -> b) -> m a -> m b
Given a monad, the other definitions could be made as
pure a = return a
fmap f ma = do { a <- ma ; return (f a) }
liftA2 f ma mb = do { a <- ma ; b <- mb ; return (f a b) }
(ma >>= k) = do { a <- ma ; b <- k a ; return b }