Zulip Chat Archive
Stream: lean4
Topic: try, catch, else
Eric Wieser (Jan 24 2024 at 12:12):
Python has an extended version of try
/catch
(there, except
) which has an else
clause:
try:
x = foo()
except Exception as e:
bar()
else:
baz(x)
which has semantics:
foo
crashes -> runbar
. Ifbar
crashes, propagate the errorfoo
succeeds -> runbaz
. Ifbaz
crashes, propagate the error
For what I can tell, Lean only offers
import Lean
open Lean Meta
variable (foo1 foo2 : MetaM Nat)
variable (bar : MetaM Int)
variable (baz : Nat → Nat → MetaM Int)
example : MetaM Int := do
try
let x ← foo1
let y ← foo2
baz x y
catch _ =>
bar
which has the wrong semantics when baz
fails of also catching that error.
Eric Wieser (Jan 24 2024 at 12:13):
I can write this as a workaround:
example : MetaM Int := do
let ret ← try
let x ← foo1
let y ← foo2
pure <| Sum.inl (x, y)
catch _ =>
pure <| Sum.inr (← bar)
match ret with
| .inl (x, y) => baz x y
| .inr r => return r
but it's a lot less clear
Eric Wieser (Jan 24 2024 at 12:25):
(I previously asked this here)
James Gallicchio (Jan 24 2024 at 17:56):
I thought we had finally
?
Eric Wieser (Jan 24 2024 at 17:58):
finally
runs even if an error does happen, and so does not have access to local variables within the try
Eric Wieser (Jan 26 2024 at 14:20):
Ah, this works:
import Lean
open Lean Meta
def foo1 : MetaM Nat := return 1
def foo2 (fail : Bool) : MetaM Nat := if fail then failure else return 10
def bar : MetaM Int := do
trace[debug] "ran bar"
return 2
def baz (x y : Nat) : MetaM Int := do
trace[debug] "ran baz"
return x + y
/--
Would like to write as
try
let x ← foo1
let y ← foo2 false
catch _ =>
bar
else
baz x y
-/
def test : MetaM Int := do
(← try
let x ← foo1
let y ← foo2 false
return do
baz x y
catch _ =>
return do
bar)
set_option trace.debug true
#eval test
Eric Wieser (Jan 26 2024 at 14:21):
The trick is to enter a nested do
block inside the try
, but call it from the outside
Eric Wieser (Jan 26 2024 at 14:22):
But this of course means that you can't do a return
inside the try
block; so this would need to be handled as part of the do
elaborator
Mac Malone (Jan 26 2024 at 16:56):
@Eric Wieser When it comes to mirroring Python logic, it should be noted that the Lean try/catch
and Python try/except
do not already have the same semantics:
Lean
-- error: catch2
#eval show IO Unit from do
try
throw <| IO.userError "try"
catch _ =>
throw <| IO.userError "catch1"
catch _ =>
throw <| IO.userError "catch2"
Python
# error: catch1
try:
raise RuntimeError("try")
except Exception:
raise RuntimeError("catch1")
except Exception:
raise RuntimeError("catch2")
The difference here being that the Lean try/catch
is cumulative whereas the Python try/except
is disjoint. Thus, if Python-style try/else
where implemented in Lean, there is design decision on whether it should succeed if the just try
was error free or if the all the catch
satements were also error free. The later would appear to be more consistent with the current design.
Eric Wieser (Jan 26 2024 at 16:59):
I guess the Lean way to spell disjoint catch
es is
try
throw <| IO.userError "try"
catch
| (.userError s) =>
throw <| IO.userError "catch1"
| _ =>
throw <| IO.userError "catch2"
Eric Wieser (Jan 26 2024 at 17:00):
I think it would be reasonable to just ban multiple catch
es if else
is used
Mauricio Collares (Jan 26 2024 at 17:14):
Eric Wieser said:
finally
runs even if an error does happen, and so does not have access to local variables within thetry
Not sure I follow; control blocks do not create new lexical scopes in Python, and therefore local variables inside the try
are local to the function you're in, and will be accessible in the finally block. Of course, if an exception is raised before assigning to a new variable inside the try
block, it won't be defined in the finally
block (unlike JS, say, where let bindings are hoisted).
Mac Malone (Jan 26 2024 at 17:37):
Eric Wieser said:
I guess the Lean way to spell disjoint
catch
es is
That only works if the exception types are homongenous. There is no way I know of to express a heterogenous disjoint try/catch
in Lean. For example, consider:
-- catchIO
#eval show IO (Except Nat Unit) from ExceptT.run do
try
throw 0
catch n : Nat =>
throwThe IO.Error <| .userError "catchNat"
catch e : IO.Error =>
throwThe IO.Error <| .userError "catchIO"
Mac Malone (Jan 26 2024 at 17:51):
Here is a quick-and-dirty example of a cumulative try/catch/else
in Lean:
import Lean.Parser.Do
open Lean Parser Term
syntax doElseTerm :=
Term.binderIdent (" : " term)?
syntax doElse :=
ppDedent(ppLine) ("else " atomic(doElseTerm darrow)?) doSeq
syntax (name := doTryElse) (priority := high)
"try" doSeq (doCatch <|> doCatchMatch)* (doElse)? (doFinally)? : doElem
instance : Coe (TSyntax ``Term.hole) Term where
coe x := Unhygienic.run do withRef x `($x:hole)
instance : Coe (TSyntax [identKind, ``Term.hole]) Term where
coe x := match x with | `($x:ident) => x | `($x:hole) => x | _ => unreachable!
instance : Coe (TSyntax ``doTry) (TSyntax ``doSeq) where
coe x := Unhygienic.run do withRef x `(doSeq|$x:doTry)
macro_rules
| `(doTryElse|try $x:doSeq else%$ref $[$a? =>]? $y:doSeq) => do
let a : Term ← do
if let some a := a? then
let `(doElseTerm|$a $[: $ty?]?) := a
| Macro.throwUnsupported
if let some ty := ty? then `(($a : $ty)) else pure a
else
withRef ref `(_)
`(doElem|match (← observing do $x) with | .ok $a => $y | .error e => throw e)
| `(doTryElse|try $x:doSeq $catches* $[$els?:doElse]? $[$fin?]?) => do
let mut x : TSyntax ``doSeq := x
x := ← `(doTry|try $x:doSeq $catches*)
if let some els := els? then x := ← `(doSeq|try $x:doSeq $els:doElse)
if let some fin := fin? then x := ← `(doTry|try $x:doSeq $fin:doFinally)
`(doElem|do $x)
def foo : IO Int := pure 1
def bar : IO Int := pure 2
def baz (x : Int) : IO Int := return x + 10
-- 11
#eval show IO Int from do
try
foo
catch _ =>
bar
else x =>
baz x
Eric Wieser (Jan 26 2024 at 18:50):
Doesn't that else
run if foo fails?
Eric Wieser (Jan 26 2024 at 18:50):
And does this work for accessing local variables within the try like the ones in my example?
Last updated: May 02 2025 at 03:31 UTC