Monads
Today we will practice with monads, implement a few, learn about fmap
and join
,
and practice proving satisfaction of the Monad Laws.
Maybe
One of the simplest monads is the Maybe monad, which you might also know as
the Option monad. Recall the definition of bind
and return
for the Maybe
monad you saw in lecture:
let return x = Some x
let bind m f =
match m with
| Some x -> f x
| None -> None
let (>>=) = bind
return
takes a value and creates a trivial option
out of it, while bind
takes an option
and a function, and applies the function to the contents of that
option
, creating a new optional value.
Exercise: add [✭]
Complete the definition of add
according to its specification using >>=
and return
:
(*
* Computes the sum of two optional values.
* If both x and y are of the form Some p, [add x y] should be Some(px + py)
* If either x or y are None, [add x y] should be None
*)
let add (x: int option) (y : int option) =
(*Your implementation here*)
□
Exercise: cleanup [✭✭]
The following code uses some functions that produce optional values, but
whoever wrote it clearly missed last week's lecture on Monads. Rewrite it
more elegantly with bind
and return
.
val foo : unit -> int option
val bar : int -> string option
val baz : unit -> string option
val print_option : string option -> unit
let a = foo () in
let b = match a with
| Some x -> bar x
| None -> None in
let c = baz () in
let d = match (b, c) with
| Some x, Some y -> print_option (Some (x^y))
| _ -> ()
□
fmap and join
bind
and return
are not the only two useful functions for working with monads.
Although bind
makes it easy to build computation pipelines as you saw in the
previous exercises, sometimes we only want to apply a single pre-existing function
to a monad, and don't want to have to worry about upgrading it as seen in lecture.
The function fmap
makes this possible:
fmap : 'a t -> ('a -> 'b) -> 'b t
Notice the similarity in type to bind
. The key difference to is that the function
does not return a monad type, whereas it does in bind
.
fmap
also has an infix operator (>>|)
. If you wish, you can think of the |
as
denoting the end of a computation chain.
We can also reverse the order of the arguments to fmap
as so:
fmap' : ('a -> 'b) -> 'a t -> 'b t
This is equivalent to:
fmap' : ('a -> 'b) -> ('a t -> 'b t)
Written this way, it becomes clear that when fully applied, fmap'
will
take a function and apply it to the contents of its second, monadic argument.
However, when partially applied, it becomes a function that will take
an non-monadic function and 'upgrade' it to a monadic one. Written this way fmap'
is more commonly called lift
.
Exercise: fmap [✭]
Implement fmap
for the Maybe monad.
□
Another useful function is join
, called such because it has the effect of 'joining'
a doubly 'nested' monad. To use the burrito analogy, join
takes a burrito wrapped
in two tortillas and returns a new burrito with the same contents, but with only
one tortilla wrapper.
join : 'a t t -> 'a t
Exercise: join [✭]
Implement join
for the Maybe monad. join
should be None
if either of the
two 'layers' of the input option
are None
.
□
Exercise: bind 2.0 [✭✭]
Part of what makes fmap
and join
such useful functions for formulating monads
is that bind
can be implemented for all monads using only these two functions.
Do so for the Maybe monad. The Some
and None
constructors should not appear
in your solution.
□
Exercise: fmap 2.0 [✭✭]
Implement fmap
and join
using only bind
and return
. Your solution
should work for any monad.
□
Lists and Nondeterminism
Imagine, for a moment, that you had a function with a random effect. Given an input, it could return a variety of possible outputs, with no indication of what they would be based solely on the input. Examples include rolling a die, or reading a value from a network.
In statistics, we often formalize the concept of multiple outcomes using a set, called a sample space. The sample space is the set of all possible outcomes of an action. For example, when flipping a coin, the sample space is {head, tails}. When rolling a 4-sided die, the sample space is {1, 2, 3, 4}.
Sample spaces for sequential actions are simply combined into one set: the sample space for flipping two coins is {(head, tails), (head, head), (tails, head), (tails, tails)}, while the sample space for the sum of two 4-sided die rolls would be {2, 3, 4, 5, 6, 7, 8}.
Since there is no native representation for sets in OCaml, we often choose to represent sets as lists, so a function to get the sample space of rolling a die might have the type:
val roll : unit -> int list
The List monad makes it convenient to work with non-deterministic functions
by treating this list as a monadic value. return
trivially makes a singleton
list from its argument, while bind
can be thought of as performing a
non-deterministic computation on each element of its input set.
As an example:
# let l1 = roll() in (*[1; 2; 3; 4]*)
# let l2 = roll() in (*[1; 2; 3; 4]*)
# l1 >>= fun x ->
l2 >>= fun y ->
return (x + y);;
- : int list [2; 3; 4; 5; 3; 4; 5; 6; 4; 5; 6; 7; 5; 6; 7; 8]
This computes the result of adding two 4-sided dice rolls. Note that the list representation of the sample space has duplicates, something that its set counterpart does not.
Exercise: list [✭✭✭]
Implement return
and bind
for the List monad.
Hint: List.map and List.concat might be helpful.
□
Exercise: list again [✭✭]
Implement fmap
and join
for the List monad. Do not use return
nor bind
that you implemented in the previous exercise.
□
The Monad Laws
The monad laws are three equations that every monad implementation must satisfy:
(1) (return x >>= f) = f x
(2) (m >>= return) = m
(3) ((m >>= f) >>= g) = (m >>= (fun x -> f x >>= g))
Exercise: maybe coq [✭✭✭]
Here is an implementation of the Maybe monad in Coq, using Coq's
option
as the representation type:
Module MaybeMonad.
Definition maybe (A:Type) : Type := option A.
(* [return] is a keyword in Coq so we use [return'] instead *)
Definition return' {A:Type} (x:A) :=
Some x.
Definition bind {A B:Type} (m : maybe A) (f : A -> maybe B) : maybe B :=
match m with
| Some x => f x
| None => None
end.
Notation "m >>= f" := (bind m f) (right associativity, at level 60).
(* insert theorems here *)
End MaybeMonad.
Prove in Coq that the monad laws hold for that implementation of the Maybe monad.
Hint: these proofs are straightforward; destruct
is the most complicated
tactic you need.
□
Exercise: list coq [✭✭✭✭]
Implement the List monad in Coq, using Coq's list
as the representation
type. Prove in Coq that the monad laws hold for your implementation.
Hints: Make use of the Coq List standard library, both definitions
and theorems. You will need induction
. There is a tactic cbn
that is
like simpl
but is better tuned to the kinds of proofs you are doing here;
use it instead of simpl
throughout this exercise.
□