Zulip Chat Archive

Stream: new members

Topic: compact usage of Except


JJ (Aug 16 2025 at 20:56):

I have converted some code previously using Option α to Except ε α. Due to losing the auto-conversion of α -> Option α, and the compactness of none vs. Except.error err, the code is quite a bit more verbose. Does anyone have advice for golfing this down?

import Lean.Data.AssocList

-- We implement a useful helper method on Option for conversion to Except.
namespace Option
  def toExcept (self : Option α) (err : ε) : Except ε α :=
    match self with
    | .some a => Except.ok a
    | .none => Except.error err
end Option

-- We consider types separate from terms, to start.
-- TODO: how to rename identifiers locally? would like to use Type
inductive Type' where
| Nat
| Func (a b : Type')
deriving BEq  -- structural equality is enough, for now.

-- The simply typed lambda calculus.
inductive Expr where
-- Nat
| zero
| succ (num : Expr)
| ind (num base step : Expr) (type : Type')
-- Func
| abs (name : String) (body : Expr)
| app (func arg : Expr)
| var (name : String)
-- Type'
| the (expr : Expr) (type : Type')

instance : ToString Type' := toString
where toString : Type' -> String
| .Nat => "Nat" | .Func a b => s!"{toString a} → {toString b}"

instance : ToString Expr := toString
where toString : Expr -> String
| .zero => s!"0"
| .succ num => s!"{toString num} ++"
| .ind num base step type => s!"(ind {type} {toString num} {toString base} {toString step})"
| .abs name body => s!"(λ.{name} {toString body})"
| .app func arg => s!"{toString func} {toString arg}"
| .var name => name
| .the expr type => s!"({toString expr} : {type})"

-- The type context is a mapping from names to types.
abbrev Context := Lean.AssocList String Type'

-- Check an expression against a given type.
partial def check (expr : Expr) (type : Type') (ctx : Context) : Bool :=
  match expr with
  | .zero => type == .Nat
  | .succ num => type == .Nat && check num .Nat ctx
  | .abs name body =>
    match type with
    | .Func a b => check body b (ctx.cons name a)
    | .Nat => false
  | _ =>
    match synth expr ctx with
    | .ok type' => type == type'
    | .error _ => false

-- Synthesize a type from an expression, if possible.
-- We report useful error messages on failure.
-- TODO: with ε ≠ α, any nice way to automagically convert types?
where synth (expr : Expr) (ctx : Context) : Except String Type' := do
  match expr with
  | .zero => Except.ok Type'.Nat
  | .succ num =>
    if check num .Nat ctx then Except.ok Type'.Nat
    else Except.error s!"expression {num} not of type Nat at {expr}"
  | .the expr type =>
    if check expr type ctx then Except.ok type
    else Except.error s!"expression {expr} not of type {type}"
  | .ind num base step type =>
    let num_type <- synth num ctx
    if num_type == .Nat then
      if check base type ctx then
        if check step (.Func .Nat (.Func type type)) ctx then
          Except.ok type
        else Except.error "step {step} of incorrect type"
      else Except.error "base {base} of incorrect type"
    else Except.error "num {num} not of type Nat"
  | .app func arg =>
    let func_type <- synth func ctx
    match func_type with
    | .Func a b =>
      if check arg a ctx then Except.ok b
      else Except.error s!"argument {arg} not of type {a} at {expr}"
    | .Nat => Except.error "attempting to apply an argument to a Nat"
  | .var name => ctx.find? name |>.toExcept "name {name} not found in context"
  | .abs _ _ => Except.error "cannot synth polymorphic type of abstraction name at {expr}"

JJ (Aug 22 2025 at 17:53):

I would still be interested in any advice here :-)

JJ (Aug 22 2025 at 18:07):

Hmm, an open Except helped quite a bit. I think it might be neat to auto convert non-string types to Except.ok, but obvs seem why this would be bad, too... prefixing everything with ok <| isn't half bad.

Kyle Miller (Aug 22 2025 at 18:32):

There's dotted function notation as an alternative to open Except. I see you're already using it with .Func, etc. You can write .ok and .error.

However, given that you are using do notation, any reason you're not using return and throw?

JJ (Aug 22 2025 at 18:33):

I was unaware throw existed!

Kyle Miller (Aug 22 2025 at 18:38):

There's also making use of do more and making a function to help you check conditions.

where synth (expr : Expr) (ctx : Context) : Except String Type' := do
  let guardOrThrow (b : Bool) (msg : Unit  String) : Except String Unit := do
    unless b do throw (msg ())
  match expr with
  | .zero => return Type'.Nat
  | .succ num =>
    guardOrThrow (check num .Nat ctx) fun _ => s!"expression {num} not of type Nat at {expr}"
    return Type'.Nat
  | .the expr type =>
    guardOrThrow (check expr type ctx) fun _ => s!"expression {expr} not of type {type}"
    return type
  | .ind num base step type =>
    let num_type <- synth num ctx
    guardOrThrow (num_type == .Nat) fun _ => s!"num {num} not of type Nat"
    guardOrThrow (check base type ctx) fun _ => s!"base {base} of incorrect type"
    guardOrThrow (check step (.Func .Nat (.Func type type)) ctx) fun _ => s!"step {step} of incorrect type"
    return type
  | .app func arg =>
    let func_type <- synth func ctx
    match func_type with
    | .Func a b =>
      guardOrThrow (check arg a ctx) fun _ => s!"argument {arg} not of type {a} at {expr}"
      return b
    | .Nat => throw "attempting to apply an argument to a Nat"
  | .var name => ctx.find? name |>.toExcept s!"name {name} not found in context"
  | .abs _ _ => throw s!"cannot synth polymorphic type of abstraction name at {expr}"

The Unit -> String is to make sure you avoid creating the string unless the error occurs.

Eric Wieser (Aug 22 2025 at 18:48):

guardOrThrow (c) fun _ => msg

is longer than what it replaces!

unless c do throw msg

Last updated: Dec 20 2025 at 21:32 UTC