Zulip Chat Archive
Stream: lean4
Topic: Fun macro - bulleted argument lists
David Thrane Christiansen (Aug 14 2023 at 20:57):
Earlier today I saw some people using block arguments and single-statement do
in Haskell to provide a structured way of providing arguments to a function as if they were bulleted lists. This doesn't work in Lean:
List.map
do fun x => x + 1
do [1, 2, 3]
This is because Lean demands a monad even if the desugaring of the do
doesn't require it.
However, macros allow me to directly add this feature, rather than repurposing do
:
/-- Provide a number of arguments as an indented bulleted list
-/
syntax term "with" many1Indent("•" term) : term
macro_rules
| `(term| $e with $[• $a:term ]*) => `(term| $e $a*)
#eval
List.map with
• fun x => x + 1
• [1, 2, 3]
It's also possible to use bullet points for other related constructors:
syntax &"List" "with" many1Indent("•" term) : term
macro_rules
| `(term| List with $[• $a:term ]*) => `(term| [ $a,* ])
#eval
List.map with
• fun x => x + 1
• List with
• 1
• 2
• 88
I thought this was a fun little demo :-) It's probably too obscure for real code, but when list elements or function arguments are themselves large expressions and a let
would for some reason make it harder to read, something like this starts getting tempting.
James Gallicchio (Aug 14 2023 at 22:12):
seems like a fantastic gateway drug leading to structured editors :joy:
Anatole Dedecker (Aug 15 2023 at 09:14):
Do you know about docs#Id ? It exists precisely to be able to make a monad out of something that isn’t to be able to use do notation. I think you just have to use Id.run do
instead of do
Anatole Dedecker (Aug 15 2023 at 09:15):
But that doesn’t really help here since you’d need parentheses
Anatole Dedecker (Aug 15 2023 at 09:16):
The other option is to simply use <|
Ioannis Konstantoulas (Aug 19 2023 at 15:14):
This is lovely.
Eric Wieser (Aug 19 2023 at 16:17):
I'd argue that this would make more sense with ·
as the symbol, as this is already used for bullets within proofs
/-- Provide a number of arguments as an indented bulleted list
-/
syntax term "with" many1Indent("·" term) : term
macro_rules
| `(term| $e with $[· $a:term ]*) => `(term| $e $a*)
#eval
List.map with
· fun x => x + 1
· [1, 2, 3]
Eric Wieser (Aug 19 2023 at 16:29):
It would be neat if something like
#eval
List.map with
f x := x + 1
l := [1, 2, 3]
were allowed as term version of the syntax
example :=
List.map f l
where
f x := x + 1
l := [1, 2, 3]
or as notation for
let f x := x + 1
let l := [1, 2, 3]
List.map f l
Ioannis Konstantoulas (Aug 22 2023 at 09:53):
Eric Wieser said:
I'd argue that this would make more sense with
·
as the symbol, as this is already used for bullets within proofs/-- Provide a number of arguments as an indented bulleted list -/ syntax term "with" many1Indent("·" term) : term macro_rules | `(term| $e with $[· $a:term ]*) => `(term| $e $a*) #eval List.map with · fun x => x + 1 · [1, 2, 3]
I would love this, especially if it could help eliminate some parentheses. A lot of the time I have (term-mode) code as follows:
Or.elim p
(complicated
stuff)
(more_complicated
stuff)
and getting lost in parentheses. It is worse when I have more than two arguments. If I could replace this with the dot, that would be great. Unfortunately, when I add your code to my file, I get hit with tons of errors of the form expected ·
in match
statements.
Any ideas how to fix this error, and if some precedence rule magic can make it work like parentheses?
Joachim Breitner (Aug 22 2023 at 10:41):
@David Thrane Christiansen , neat! Have you been at the GHC Implementors Workshop 2017? I gave a lightning talk asking for that feature in Haskell (see https://www.joachim-breitner.de/blog/730-Less_parentheses, “Less parentheses 1: Bulleted argument lists”). It’s certainly on my list of syntactic ideas to consider if I ever get to write a new PL.
Mario Carneiro (Aug 22 2023 at 10:43):
One way you can avoid the parentheses is using tactics:
refine Or.elim p ?_ ?_ -- or apply Or.elim p
· exact complicated stuff
· exact more_complicated stuff
Ioannis Konstantoulas (Aug 22 2023 at 11:09):
Mario Carneiro said:
One way you can avoid the parentheses is using tactics:
refine Or.elim p ?_ ?_ -- or apply Or.elim p · exact complicated stuff · exact more_complicated stuff
This is neat, but using it for arbitrary functions (rather than just theorems) becomes cumbersome; I'd like to have this bulleted argument list for all applications to avoid parentheses.
Mario Carneiro (Aug 22 2023 at 17:08):
I definitely use refine
in this way, even for definitions, mainly to avoid awkwardly nested large subterms by breaking them out into separate blocks
Mario Carneiro (Aug 22 2023 at 17:11):
bad:
example : (∀ x, x < 0) ↔ Nonempty (∀ y, y = y + 1) :=
⟨fun h => ⟨by
some
tactics
here⟩,
fun ⟨h⟩ => by
some
other
tactics⟩
good:
example : (∀ x, x < 0) ↔ Nonempty (∀ y, y = y + 1) := by
refine ⟨fun h => ⟨?_⟩, fun ⟨h⟩ => ?_⟩
· some
tactics
here
· some
other
tactics
Joachim Breitner (Aug 22 2023 at 17:16):
Does that mean that refine
works as a term as well, not only in tactic mode? Or did you forget a by
?
Mario Carneiro (Aug 22 2023 at 17:43):
forgot a by
Ioannis Konstantoulas (Aug 22 2023 at 19:52):
But do you also use it for normal programming? That is what I mean by "general functions"; do you go to tactic mode in order to invoke functions with complicated arguments? I am not saying it's wrong, it just feels a bit too much to me.
Mario Carneiro (Aug 22 2023 at 19:58):
Yes, sometimes. I would need a more realistic example to evaluate for sure
Mario Carneiro (Aug 22 2023 at 19:59):
I definitely try to avoid large subexpressions, but in a do block I would more likely accomplish that by binding the subexpressions to let
binders
David Thrane Christiansen (Aug 23 2023 at 11:21):
Joachim Breitner said:
David Thrane Christiansen , neat! Have you been at the GHC Implementors Workshop 2017? I gave a lightning talk asking for that feature in Haskell (see https://www.joachim-breitner.de/blog/730-Less_parentheses, “Less parentheses 1: Bulleted argument lists”). It’s certainly on my list of syntactic ideas to consider if I ever get to write a new PL.
I was not there, unfortunately, but you seem to have made an impact :-)
It's beginning to sound like this quick hack of a macro deserves some more thought, to properly account for implicit arguments and by-name arguments, and then it could even end up being useful!
Mac Malone (Aug 23 2023 at 21:12):
@David Thrane Christiansen / @Eric Wieser / @Ioannis Konstantoulas Here is version that supports by-name arguments and function sugar:
import Lean.Parser.Term
open Lean
open Parser.Term
def funTk := leading_parser Lean.Parser.unicodeSymbol " ↦ " " => "
syntax funBulletArg := atomic(ident funBinder* (typeSpec)? funTk) term
syntax namedBulletArg := atomic(ident " := ") term
/-- An argument written in bullet form. -/
syntax bulletArg := "· " (namedBulletArg <|> funBulletArg <|> term)
/-- Provide a number of arguments as an indented bulleted list. -/
syntax:lead term:max " with' " ppIndent(many1Indent(ppLine bulletArg)) : term
macro_rules
| `(term| $e with' $[· $as]*) =>
as.foldlM (β := Term) (init := e) fun e a =>
let k := a.raw.getKind
match k with
| ``namedBulletArg =>
match a with
| `(namedBulletArg| $id:ident := $a) => `($e ($id := $a))
| _ => Macro.throwErrorAt a s!"ill-formed {k}"
| ``funBulletArg =>
match a with
| `(funBulletArg| $id:ident $bs* $[$ty?]? => $a) =>
`($e ($id := fun $bs* $[$ty?]? => $a))
| _ => Macro.throwErrorAt a s!"ill-formed {k}"
| _ => `($e $(⟨a.raw⟩)) -- term
#eval
List.map with'
· α := Nat
· f x ↦ x + 1
· [1, 2, 3]
It currently has to use with'
to avoid clashing with match
's, but this could be solved with a change in core that uses withForbiddenTk
in match
(and change
).
Eric Wieser (Aug 23 2023 at 21:15):
I'd argue it should be f x := x
not f x => x
, else it's inconsistent with where
and let
syntax
David Thrane Christiansen (Aug 23 2023 at 21:20):
That's pretty great!
I'd tend to agree with @Eric Wieser about the color of the velocipede storage facility.
Mario Carneiro (Aug 23 2023 at 21:23):
to be fair, lean is quite inconsistent about :=
vs =>
elsewhere in the grammar anyway
Mac Malone (Aug 23 2023 at 21:24):
@Eric Wieser / @David Thrane Christiansen In that case, here you go:
import Lean.Parser.Term
open Lean
open Parser.Term
/-- A bullet argument: A nameable argument with sugar for functions. -/
syntax bulletArg := atomic(ident funBinder* (typeSpec)? " := ")? term
/-- Provide a number of arguments as an indented bulleted list. -/
syntax:lead term:max " with' " ppIndent(many1Indent(ppLine "· " bulletArg)) : term
macro_rules
| `(term| $e with' $[· $as]*) =>
as.foldlM (β := Term) (init := e) fun e a =>
match a with
| `(bulletArg| $id:ident $bs* $[$ty?]? :=%$dtk $a) =>
if bs.size > 0 then
`($e ($id :=%$dtk fun $bs* $[$ty?]? => $a))
else
if let some ty := ty? then
match ty with
| `(typeSpec| :%$tk $ty) =>
`($e ($id :=%$dtk ($a :%$tk $ty)))
| _ => Macro.throwErrorAt ty "ill-formed type specifier"
else
`($e ($id :=%$dtk $a))
| `(bulletArg| $a:term) => `($e $a)
| _ => Macro.throwErrorAt a "ill-formed bullet argument"
#eval
List.map with'
· α := Nat
· f x := x + 1
· [1, 2, 3]
David Thrane Christiansen (Aug 23 2023 at 21:30):
Now we just need to hope this beast never escapes the lab :-)
Mac Malone (Aug 23 2023 at 21:48):
Note that this syntax could be made simpler by dropping the bullet without introducing significant ambiguity (due to its already space-sensitive nature).
Mario Carneiro (Aug 23 2023 at 22:22):
aside: I would really like (f x := x + 1)
to be legal for regular optional args as well
Mario Carneiro (Aug 23 2023 at 22:22):
that comes up in particular for (motive := _)
args
Last updated: Dec 20 2023 at 11:08 UTC