Useful Monads
Note: This file's contents assumes you have read through and are somewhat familiar with the contents of the file, Syntax/Prelude Syntax/Reading Do as Nested Binds.md
.
So far, we have only shown you the Box
monad to help you get used to the syntax and see the logic for how Monad
and bind
/>>=
works. (The Box
type is a learner-friendly name for the Identity
monad, which we'll cover later in the Application Structure
folder.)
However, Monads
are used to compose two or more computations that occur within the same context (where context refers to the monadic type being used). Whereas the monadic type, Box
/Identity
, only has one possible value, the below types have two possible values. Functions that produce two possible outputs don't typically compose. However, the Monad
type class enables us to compose them using "railway-oriented programming" (~Scott Wlaschin).
When we compose different monadic types, we get different control flows. The do
notation helps us avoid the Pyramid of Doom boilerplate code and emphasizes developer intent.
The Maybe Monad
JavaScript Code
In JavaScript, we might write this code:
let a = computation();
if (a == null) {
return null;
} else {
let b = compute1(a);
if (b == null) {
return null;
} else {
let c = compute2(b);
if (c == null) {
return null;
} else {
return compute3(c);
}
}
}
PureScript Code (non-idiomatic)
In PureScript, we could write the following non-idiomatic code that repeats this Pyramid of Doom:
data Maybe a
= Nothing
| Just a
someComputation :: Maybe Unit
someComputation =
case computation of
Nothing -> Nothing
Just a -> case compute1 a of
Nothing -> Nothing
Just b -> case compute2 b of
Nothing -> Nothing
Just c -> compute3 c
where
computation :: Maybe a
compute1 :: a -> Maybe b
compute2 :: b -> Maybe c
compute3 :: c -> Maybe Unit
PureScript Code (idiomatic)
Or, we could use Maybe
's Monad
instance via do notation
to write idiomatic PureScript code:
data Maybe a
= Nothing
| Just a
instance Bind Maybe where
bind :: forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
-- when given a Nothing, stop all possible future computations
-- and return immediately.
bind Nothing _ = Nothing
-- when given a Just, run the function on its contents
-- and continue any Monadic computations
bind (Just a) f = f a
someComputation :: Maybe ReturnValue
someComputation = do
a <- computation
b <- compute1 a
c <- compute2 b
compute3 c
where
computation :: Maybe a
compute1 :: a -> Maybe b
compute2 :: b -> Maybe c
compute3 :: c -> Maybe Unit
If a Nothing
value is given at any point in the nested-bind
computations, it will short-circuit and return immediately.
What is a real-world example of using the Maybe monad? One often writes monadic code using Maybe as the Monad to lookup values in some structure (e.g. Map
, Array
, List
, or Tree
). Often, this control flow reads like this: "Try to get value X. If it exists, try to get value Y. If that exists, do something with both. If either one of them does not exist, stop and return immediately." In other words...
example :: Maybe String
example = do
x <- index 4 array
y <- lookup "fooKey" map
pure (x + y)
The Either Monad
JavaScript Code
In JavaScript, we might write this code:
let a = computation();
if (isError(a)) {
return a;
} else {
let b = compute1(a);
if (isError(b)) {
retun b;
} else {
let c = compute2(b);
if (isError(c)) {
return c;
} else {
return compute3(c);
}
}
}
PureScript Code (non-idiomatic)
data Either a b
= Left a
| Right b
someComputation :: Either ErrorType ReturnValue
someComputation = do
case computation of
Left err -> Left err
Right a -> case compute1 a of
Left err -> Left err
Right b -> case compute2 b of
Left err -> Left err
Right c -> compute3 c
where
computation :: Either ErrorType a
compute1 :: a -> Either ErrorType b
compute2 :: b -> Either ErrorType c
compute3 :: c -> Either ErrorType Unit
PureScript Code (idiomatic)
Or, we could use Either
's Monad
instance via do notation
to write idiomatic PureScript code:
data Either a b
= Left a
| Right b
instance Bind (Either a) where
bind :: forall b c. Either a b -> (b -> Either a c) -> Either a c
-- when given a Left, stop all possible future computations
-- and return immediately.
bind l@(Left _) _ = l
-- when given a Right, run the function on its contents
-- and continue any Monadic computations
bind (Right a) f = f a
someComputation :: Either ErrorType ReturnValue
someComputation = do
a <- computation
b <- compute1 a
c <- compute2 b
compute3 c
If a Left
value is given at any point in the nested-bind
computations, it will short-circuit and return immediately.
What is a real-world example of using the Either monad? One often uses it to validate that some data is correct. It reads like, "Try to parse the given String
into an Int
. If it fails, stop. Otherwise, try to parse the given String
into a Foo
. If it fails, stop. Otherwise, take the Int
and the Foo
and do something with them."
example :: String -> Either String ValidatedData
example string = do
intValue <- parseString string
fooValue <- parseNextPart
doSomethingWith intValue fooValue
The List / Array Monad
We use the List
type below in our examples. However, the Array
type works exactly the same way.
JavaScript Code
In JavaScript, we might write this code:
let list1 = [1, 2, 3];
let list2 = [2, 3, 4];
let list3 = [3, 4, 5];
var finalList = [];
for (i of list1) {
for (h of list2) {
for (j of list3) {
finalList.push(i + h + j);
}
}
}
return finalList;
PureScript Code (idiomatic)
The non-idiomatic version of the PureScript code below is complicated because it uses a lot of recusion. Thus, I do not show it here. Rather, we'll only show the idiomatic version:
data List a
= Nil
| Cons a (List a)
-- bind implementation not shown here
instance Bind List where
bind :: forall a b. List a -> (a -> List b) -> List b
-- when given a Nil (end of list), stop all potential future computations and return immediately.
bind Nil _ = Nil
-- when given a non-empty list, run the future computations on the head
-- and then prepend it to the rest of the computations on the tail.
bind (head : tail) f = append (f head) (bind tail f)
append :: List x -> List x -> List x
append =
-- implementation not shown here, but the result will be
-- append (1 : 2 : Nil) (3 : 4 : Nil) == (1 : 2 : 3 : 4 : Nil)
someComputation :: List Int
someComputation = do
a <- (1 : 2 : 3 : Nil)
b <- (2 : 3 : 4 : Nil)
c <- (3 : 4 : 5 : Nil)
pure (a + b + c)
which outputs:
-- a = 1, b = 2
( 6 : 7 : 8
-- a = 1, b = 3
: 7 : 8 : 9
-- a = 1, b = 4
: 8 : 9 : 10
-- a = 2, b = 2
: 7 : 8 : 9
-- a = 2, b = 3
: 8 : 9 : 10
-- a = 2, b = 4
: 9 : 10 : 11
-- a = 3, b = 2
: 8 : 9 : 10
-- a = 3, b = 3
: 9 : 10 : 11
-- a = 3, b = 4
: 10 : 11 : 12
: Nil)
Concluding Thoughts
Different monadic types lead to different control flow statements. We've only shown a few here.
We will see more control flow options in the Application Structure
folder, but there's more ground-work to cover before it'll make sense.