* doc: add documentation on functors. * fix: make comments green * minor tweaks * doc: add section on Applicatives. * doc: add some more info on the laws from Mario. * doc: add law list and move lazy evaluation up so the chapter ends properly. * doc: Add something on seqLeft and seqRight. * doc: add section on monads. * doc: fix some typos. * doc: switch to LeanInk for chaper on Monads. * doc: remove old files. * doc: fix mdbook test errors. * doc: add part 4: readers * doc: add section on State monads * doc: fix some typos and add some more details. * doc: fix typos and add some CR feedback. * doc: add Except monad section. * doc: add info on monad transformers. * Delete transformers.lean.md * doc: fix some typos. * doc: fix typos and move forward reference to monad lifting. * doc: Update `State` to `StateM` * doc: fix references to State to become StateM. * doc: generalize indexOf implementation. * doc: add chapter on monad laws and move "law" sections to this chapter to avoid redundancy. * doc: add theorem * Delete laws.lean.md * doc: fix some typos. * doc: fix broken link. * doc: fox typos. * fix: language changed from "us" to "you". * doc: fix code review comments. * doc: some word smithing * doc: some word smithing and sample simplification. * doc: add bad_option_map example. * doc: add side note on `return` statement and fix heading level consistency. * Add `withReader` info * doc: change language from us, our, your, we, we'll, we've to "you" * doc: add some forward links. * doc: put spaces around colon in function arguments like "(x : List Nat)" * doc: Add backticks on map Remove commands on multiline structure instance Fix centerdot Add "one of" * doc: Remove info about Functor in other languages. * doc: add info on <$> being left associative remove another forward reference to monad fix typo `operatoions` remove unneccesary parens after <$> * doc: fix Type u -> Type v fix you -> your use `let val? ← IO.getEnv name` in Readers call it "context" rather than "state". * doc: fix withReader docs use 'context' to describe the ReaderM state. remove "trivial" type inference => Lean "abstract classes" => "abstract structures" remove unnecessary parens * doc: fix bug in explanation of `let x ← readerFunc2` Fix explanation of equivalence between `def f (a : Nat) : String` and `def f : Nat → String` * doc: move hasSomeItemGreaterThan to Except.lean Add validateList List.anyM example. fix `def f (a : Nat) : String` language. * doc: fix "What transformation are you referring to" * doc: fix typo. * doc: add missing period. * doc: fix validateList * doc: explain `λ` notation. * doc: reword the map, seq, bind comparison. * doc: fix some more 'reader state' to 'reader context' language * doc: fix wrote statement about return only works in do blocks. * doc: fix typo Co-authored-by: Sebastian Ullrich <sebasti@nullri.ch> * doc: improve language Co-authored-by: Sebastian Ullrich <sebasti@nullri.ch> * doc: fix typo Co-authored-by: Sebastian Ullrich <sebasti@nullri.ch> * Add info on what a do block is doing for you. * doc: define definitionally equal * doc: make `readerFunc3.run env` canonical. * doc: remove unnecessary parens. * doc: fix typos * doc: make List.map a bit more clear in the intro to Functors. * doc: simplify readerFunc3WithReader * doc: switch to svg for diagram so it works better on dark themes. * doc: align nodes in diagram and convert to svg. * doc: simplify playGame using while true. * doc: drop confusing statement about "definitionally equal" * doc: switch to `import Lean.Data.HashMap` * doc: fix typo "operatoins" * doc: update diagram to add more info and polish the intro paragraphs so they better match the actual contents of each chapter. * doc: fix typo * doc: fix typo. Co-authored-by: Sebastian Ullrich <sebasti@nullri.ch>
166 lines
No EOL
6.3 KiB
Text
166 lines
No EOL
6.3 KiB
Text
/-!
|
||
# Except
|
||
|
||
The `Except` Monad adds exception handling behavior to your functions. Exception handling
|
||
in other languages like Python or Java is done with a built in `throw` method that you
|
||
can use anywhere. In `Lean` you can only `throw` an exception when your function is
|
||
executing in the context of an `Except` monad.
|
||
|
||
-/
|
||
def divide (x y: Float): Except String Float :=
|
||
if y == 0 then
|
||
throw "can't divide by zero"
|
||
else
|
||
pure (x / y)
|
||
|
||
#eval divide 5 2 -- Except.ok 2.500000
|
||
#eval divide 5 0 -- Except.error "can't divide by zero"
|
||
|
||
/-!
|
||
|
||
Just as the `read` operation was available from the `ReaderM` monad and the `get` and `set`
|
||
operations came with the `StateM` monad, here you can see a `throw` operation is provided by the
|
||
`Except` monad.
|
||
|
||
So in Lean, `throw` is not available everywhere like it is in most imperative programming languages.
|
||
You have to declare your function can throw by changing the type signature to `Except String Float`.
|
||
This creates a function that might return an error of type `String` or it might return a value of
|
||
type `Float` in the non-error case.
|
||
|
||
Once your function is monadic you also need to use the `pure` constructor of the `Except` monad to
|
||
convert the pure non-monadic value `x / y` into the required `Except` object. See
|
||
[Applicatives](applicatives.lean.md) for details on `pure`.
|
||
|
||
Now this return typing would get tedious if you had to include it everywhere that you call this
|
||
function, however, Lean type inference can clean this up. For example, you can define a test
|
||
function can calls the `divide` function and you don't need to say anything here about the fact that
|
||
it might throw an error, because that is inferred:
|
||
-/
|
||
def test := divide 5 0
|
||
|
||
#check test -- Except String Float
|
||
/-!
|
||
|
||
Notice the Lean compiler infers the required `Except String Float` type information for you.
|
||
And now you can run this test and get the expected exception:
|
||
|
||
-/
|
||
#eval test -- Except.error "can't divide by zero"
|
||
/-!
|
||
|
||
## Chaining
|
||
|
||
Now as before you can build a chain of monadic actions that can be composed together using `bind (>>=)`:
|
||
-/
|
||
def square (x : Float) : Except String Float :=
|
||
if x >= 100 then
|
||
throw "it's absolutely huge"
|
||
else
|
||
pure (x * x)
|
||
|
||
#eval divide 6 2 >>= square -- Except.ok 9.000000
|
||
#eval divide 6 0 >>= square -- Except.error "can't divide by zero"
|
||
#eval divide 100 1 >>= square -- Except.error "it's absolutely huge"
|
||
|
||
def chainUsingDoNotation := do
|
||
let r ← divide 6 0
|
||
square r
|
||
|
||
#eval chainUsingDoNotation -- Except.error "can't divide by zero"
|
||
|
||
/-!
|
||
Notice in the second `divide 6 0` the exception from that division was nicely propagated along
|
||
to the final result and the square function was pretty much ignored in that case.
|
||
|
||
Remember also that you can chain the actions with implicit binding by using the `do` notation
|
||
as you see in the `chainUsingDoNotation` function above.
|
||
|
||
## Try/Catch
|
||
|
||
Now with all good exception handling you also want to be able to catch exceptions so your program
|
||
can try continue on or do some error recovery task, which you can do like this:
|
||
-/
|
||
def testCatch :=
|
||
try
|
||
let r ← divide 8 0 -- 'r' is type Float
|
||
pure (toString r)
|
||
catch e =>
|
||
pure s!"Caught exception: {e}"
|
||
|
||
#check testCatch -- Except String String
|
||
/-!
|
||
|
||
Note that the type inferred by Lean for this function is `Except String String` so unlike the
|
||
`test` function earlier, this time Lean type inference has figured out that since the pure
|
||
value `(toString r)` is of type `String`, then this function must have type `Except String String`
|
||
so you don't have to explicitly state this. You can always hover your mouse over `testCatch`
|
||
or use `#check testCatch` to query Lean interactively to figure out what type inference
|
||
has decided. Lean type inference makes life easy for you, so it's good to use it
|
||
when you can.
|
||
|
||
You can now see the try/catch working in this eval:
|
||
-/
|
||
#eval testCatch -- Except.ok "Caught exception: can't divide by zero"
|
||
/-!
|
||
|
||
Notice the `Caught exception:` wrapped message is returned, and that it is returned as an
|
||
`Except.ok` value, meaning `testCatch` eliminated the error result as expected.
|
||
|
||
So you've interleaved a new concept into your functions (exception handling) and the compiler is still
|
||
able to type check everything just as well as it does for pure functions and it's been able to infer
|
||
some things along the way to make it even easier to manage.
|
||
|
||
Now you might be wondering why `testCatch` doesn't infer the return type `String`? Lean does this as a
|
||
convenience since you could have a rethrow in or after the catch block. If you really want to stop
|
||
the `Except` type from bubbling up you can unwrap it like this:
|
||
|
||
-/
|
||
def testUnwrap : String := Id.run do
|
||
let r ← divide 8 0 -- r is type Except String Float
|
||
match r with
|
||
| .ok a => toString a -- 'a' is type Float
|
||
| .error e => s!"Caught exception: {e}"
|
||
|
||
#check testUnwrap -- String
|
||
#eval testUnwrap -- "Caught exception: can't divide by zero"
|
||
|
||
/-!
|
||
|
||
The `Id.run` function is a helper function that executes the `do` block and returns the result where
|
||
`Id` is the _identity monad_. So `Id.run do` is a pattern you can use to execute monads in a
|
||
function that is not itself monadic.
|
||
|
||
## Monadic functions
|
||
|
||
You can also write functions that are designed to operate in the context of a monad.
|
||
These functions typically end in upper case M like `List.forM` used below:
|
||
-/
|
||
|
||
def validateList (x : List Nat) (max : Nat): Except String Unit := do
|
||
x.forM fun a => do
|
||
if a > max then throw "illegal value found in list"
|
||
|
||
#eval validateList [1, 2, 5, 3, 8] 10 -- Except.ok ()
|
||
#eval validateList [1, 2, 5, 3, 8] 5 -- Except.error "illegal value found in list"
|
||
|
||
/-!
|
||
Notice here that the `List.forM` function passes the monadic context through to the inner function
|
||
so it can use the `throw` function from the `Except` monad.
|
||
|
||
The `List.forM` function is defined like this where `[Monad m]` means "in the context of a monad `m`":
|
||
|
||
-/
|
||
def forM [Monad m] (as : List α) (f : α → m PUnit) : m PUnit :=
|
||
match as with
|
||
| [] => pure ⟨⟩
|
||
| a :: as => do f a; List.forM as f
|
||
/-!
|
||
|
||
## Summary
|
||
|
||
Now that you know all these different monad constructs, you might be wondering how you can combine
|
||
them. What if there was some part of your state that you wanted to be able to modify (using the
|
||
State monad), but you also needed exception handling. How can you get multiple monadic capabilities
|
||
in the same fuunction. To learn the answer, head to [Monad Transformers](transformers.lean.md).
|
||
|
||
-/ |