178 lines
No EOL
6.9 KiB
Text
178 lines
No EOL
6.9 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 that 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 ignored in that case. You can see why the
|
||
`square` function was ignored if you look at the implementation of `Except.bind`:
|
||
-/
|
||
def bind (ma : Except ε α) (f : α → Except ε β) : Except ε β :=
|
||
match ma with
|
||
| Except.error err => Except.error err
|
||
| Except.ok v => f v
|
||
/-!
|
||
|
||
Specifically notice that it only calls the next function `f v` in the `Except.ok`, and
|
||
in the error case it simply passes the same error along.
|
||
|
||
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 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. This works for all monads except `IO` which, as stated earlier,
|
||
you cannot invent out of thin air, you must use the `IO` monad given to your `main` function.
|
||
|
||
## 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 function. To learn the answer, head to [Monad Transformers](transformers.lean.md).
|
||
|
||
-/ |