Learn Haskell Part 6: Express sequence of actions with Monads
Choice from past context
The problem with ifThenElseA
There is a problem with Applicative. It returns weird results when using this abstraction to make decisions. We implemented before a function called ifThenElse:
ifThenElse p y n = if p then y else n
We applied this function to values wrapped in the Maybe Functor using <*> (merge) operator, but did not see really how it worked when a Nothing is given. Let's try:
λ> ifThenElseA p y n = fmap ifThenElse p <*> y <*> n
λ> ifThenElseA Nothing (Just 1) (Just 2)
Nothing
For now, nothing really surprising, if we cannot even test the predicate, well, we cannot chose a value. Let's try something else:
λ> ifThenElseA (Just True) Nothing (Just 2)
Nothing
Ok, still understandable, if we chose the y value, since it's Nothing, we get Nothing. Let's continue:
λ> ifThenElseA (Just True) (Just 1) Nothing
Nothing
Now it's weird... Since we get Just True as the condition result, shouldn't we get Just 1 ? Why do we have Nothing as a result ? Well, it is because we are not doing things in sequence.
In an imperative language, we are used to see these constructs often:
In this example, the if structure choses between two programs to execute. We don't call them programs but branches or code blocks or even statements, but the important thing here is that they are not executed yet, the if statement choses which branch to execute next. So it will work, because the branch that tried to read the first element of the array will never be executed if the array is empty.
But in Haskell, we work with values, not programs. So in order to call ifThenElseA, we need to have its 3 arguments already computed. If we develop (beta-reduce) the expression ifThenElseA (Just True) Nothing (Just 2), we get this:
case (Just True) of
Nothing -> Nothing
-- see, here, the second argument is directy matched, and outputs Nothing
-- because we bypass everything
Just p -> case Nothing of
Nothing -> Nothing
Just y -> case (Just 2) of
Nothing -> Nothing
-- and below here, we have the if then else expression
-- which will be evaluated only if all arguments are Just
Just n -> if p then y else n
A solution for the Maybe type
If we want our code to have a similar semantic as the JavaScript code above, we would need a function that develops to this:
case maybeP of
Nothing -> Nothing
Just p -> if p then maybeY else maybeN
Here, maybeP, is first evaluated, and we chose which of maybeY or maybeN needs to be evaluated next. You can implement a function ifThenElseFixed that does exactly this, and see for yourself.
-- write this in a module
ifThenElseFixed p y n =
case p of
Nothing -> Nothing
Just p -> if p then y else n
λ> -- and try it in ghci
λ> ifThenElseFixed Nothing (Just 1) (Just 2)
Nothing
λ> ifThenElseFixed (Just False) (Just 1) Nothing
Nothing
λ> ifThenElseFixed (Just True) (Just 1) Nothing
Just 1
It works ! We managed to chose which context to evaluate next, based on a previously evaluated context.
The Monad type class
A Monad is a Functor which allows to chose which context to evaluate next, based on a previously evaluated context. Exactly like what we did with ifThenElseFixed earlier. Let's use this abstraction for more useful things, and understand how it allows that.
The case of evalRPNExpression
Recall evalRPNExpression:
evalRPNExpression s [] = s
evalRPNExpression s (sym:syms) =
case evalRPNSymbol s sym of
Left err -> Left err
Right s -> evalRPNExpression s syms
It has roughly the same form as the ifThenElseFixed function we saw earlier. Not convinced ? Look:
evalRPNExpression s [] = s
evalRPNExpression s (sym:syms) =
case result of
Left err -> Left err
Right s -> go s
where
result = evalRPNSymbol s sym
go s = evalRPNExpression s syms
ifThenElseFixed p y n =
case result of
Nothing -> Nothing
Just p -> go p
where
result = p
go p = if p then y else n
Ok, they work on different context types, one is Result (which is an alias for Either String) and the other is Maybe. But the type pattern is the same if we abstract the Functor:
- They both work on a
resultvalue of typef a - They both apply a function
goof typea -> f b - They both return a value of type
f b
For evalRPNExpression we have:
f = Resulta = RPNSymbolb = RPNStack
For ifThenElseFixed we have:
f = Maybea = Boolb = x
It also works with evalRPN, it can be rewritten in terms of result and go:
evalRPN str = case result of
Left error -> Left error
Right exp -> go exp
where
result = readRPNExpression str
go exp = evalRPNExpression [] exp
Monad definition
As always, when we study a type class, we first look at its API:
λ> :i Monad
type Monad :: (* -> *) -> Constraint
-- Defined in ‘GHC.Base’
-- Defined in ‘GHC.Base’
To be a Monad, you need:
- To be a
Applicative(class Applicative m => Monad m where) - To implement the
(>>=)operator ({-# MINIMAL (>>=) #-})
Take a closer look at (>>=):
Does it ring any bell ? Don't remember the types of result and go in previous code ? Yes, that's it, this is exactly the function we need to combine them. It takes some value of type a wrapped in a Monad m, a function that will work on this value and produce a new wrapped value, and it gives the produced value along with a final context (the wrapping Monad). Try rewriting ifThenElseFixed, evalRPNExpression and evalRPN with (>>=):
ifThenElseFixed p y n = result >>= go
where
result = p
go p = if p then y else n
evalRPNExpression s [] = s
evalRPNExpression s (sym:syms) = result >>= go
where
result = evalRPNSymbol s sym
go s = evalRPNExpression s syms
evalRPN s = result >>= go
where
result = readRPNExpression s
go p = evalRPNExpression [] p
Since the expressions are very simple, we can even get rid of result and go objects and just copy paste their definition:
ifThenElseFixed p y n = p >>= (\p -> if p then y else n)
evalRPNExpression s [] = s
evalRPNExpression s (sym:syms) = evalRPNSymbol s sym >>= (\s -> evalRPNExpression s syms)
-- we can even use partial application to make the expression more elegant
evalRPN s = readRPNExpression s >>= evalRPNExpression []
You may already have used Monads in your life if you worked with the Promise type in JavaScript. The operator (>>=) is exactly the same as the .then() method. Imagine if readRPNExpression and evalRPNExpression were async operations, you may have implemented evalRPN as:
This is equivalent to this:
Yes, async in JavaScript is a Monad ! It encapsulates the whole "chain together functions to call when the result is ready" in this then() method. The implementation could be very complex, it's not your concern, you just have to use then().
Kleisli arrow
Don't run away ! I swear this title is not that scary ! When we worked on pure functions (a -> b) we had lots of helper functions like foldl, or (.) (composition operator) ...
These functions allow us to express powerful programs by combining smaller functions. But by working with Monads we lost them... Or did we ?.. Hey Vsauce... Of course now our functions look like a -> m b instead of a -> b, so we cannot use normal composition anymore. But could we rebuild a special composition operator that works on functions of the shape a -> m b using (>>=) ? (Don't forget to use holes to help you)
mcompose f g = \a -> g a >>= f
We talked before about the arrow type, which is a fancy name for functions. Arrows are simple functions from values of some type to values of some other type. They can be combined together to create more complex functions, we saw their power. And arrows of the shape a -> m b, which we can call "monadic" functions have the same powers as a -> b arrows. They real name is Kleisli arrows, named after the mathematician Heinrich Kleisli who discovered them.
So we saw that composition is still possible using these monadic functions. Let's see what other operations we can still do. Before modifying our functions, we implemented evalRPNExpression with the foldl function. Since monadic functions have (at least) the same powers as normal ones, we can have a monadic version of foldl. It is called foldM, and here is its type, compared to foldl type:
And it is the perfect function to re-implement readRPNExpression as we did before but within the Result monad. Try to implement it:
-- in developed form
evalRPNExpression stack expr = foldM (\stack symbol -> evalRPNSymbol stack symbol) stack expr
-- using partial application
evalRPNExpression = foldM evalRPNSymbol
As always, after modifying the code, we reload the code and test it:
λ> :r
λ> evalRPN "1 2 + 3 *"
Right [9]
λ> evalRPN "1 2 + *"
Left "Stack underflow at symbol *"
And of course it still works, we just changed the very specific code to generic code. This will come in handy later.