300 lines
13 KiB
Text
300 lines
13 KiB
Text
/-!
|
||
# Monads
|
||
|
||
Building on [Functors](functors.lean.md) and [Applicatives](applicatives.lean.md) we can now
|
||
introduce [monads](https://en.wikipedia.org/wiki/Monad_%28category_theory%29).
|
||
|
||
A monad is another type of abstract, functional structure. Let's explore what makes it different
|
||
from the first two structures.
|
||
|
||
## What is a Monad?
|
||
|
||
A monad is a computational context. It provides a structure that allows you to chain together
|
||
operations that have some kind of shared state or similar effect. Whereas pure functional code can
|
||
only operate on explicit input parameters and affect the program through explicit return values,
|
||
operations in a monad can affect other computations in the chain implicitly through side effects,
|
||
especially modification of an implicitly shared value.
|
||
|
||
## How are monads represented in Lean?
|
||
|
||
Like functors and applicatives, monads are represented with a type class in Lean:
|
||
|
||
```lean,ignore
|
||
class Monad (m : Type u → Type v) extends Applicative m, Bind m where
|
||
```
|
||
|
||
Just as every applicative is a functor, every monad is also an applicative and there's one more new
|
||
base type class used here that you need to understand, namely, `Bind`.
|
||
|
||
```lean,ignore
|
||
class Bind (f : Type u → Type v) where
|
||
bind : {α β : Type u} → f α → (α → f β) → f β
|
||
```
|
||
|
||
The `bind` operator also has infix notation `>>=` where `x >>= g` represents the result of executing
|
||
`x` to get a value of type `f α` then unwrapping the value `α` from that and passing it to function
|
||
`g` of type `α → f β` returning the result of type `f β` where `f` is the target structure type
|
||
(like `Option` or List)
|
||
|
||
This `bind` operation looks similar to the other ones you've seen so far, if you put them all
|
||
together `Monad` has the following operations:
|
||
|
||
```lean,ignore
|
||
class Monad (f : Type u → Type v) extends Applicative f, Bind f where
|
||
pure {α : Type u} : α → f α
|
||
map : {α β : Type u} → (α → β) → f α → f β
|
||
seq : {α β : Type u} → f (α → β) → (Unit → f α) → f β
|
||
bind : {α β : Type u} → f α → (α → f β) → f β
|
||
...
|
||
```
|
||
|
||
Notice `Monad` also contains `pure` it must also have a "default" way to wrap a value in the
|
||
structure.
|
||
|
||
The `bind` operator is similar to the applicative `seq` operator in that it chains two operations,
|
||
with one of them being function related. Notice that `bind`, `seq` and `map` all take a function of
|
||
some kind. Let's examine those function types:
|
||
|
||
- map: `(α → β)`
|
||
- seq: `f (α → β)`
|
||
- bind: `(α → f β)`
|
||
|
||
So `map` is a pure function, `seq` is a pure function wrapped in the structure, and `bind` takes a
|
||
pure input but produces an output wrapped in the structure.
|
||
|
||
Note: we are ignoring the `(Unit → f α)` function used by `seq` here since that has a special
|
||
purpose explained in [Applicatives Lazy Evaluation](applicatives.lean.md#lazy-evaluation).
|
||
|
||
## Basic Monad Example
|
||
|
||
Just as `Option` is a functor and an applicative functor, it is also a monad! Let's start with how
|
||
`Option` implements the Monad type class.
|
||
|
||
-/
|
||
instance : Monad Option where
|
||
pure := Option.some
|
||
bind := Option.bind
|
||
/-!
|
||
|
||
where:
|
||
|
||
```lean,ignore
|
||
def Option.bind : Option α → (α → Option β) → Option β
|
||
| none, _ => none
|
||
| some a, f => f a
|
||
```
|
||
|
||
> **Side note**: this function definition is using a special shorthand syntax in Lean where the `:=
|
||
match a, b with` code can be collapsed away. To make this more clear consider the following simpler
|
||
example, where `Option.bind` is using the second form like `bar`:
|
||
|
||
-/
|
||
def foo (x : Option Nat) (y : Nat) : Option Nat :=
|
||
match x, y with
|
||
| none, _ => none
|
||
| some x, y => some (x + y)
|
||
|
||
def bar : Option Nat → Nat → Option Nat
|
||
| none, _ => none
|
||
| some x, y => some (x + y)
|
||
|
||
#eval foo (some 1) 2 -- some 3
|
||
#eval bar (some 1) 2 -- some 3
|
||
/-!
|
||
What is important is that `Option.bind` is using a `match` statement to unwrap the input value
|
||
`Option α`, if it is `none` then it does nothing and returns `none`, if it has a value of type `α`
|
||
then it applies the function in the second argument `(α → Option β)` to this value, which is
|
||
the expression `f a` that you see in the line ` | some a, f => f a` above. The function
|
||
returns a result of type `Option β` which then becomes the return value for `bind`. So there
|
||
is no structure wrapping required on the return value since the input function already did that.
|
||
|
||
But let's bring in the definition of a monad. What does it mean to describe `Option` as a
|
||
computational context?
|
||
|
||
The `Option` monad encapsulates the context of failure. Essentially, the `Option` monad lets us
|
||
abort a series of operations whenever one of them fails. This allows future operations to assume
|
||
that all previous operations have succeeded. Here's some code to motivate this idea:
|
||
|
||
-/
|
||
def optionFunc1 : String -> Option Nat
|
||
| "" => none
|
||
| str => some str.length
|
||
|
||
def optionFunc2 (i : Nat) : Option Float :=
|
||
if i % 2 == 0 then none else some (i.toFloat * 3.14159)
|
||
|
||
def optionFunc3 (f : Float) : Option (List Nat) :=
|
||
if f > 15.0 then none else some [f.floor.toUInt32.toNat, f.ceil.toUInt32.toNat]
|
||
|
||
def runOptionFuncs (input : String) : Option (List Nat) :=
|
||
match optionFunc1 input with
|
||
| none => none
|
||
| some i => match optionFunc2 i with
|
||
| none => none
|
||
| some f => optionFunc3 f
|
||
|
||
#eval runOptionFuncs "big" -- some [9, 10]
|
||
/-!
|
||
|
||
Here you see three different functions that could fail. These are then combined in `runOptionFuncs`.
|
||
But then you have to use nested `match` expressions to check if the previous result succeeded. It
|
||
would be very tedious to continue this pattern much longer.
|
||
|
||
The `Option` monad helps you fix this. Here's what this function looks like using the `bind`
|
||
operator.
|
||
|
||
-/
|
||
|
||
def runOptionFuncsBind (input : String) : Option (List Nat) :=
|
||
optionFunc1 input >>= optionFunc2 >>= optionFunc3
|
||
|
||
#eval runOptionFuncsBind "big" -- some [9, 10]
|
||
/-!
|
||
|
||
It's much cleaner now! You take the first result and pass it into the second and third functions
|
||
using the `bind` operation. The monad instance handles all the failure cases so you don't have to!
|
||
|
||
Let's see why the types work out. The result of `optionFunc1` input is simply `Option Nat`. Then the
|
||
bind operator allows you to take this `Option Nat` value and combine it with `optionFunc2`, whose type
|
||
is `Nat → Option Float` The **bind operator resolves** these to an `Option Float`. Then you pass this
|
||
similarly through the bind operator to `optionFunc3`, resulting in the final type, `Option (List Nat)`.
|
||
|
||
Your functions will not always combine so cleanly though. This is where `do` notation comes into play.
|
||
This notation allows you to write monadic operations one after another, line-by-line. It almost makes
|
||
your code look like imperative programming. You can rewrite the above as:
|
||
-/
|
||
|
||
def runOptionFuncsDo (input : String) : Option (List Nat) := do
|
||
let i ← optionFunc1 input
|
||
let f ← optionFunc2 i
|
||
optionFunc3 f
|
||
|
||
#eval runOptionFuncsDo "big" -- some [9, 10]
|
||
/-!
|
||
|
||
The `←` operator used here is special. It effectively unwraps the value on the right-hand side from
|
||
the monad. This means the value `i` has type `Nat`, _even though_ the result of `optionFunc1` is
|
||
`Option Nat`. This is done using a `bind` operation under the hood.
|
||
|
||
> Note you can use `<-` or the nice unicode symbol `←` which you can type into VS code by typing
|
||
these characters `\l `. When you type the final space, `\l` is replaced with `←`.
|
||
|
||
Observe that we do not unwrap the final line of the computation. The function result is `Option
|
||
(List Nat)` which matches what `optionFunc3` returns. At first glance, this may look more complicated
|
||
than the `bind` example. However, it gives you a lot more flexibility, like mixing monadic and
|
||
non-monadic statements, using if then/else structures with their own local do blocks and so on. It
|
||
is particularly helpful when one monadic function depends on multiple previous functions.
|
||
|
||
## Example using List
|
||
|
||
You can easily make `List` into a monad with the following, since List already provides an
|
||
implementation of `pure` and `bind`.
|
||
|
||
-/
|
||
instance : Monad List where
|
||
pure := List.pure
|
||
bind := List.bind
|
||
/-!
|
||
|
||
Like you saw with the applicative `seq` operator, the `bind` operator applies the given function
|
||
to every element of the list. It is useful to look at the bind implementation for List:
|
||
|
||
-/
|
||
open List
|
||
def bind (a : List α) (b : α → List β) : List β := join (map b a)
|
||
/-!
|
||
|
||
So `Functor.map` is used to apply the function `b` to every element of `a` but this would
|
||
return a whole bunch of little lists, so `join` is used to turn those back into a single list.
|
||
|
||
Here's an example where you use `bind` to convert a list of strings into a combined list of chars:
|
||
|
||
-/
|
||
|
||
#eval "apple".toList -- ['a', 'p', 'p', 'l', 'e']
|
||
|
||
#eval ["apple", "orange"] >>= String.toList
|
||
-- ['a', 'p', 'p', 'l', 'e', 'o', 'r', 'a', 'n', 'g', 'e']
|
||
|
||
/-!
|
||
|
||
|
||
## The IO Monad
|
||
|
||
The `IO Monad` is perhaps the most important monad in Lean. It is also one of the hardest monads to
|
||
understand starting out. Its actual implementation is too intricate to discuss when first learning
|
||
monads. So it is best to learn by example.
|
||
|
||
What is the **computational context** that describes the IO monad? IO operations can read
|
||
information from or write information to the terminal, file system, operating system, and/or
|
||
network. They interact with systems outside of your program. If you want to get user input, print a
|
||
message to the user, read information from a file, or make a network call, you'll need to do so
|
||
within the IO Monad.
|
||
|
||
The state of the world outside your program can change at virtually any moment, and so this IO
|
||
context is particularly special. So these IO operations are "side effects" which means you cannot
|
||
perform them from "pure" Lean functions.
|
||
|
||
Now, the most important job of pretty much any computer program is precisely to perform this
|
||
interaction with the outside world. For this reason, the root of all executable Lean code is a
|
||
function called main, with the type `IO Unit`. So every program starts in the IO monad!
|
||
|
||
When your function is `IO` monadic, you can get any input you need, call into "pure" code with the
|
||
inputs, and then output the result in some way. The reverse does not work. You cannot call into IO
|
||
code from pure code like you can call into a function that takes `Option` as input. Another way to
|
||
say this is you cannot invent an `IO` context out of thin air, it has to be given to you in your
|
||
`main` function.
|
||
|
||
Let's look at a simple program showing a few of the basic IO functions. It also uses `do` notation
|
||
to make the code read nicely:
|
||
-/
|
||
def main : IO Unit := do
|
||
IO.println "enter a line of text:"
|
||
let stdin ← IO.getStdin -- IO IO.FS.Stream (monadic)
|
||
let input ← stdin.getLine -- IO.FS.Stream → IO String (monadic)
|
||
let uppercased := input.toUpper -- String → String (pure)
|
||
IO.println uppercased -- IO Unit (monadic)
|
||
/-!
|
||
|
||
So, once again you can see that the `do` notation lets you chain a series of monadic actions.
|
||
`IO.getStdin` is of type `IO IO.FS.Stream` and `stdin.getLine` is of type `IO String`
|
||
and `IO.println` is of type `IO Unit`.
|
||
|
||
In between you see a non-monadic expression `let uppercased := input.toUpper` which is fine too.
|
||
A let statement can occur in any monad. Just as you could unwrap `i` from `Option Nat` to get the
|
||
inner Nat, you can use `←` to unwrap the result of `getLine` to get a String. You can then manipulate
|
||
this value using normal pure string functions like `toUpper`, and then you can pass the result to the
|
||
`IO.println` function.
|
||
|
||
This is a simple echo program. It reads a line from the terminal, and then prints the line back out
|
||
capitalized to the terminal. Hopefully it gives you a basic understanding of how IO works.
|
||
|
||
You can test this program using `lean --run` as follows:
|
||
|
||
```
|
||
> lean --run Main.lean
|
||
enter a line of text:
|
||
the quick brown fox
|
||
THE QUICK BROWN FOX
|
||
```
|
||
|
||
Here the user entered the string `the quick brown fox` and got back the uppercase result.
|
||
|
||
## What separates Monads from Applicatives?
|
||
|
||
The key that separates these is **context**. You cannot really determine the structure of
|
||
"future" operations without knowing the results of "past" operations, because the past can alter the
|
||
context in which the future operations work. With applicatives, you can't get the final function
|
||
result without evaluating everything, but you can determine the structure of how the operation will
|
||
take place. This allows some degree of parallelism with applicatives that is not generally possible
|
||
with monads.
|
||
|
||
|
||
## Conclusion
|
||
|
||
Hopefully you now have a basic level understanding of what a monad is. But perhaps some more
|
||
examples of what a "computational context" means would be useful to you. The Reader, State and
|
||
Except monads each provide a concrete and easily understood context that can be compared easily to
|
||
function parameters. You can learn more about those in [Reader monads](readers.lean.md),
|
||
[State monads](states.lean.md), and the [Except monad](except.lean.md).
|
||
-/
|