Zulip Chat Archive

Stream: general

Topic: Weird issue with do notation/reader monad


Aaron Eline (Feb 04 2026 at 15:28):

I'm not sure why these should be semantically different, but they are? Unsure if I'm just really misunderstanding things. Could someone clarify why this works the way it does or if I'm holding it wrong?

abbrev M (α : Type)  : Type := ReaderT Env (Result TypeError) α

def locally (f : Env  Env) (k : M α) : M α :=  do
  let env  read
  k (f env)

def locally' (f : Env  Env) (k : M α) : M α :=  do
  let env  read
  -- This does not work
  -- Results in bindings getting lost
  ReaderT.run k (f env)

image.png
(This is the bind that locally' desugars

Malvin Gattinger (Feb 04 2026 at 16:46):

I wanted to try out the code on live.lean-lang.org but cannot get it to compile. Do you have an #mwe with imports?

Aaron Eline (Feb 05 2026 at 00:47):

Here is a mwe:

abbrev M (α : Type)  : Type := ReaderT Nat Option α

def locally (f : Nat  Nat) (k : M α) : M α :=  do
  let env  read
  k (f env)

def locally' (f : Nat  Nat) (k : M α) : M α :=  do
  let env  read
  -- This does not work
  -- Results in bindings getting lost
  ReaderT.run k (f env)

def some_computation (x : Nat) : M Nat := do
  let z  read;
  return (x + z)

def higher : M Nat := do
  locally (λ x => x + 1) (some_computation 2)

def higher' : M Nat := do
  locally' (λ x => x + 1) (some_computation 2)

def top : Option Nat :=
  ReaderT.run higher 0

def top' : Option Nat :=
  ReaderT.run higher' 0

#eval top
#eval top'

Eric Wieser (Feb 05 2026 at 02:20):

Using #print you can see you got

def locally' : {α : Type}  (Nat  Nat)  M α  M α :=
fun {α} f k => do
  let env  read
  (liftM k).run (f env)

where that liftM is probably causing trouble

Aaron Liu (Feb 05 2026 at 02:35):

liftM k has type ReaderT Nat M α

Aaron Liu (Feb 05 2026 at 02:35):

so you've added on an additional Nat reader state which is then immediately discarded by liftM

Aaron Eline (Feb 05 2026 at 03:21):

Sure, but _why_, is the do notation behaving weird here? ReaderT.run k (f e) and k (f e) reduce to the same thing, so why is using inside do changing things?

Aaron Liu (Feb 05 2026 at 03:30):

k (f env) has type Option α so it inserts a coercion and you get liftM (k (f env))

Aaron Eline (Feb 05 2026 at 03:31):

k (f env) is the one that produces the correct results?

Aaron Liu (Feb 05 2026 at 03:31):

ReaderT.run k (f e) has an expected type M α so it elaborates the argument k with expected type ReaderT Nat M α

Aaron Liu (Feb 05 2026 at 03:31):

so the coercion is inserted inside

Aaron Liu (Feb 05 2026 at 03:32):

this doesn't happen with k (f env) because the type of k doesn't have any holes in it so having an expected type doesn't change your elaboration

Aaron Liu (Feb 05 2026 at 03:32):

so it works if you elaborate ReaderT.run k (f e) without an expected type

Harry Goldstein (Feb 05 2026 at 03:40):

Is the claim here that this is a type inference issue, then? Somehow inference is assuming ReaderT.run is running a different reader monad, which is inserted via coercion, rather than running the reader that it seems like it should be running? This seems like pretty unintuitive behavior

Eric Wieser (Feb 05 2026 at 06:12):

If you write

def locally' (f : Nat  Nat) (k : M α) : M α :=  do
  let env  read
  (ReaderT.run k (f env) :)

then it does what you expect

Eric Wieser (Feb 05 2026 at 06:13):

Otherwise the coercion gets inserted in the wrong place

Eric Wieser (Feb 05 2026 at 06:13):

At any rate, you're looking for docs#withTheReader I think

Harry Goldstein (Feb 05 2026 at 12:49):

I love that if you just write a little smiley face it works correctly! :)

Do we think there's any hope of making this behavior more intuitive / less error-prone? I don't think the code that Aaron wrote was particularly arcane — an average user could totally run into this and write some pretty hard-to-find bugs. Would a change to the implicit arguments of run help? Or maybe some kind of warning on do elaboration? I realize that this behavior is consistent with the rest of Lean, so it's not a bug in that sense, but I would call this behavior "incorrect" from the perspective of most reasonable end-users

Eric Wieser (Feb 05 2026 at 15:46):

One thing that I think would help is changing ReaderT (and stateT etc) to a structure along the lines of

structure ReaderT (R m A) where
  mk :: run (r : R) : m A

Aaron Eline (Feb 05 2026 at 15:46):

Chiming in that I did in fact find this behavior baffling, as someone who's relatively used to writing monadic code in ocaml/haskell. Took me a bit to find the workaround I did.

Eric Wieser (Feb 05 2026 at 15:47):

Maybe @Sebastian Ullrich knows if that change would result in worse compilation output

Sebastian Ullrich (Feb 05 2026 at 15:56):

It shouldn't, no

Robin Arnez (Feb 05 2026 at 15:57):

It would?

structure ReaderTStruct (ρ : Type u) (m : Type u  Type v) (α : Type u) where
  mk :: run (r : ρ) : m α

set_option trace.compiler.ir.result true
/--
trace: [Compiler.IR] [result]
    def testStruct._lam_0 (x_1 : @& tobj) : tobj :=
      inc x_1;
      ret x_1
    def testStruct._lam_0._boxed (x_1 : tobj) : tobj :=
      let x_2 : tobj := testStruct._lam_0 x_1;
      dec x_1;
      ret x_2
[Compiler.IR] [result]
    def testStruct._closed_0 : obj :=
      let x_1 : obj := pap testStruct._lam_0._boxed;
      ret x_1
    def testStruct : obj :=
      let x_1 : obj := testStruct._closed_0;
      ret x_1
-/
#guard_msgs in
def testStruct : ReaderTStruct Nat Id Nat :=
  .mk fun x => x

/--
trace: [Compiler.IR] [result]
    def test (x_1 : @& tobj) : tobj :=
      inc x_1;
      ret x_1
    def test._boxed (x_1 : tobj) : tobj :=
      let x_2 : tobj := test x_1;
      dec x_1;
      ret x_2
-/
#guard_msgs in
def test : ReaderT Nat Id Nat :=
  .mk fun x => x

ReaderTStruct forces the compiler to allocate a closure

Robin Arnez (Feb 05 2026 at 15:58):

At least, with the current state of the compiler

Aaron Liu (Feb 05 2026 at 23:15):

Eric Wieser said:

One thing that I think would help is changing ReaderT (and stateT etc) to a structure along the lines of

structure ReaderT (R m A) where
  mk :: run (r : R) : m A

I'm not sure this would help?


Last updated: Feb 28 2026 at 14:05 UTC