322 lines
11 KiB
Text
322 lines
11 KiB
Text
/-!
|
||
# Monad Laws
|
||
|
||
In the previous sections you learned how to use [Functors](functors.lean.md),
|
||
[Applicatives](applicatives.lean.md), and [Monads](monads.lean.md), and you played with some useful
|
||
instances including [Option](monads.lean.md), [IO](monads.lean.md), [Reader](readers.lean.md),
|
||
[State](states.lean.md) and [Except](except.lean.md) and you learned about composition using [Monad
|
||
Transformers](transformers.lean.md).
|
||
|
||
So far, you've learned the concrete details you need in order to _use_ monads in your Lean programs.
|
||
But there's still one more important concept you need if you want to _create_ new functors,
|
||
applicatives and monads. Namely, the notion of _structural "laws"_ -- rules that these type
|
||
classes should follow in order to meet other programmers' expectations about your code.
|
||
|
||
## Life without Laws
|
||
|
||
Remember Lean represents each of these abstract structures by a type class. Each of these type classes
|
||
has one or two main functions. So, as long as you implement those functions and it type checks, you
|
||
have a new functor, applicative, or monad, right?
|
||
|
||
Well not quite. Yes, your program will compile and you'll be able to use the instances. But this
|
||
doesn't mean your instances follow the mathematical constructs. If they don't, your instances won't
|
||
fulfill other programmers' expectations. Each type class has its own "laws". For instance, suppose
|
||
you have the following Point Functor:
|
||
-/
|
||
structure Point (α : Type) where
|
||
x : α
|
||
y : α
|
||
deriving Repr, BEq
|
||
|
||
def Point.map (f : α → β) (s : Point α) : Point β :=
|
||
{ x := f s.y, -- an example of something weird
|
||
y := f s.x }
|
||
|
||
instance : Functor Point where
|
||
map := Point.map
|
||
|
||
#eval (·+2) <$> (Point.mk 1 2) -- { x := 4, y := 3 }
|
||
|
||
/-!
|
||
This Point does something weird, when you `map` it because it transposes the `x` and `y` coordinates
|
||
which is not what other people would expect from a `map` function. In fact, it breaks the rules
|
||
as you will see below.
|
||
|
||
## What are the Functor laws?
|
||
|
||
Functors have two laws: the _identity_ law, and the _composition_ law. These laws express behaviors that
|
||
your functor instances should follow. If they don't, other programmers will be very confused at the
|
||
effect your instances have on their program.
|
||
|
||
The identity law says that if you "map" the identity function (`id`) over your functor, the
|
||
resulting functor should be the same. A succinct way of showing this on a `List` functor is:
|
||
|
||
-/
|
||
def list1 := [1,2,3]
|
||
|
||
#eval id <$> list1 == list1 -- true
|
||
/-!
|
||
|
||
Now let's try the same test on the `Point` functor:
|
||
-/
|
||
|
||
def p1 : Point Nat := (Point.mk 1 2)
|
||
|
||
#eval id <$> p1 == p1 -- false
|
||
|
||
/-!
|
||
Oh, and look while the `List` is behaving well, the `Point` functor fails this identity test.
|
||
|
||
The _composition_ law says that if you "map" two functions in succession over a functor, this
|
||
should be the same as "composing" the functions and simply mapping that one super-function over the
|
||
functor. In Lean you can compose two functions using `Function.comp f g` (or the syntax `f ∘ g`,
|
||
which you can type in VS code using `\o `) and you will get the same results from both of these
|
||
showing that the composition law holds for `List Nat`:
|
||
|
||
-/
|
||
def double (x : Nat) := x + x
|
||
def square (x : Nat) := x * x
|
||
|
||
#eval double <$> (square <$> list1) -- [2, 8, 18]
|
||
|
||
#eval (double <$> (square <$> list1)) == ((double ∘ square) <$> list1) -- true
|
||
|
||
-- ok, what about the Point class?
|
||
#eval double <$> (square <$> p1) -- { x := 2, y := 8 }
|
||
#eval (double ∘ square) <$> p1 -- { x := 8, y := 2 }
|
||
|
||
#eval double <$> (square <$> p1) == (double ∘ square) <$> p1 -- false
|
||
/-!
|
||
Note that composition also fails on the bad `Point` because the x/y transpose.
|
||
|
||
As you can see this bad `Point` implementation violates both of the functor laws. In this case it
|
||
would not be a true functor. Its behavior would confuse any other programmers trying to use it. You
|
||
should take care to make sure that your instances make sense. Once you get a feel for these type
|
||
classes, the likelihood is that the instances you'll create will follow the laws.
|
||
|
||
You can also write a bad functor that passes one law but not the other like this:
|
||
-/
|
||
def bad_option_map {α β : Type u} : (α → β) → Option α → Option β
|
||
| _, _ => none
|
||
|
||
instance : Functor Option where
|
||
map := bad_option_map
|
||
|
||
def t1 : Option Nat := some 10
|
||
|
||
#eval id <$> t1 == t1 -- false
|
||
#eval double <$> (square <$> t1) == (double ∘ square) <$> t1 -- true
|
||
/-!
|
||
|
||
This fails the id law but obeys the composition law. Hopefully this explains the value of these
|
||
laws, and you don't need to see any more bad examples!
|
||
|
||
## What are the Applicative Laws?
|
||
|
||
While functors have two laws, applicatives have four laws:
|
||
|
||
- Identity
|
||
- Homomorphism
|
||
- Interchange
|
||
- Composition
|
||
|
||
### Identity
|
||
|
||
`pure id <*> v = v`
|
||
|
||
Applying the identity function through an applicative structure should not change the underlying
|
||
values or structure. For example:
|
||
-/
|
||
instance : Applicative List where
|
||
pure := List.pure
|
||
seq f x := List.bind f fun y => Functor.map y (x ())
|
||
|
||
#eval pure id <*> [1, 2, 3] -- [1, 2, 3]
|
||
/-!
|
||
|
||
The `pure id` statement here is wrapping the identity function in an applicative structure
|
||
so that you can apply that over the container `[1, 2, 3]` using the Applicative `seq` operation
|
||
which has the notation `<*>`.
|
||
|
||
To prove this for all values `v` and any applicative `m` you can write this theorem:
|
||
-/
|
||
example [Applicative m] [LawfulApplicative m] (v : m α) :
|
||
pure id <*> v = v :=
|
||
by simp -- Goals accomplished 🎉
|
||
/-!
|
||
|
||
### Homomorphism
|
||
|
||
`pure f <*> pure x = pure (f x)`
|
||
|
||
Suppose you wrap a function and an object in `pure`. You can then apply the wrapped function over the
|
||
wrapped object. Of course, you could also apply the normal function over the normal object, and then
|
||
wrap it in `pure`. The homomorphism law states these results should be the same.
|
||
|
||
For example:
|
||
|
||
-/
|
||
def x := 1
|
||
def f := (· + 2)
|
||
|
||
#eval pure f <*> pure x = (pure (f x) : List Nat) -- true
|
||
/-!
|
||
|
||
You should see a distinct pattern here. The overriding theme of almost all these laws is that these
|
||
`Applicative` types should behave like normal containers. The `Applicative` functions should not
|
||
have any side effects. All they should do is facilitate the wrapping, unwrapping, and transformation
|
||
of data contained in the container resulting in a new container that has the same structure.
|
||
|
||
### Interchange
|
||
|
||
`u <*> pure y = pure (. y) <*> u`.
|
||
|
||
This law is is a little more complicated, so don't sweat it too much. It states that the order that
|
||
you wrap things shouldn't matter. One the left, you apply any applicative `u` over a pure wrapped
|
||
object. On the right, you first wrap a function applying the object as an argument. Note that `(·
|
||
y)` is short hand for: `fun f => f y`. Then you apply this to the first applicative `u`. These
|
||
should be the same.
|
||
|
||
For example:
|
||
|
||
-/
|
||
def y := 4
|
||
def g : List (Nat → Nat) := [(· + 2)]
|
||
|
||
#eval g <*> pure y = pure (· y) <*> g -- true
|
||
/-!
|
||
|
||
You can prove this with the following theorem:
|
||
-/
|
||
example [Applicative m] [LawfulApplicative m] (u : m (α → β)) (y : α) :
|
||
u <*> pure y = pure (· y) <*> u :=
|
||
by simp [pure_seq] -- Goals accomplished 🎉
|
||
|
||
/-!
|
||
|
||
### Composition:
|
||
|
||
`u <*> v <*> w = u <*> (v <*> w)`
|
||
|
||
This final applicative law mimics the second functor law. It is a composition law. It states that
|
||
function composition holds across applications within the applicative:
|
||
|
||
For example:
|
||
|
||
-/
|
||
def u := [1, 2]
|
||
def v := [3, 4]
|
||
def w := [5, 6]
|
||
|
||
#eval pure (·+·+·) <*> u <*> v <*> w
|
||
-- [9, 10, 10, 11, 10, 11, 11, 12]
|
||
|
||
#eval let grouping := pure (·+·) <*> v <*> w
|
||
pure (·+·) <*> u <*> grouping
|
||
-- [9, 10, 10, 11, 10, 11, 11, 12]
|
||
/-!
|
||
|
||
To test composition you see the separate grouping `(v <*> w)` then that can be used in the outer
|
||
sequence `u <*> grouping` to get the same final result `[9, 10, 10, 11, 10, 11, 11, 12]`.
|
||
|
||
## What are the Monad Laws?
|
||
|
||
Monads have three laws:
|
||
|
||
- Left Identity
|
||
- Right Identity
|
||
- Associativity
|
||
|
||
### Left Identity
|
||
|
||
Identity laws for monads specify that `pure` by itself shouldn't really change anything about the
|
||
structure or its values.
|
||
|
||
Left identity is `x >>= pure = x` and is demonstrated by the following examples on a monadic `List`:
|
||
-/
|
||
instance : Monad List where
|
||
pure := List.pure
|
||
bind := List.bind
|
||
|
||
def a := ["apple", "orange"]
|
||
|
||
#eval a >>= pure -- ["apple", "orange"]
|
||
|
||
#eval a >>= pure = a -- true
|
||
|
||
/-!
|
||
|
||
### Right Identity
|
||
|
||
Right identity is `pure x >>= f = f x` and is demonstrated by the following example:
|
||
-/
|
||
def h (x : Nat) : Option Nat := some (x + 1)
|
||
def z := 5
|
||
|
||
#eval pure z >>= h -- some 6
|
||
#eval h z -- some 6
|
||
|
||
#eval pure z >>= h = h z -- true
|
||
/-!
|
||
|
||
So in this example, with this specific `z` and `h`, you see that the rule holds true.
|
||
|
||
|
||
### Associativity
|
||
|
||
The associativity law is written as:
|
||
```lean,ignore
|
||
x >>= f >>= g = x >>= (λ x => f x >>= g)
|
||
```
|
||
where `(x : m α)` and `(f : α → m β)` and `(g : β → m γ)`.
|
||
|
||
The associativity law is difficult to parse like some of the applicative laws, but what it is saying
|
||
is that if you change the grouping of `bind` operations, you should still get the same result.
|
||
This law has a parallel structure to the other composition laws.
|
||
|
||
You can see this in action in the following rewrite of `runOptionFuncsBind` from [monads](monads.lean.md):
|
||
-/
|
||
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 runOptionFuncsBind (input : String) : Option (List Nat) :=
|
||
optionFunc1 input >>= optionFunc2 >>= optionFunc3
|
||
|
||
def runOptionFuncsBindGrouped (input : String) : Option (List Nat) :=
|
||
optionFunc1 input >>= (λ x => optionFunc2 x >>= optionFunc3)
|
||
|
||
#eval runOptionFuncsBind "big" -- some [9, 10]
|
||
#eval runOptionFuncsBindGrouped "big" -- some [9, 10]
|
||
/-!
|
||
|
||
Notice here we had to insert a `λ` function just like the definition says: `(λ x => f x >>= g)`.
|
||
This is because unlike applicatives, you can't resolve the structure of later operations without the
|
||
results of earlier operations quite as well because of the extra context monads provide. But you can
|
||
still group their later operations into composite functions taking their inputs from earlier on, and
|
||
the result should be the same.
|
||
|
||
|
||
## Summary
|
||
|
||
While these laws may be a bit difficult to understand just by looking at them, the good news is that
|
||
most of the instances you'll make will naturally follow the laws so long as you keep it simple, so
|
||
you shouldn't have to worry about them too much.
|
||
|
||
There are two main ideas from all the laws:
|
||
|
||
1. Applying the identity or pure function should not change the underlying values or structure.
|
||
1. It should not matter what order you group operations in. Another way to state this is function
|
||
composition should hold across your structures.
|
||
|
||
Following these laws will ensure other programmers are not confused by the behavior of your
|
||
new functors, applicatives and monads.
|
||
|
||
-/
|