Zulip Chat Archive

Stream: lean4

Topic: postfix monad arrow


James Gallicchio (Jun 27 2023 at 05:44):

have we discussed adding a way to monad-bind as a postfix operation? something like (x : m a).← would work for me. can this be implemented as a macro on expressions, or would it require modifying core do notation?

Mario Carneiro (Jun 27 2023 at 05:46):

macros are expanded before expandLiftMethod, so it should be possible to implement outside core

James Gallicchio (Jun 27 2023 at 05:48):

oh huge, let me see if the braindead macro implementation works

James Gallicchio (Jun 27 2023 at 05:53):

:thinking:

syntax term ".←" : term

macro_rules
| `($t.←) => `(← $t)

#eval show IO _ from do
  IO.getStdin.←.flush -- invalid use of `(<- ...)`, must be nested inside a 'do' expression
  return 0

James Gallicchio (Jun 27 2023 at 05:56):

from trace.Elab.do:

with_annotate_term(Term.paren "(" (Term.proj (Term.paren "(" («term_.←» `IO.getStdin ".←") ")") "." `flush) ")")
      Bind.bind✝ (((IO.getStdin.←).flush) : ?m✝ PUnit✝)
        (fun (_ : PUnit✝) => with_annotate_term(Term.doReturn "return" [(num "0")]) Pure.pure✝ 0)

I'm not entirely sure how to parse this trace

Mario Carneiro (Jun 27 2023 at 05:57):

you can ignore the with_annotate_term(...) parts, that is just for error messages

Mario Carneiro (Jun 27 2023 at 05:57):

it appears to have macro expanded the do block without expanding the IO.getStdin.← part first

Mario Carneiro (Jun 27 2023 at 05:58):

the invalid use of `(<- ...)`, must be nested inside a 'do' expression error is what happens when you try to directly elaborate the expression <- e

Mario Carneiro (Jun 27 2023 at 05:58):

it is supposed to be eliminated as part of the macro expansion for the do, which finds and replaces them

James Gallicchio (Jun 27 2023 at 05:59):

Right. so it seems like the do elaborator doesn't fully elaborate the inner expressions before eliminating arrows.

Mario Carneiro (Jun 27 2023 at 06:00):

I see this:

      match (← liftMacroM <| expandMacro? doElem) with
      | some doElem => doSeqToCode (doElem::doElems)
      | none =>
      match (← liftMacroM <| expandDoIf? doElem) with
      | some doElem => doSeqToCode (doElem::doElems)
      | none =>
        let (liftedDoElems, doElem) ← expandLiftMethod doElem
        ...

but now I'm thinking that the first expandMacro? line is only expanding doElems that are directly macro terms

James Gallicchio (Jun 27 2023 at 06:01):

I assume in the above example the (macroful) term is wrapped in a seq doElem or whatever?

Mario Carneiro (Jun 27 2023 at 06:02):

a doExpr (which embeds term in doElem)

Mario Carneiro (Jun 27 2023 at 06:02):

the case that would be handled here is something like repeat or assert, which are actually doElems that expand to other doElems

Mario Carneiro (Jun 27 2023 at 06:03):

expansion of <- terms is done by expandLiftMethod which is not at all user-extensible:

private def liftMethodDelimiter (k : SyntaxNodeKind) : Bool :=
  k == ``Parser.Term.do ||
  k == ``Parser.Term.doSeqIndent ||
  k == ``Parser.Term.doSeqBracketed ||
  k == ``Parser.Term.termReturn ||
  k == ``Parser.Term.termUnless ||
  k == ``Parser.Term.termTry ||
  k == ``Parser.Term.termFor

private partial def hasLiftMethod : Syntax → Bool
  | Syntax.node _ k args =>
    if liftMethodDelimiter k then false
    else if k == ``Parser.Term.liftMethod then true
    else args.any hasLiftMethod
  | _ => false

James Gallicchio (Jun 27 2023 at 06:05):

can we elaborate the term while it's still contained in a doExprand before expandLiftMethod?

James Gallicchio (Jun 27 2023 at 06:06):

oh, but we'd have to explicitly elaborate in every term place in the do notation

Mario Carneiro (Jun 27 2023 at 06:06):

no, macro expansion happens from the outside in

Mario Carneiro (Jun 27 2023 at 06:06):

you could do it if you replaced do (which you can do in this case without actually rewriting that whole file)

Mario Carneiro (Jun 27 2023 at 06:07):

(implementation aside, I think e.← looks really silly so I hope you keep it to your personal projects :stuck_out_tongue: .)

James Gallicchio (Jun 27 2023 at 06:08):

hey, the syntax is up for debate, I just frequently want a way to do it in a pipe chain, and I'd be surprised if you don't feel the same urge to not have to make intermediary let binds just to monad bind :)

James Gallicchio (Jun 27 2023 at 06:09):

and directly calling bind isn't ergonomic there either

James Gallicchio (Jun 27 2023 at 06:10):

x |> bind (.|>.foo) looks so much worse than x.←.foo

James Gallicchio (Jun 27 2023 at 06:13):

This is a much spicier proposal: .!. Then we can have e.g. (e : Except String Int).! + 5 in monadic contexts (a treat for the rustaceans)

Mario Carneiro (Jun 27 2023 at 06:17):

import Lean
open Lean

macro (name := postfixLift) x:term noWs "." noWs "←" : term => `(← $x)
macro (name := pipeLift) x:term:min "|>." noWs "←" : term => `(← $x)

syntax:arg (name := do') (priority := high) ppAllowUngrouped "do " doSeq : term

@[macro do'] def expandDo' : Macro := fun stx =>
  stx.setKind ``Parser.Term.do |>.replaceM fun stx =>
    if stx.getKind == ``postfixLift || stx.getKind == ``pipeLift then
      Macro.expandMacro? stx
    else
      pure none

#eval show IO _ from do
  IO.getStdin.←.flush
  return 0

James Gallicchio (Jun 27 2023 at 06:35):

super cool. i'm assuming you still don't want it in Std?

Alex Keizer (Jun 27 2023 at 08:41):

James Gallicchio said:

x |> bind (.|>.foo) looks so much worse than x.←.foo

There's also (←x) foo, right? A bit more parenthesis than the postfix version, but still better than explicit bind or let-bindings.

James Gallicchio (Jun 27 2023 at 16:04):

Yeah, the parentheses are what I don't like

James Gallicchio (Jun 27 2023 at 16:04):

let me find the snippets from my code

James Gallicchio (Jun 27 2023 at 16:09):

I just grepped and found this in Aesop, it's this sort of thing where I'd rather have postfix ←:
(← (← getRootGoal).get).traceTree .tree

James Gallicchio (Jun 27 2023 at 16:11):

let _ ← (← bdRef.get).1.markAllSols is also a fun one :joy:

James Gallicchio (Jun 27 2023 at 16:11):

(from LeanSAT)

James Gallicchio (Jun 27 2023 at 16:11):

the arrows just get in the way for me when I'm reading this kind of code, since i have to jump back and forth from left to right instead of reading left to right continuously

James Gallicchio (Jun 27 2023 at 16:13):

it's obviously different when you're using normal (i.e. backwards to any good category theorist) function application and are reading right to left anyways


Last updated: Feb 28 2026 at 14:05 UTC