Zulip Chat Archive
Stream: lean4
Topic: notation for unpacking structures and field assignment
Tomas Skrivan (Dec 08 2024 at 21:00):
I'm missing some convenience notation for working with structures in imperative code.
Field assignment: you can't assign fields with s.fst := 1
for s : ℕ×ℕ
.
#eval Id.run do
let mut s := (1,2)
s.fst := 3 -- error
s
This is fixable with a simple macro
open Lean Parser
syntax atomic(Term.ident) noWs "." noWs ident ":=" term : doElem
macro_rules
| `(doElem| $s:ident . $a:ident := $x) => `(doElem| $s:ident := {$s with $a:ident := $x})
now you can write
#eval Id.run do
let mut s := (1,2)
s . fst := 3
s
Unfortunately, there has to be a space round the dot, s . fst
not s.fst
. How can I fix this?
Unpacking structures: I would like to unpack structure s : ℕ×ℕ
and then refer to its field s.fst
and s.snd
only with fst
and snd
.
#eval
let ⟨..⟩ := (1,2)
fst -- error
doable with a simple elaborator
syntax (name:=let_struct_syntax) withPosition("let" "⟨..⟩" ":=" term) optSemicolon(term) : term
open Lean Elab Term Syntax Meta
elab_rules (kind:=let_struct_syntax) : term
| `(let ⟨..⟩ := $x:term
$b) => do
let X ← inferType (← elabTerm x none)
let .const struct _ := X.getAppFn' | throwError "structure expected"
let info := getStructureInfo (← getEnv) struct
let ids := info.fieldNames.map (fun n => mkIdent n)
let stx ← `(let ⟨$ids,*⟩ := $x; $b)
elabTerm stx none
Now the above works but it does not work in do
block. Ideally I would like to do
#eval Id.run do
let mut ⟨..⟩ := (1,2)
fst := 3
(fst,snd) -- (3,2)
However I do not know how to do this as you can't write elaborator for doElem
syntax category. Ideally it is only a macro but I need to infer the type of $x
and look it up for the field names.
Packing structures: once you unpack a structure you most likely need to pack it again. Any tips what the notation should look like?
These are the two options I can think of but I do not like either of them.
#eval Id.run do
let mut ⟨..⟩ := (1,2)
fst := 3
pack Prod
#eval Id.run do
let mut s ⟨..⟩ := (1,2)
fst := 3
pack s
Tomas Skrivan (Dec 08 2024 at 21:07):
In the process of writing the question I was able to solve the field assignment
syntax (name:=do_field_assigment) (priority:=high) atomic(Term.ident) ":=" term : doElem
macro_rules (kind:=do_field_assigment)
| `(doElem| $s:ident := $x) => do
let .str (.str .anonymous s) a := s.getId
| `(Parser.Term.doReassign| $s:ident := $x)
let s := mkIdent (.mkSimple s)
let a := mkIdent (.mkSimple a)
let stx ← `(Parser.Term.doReassign| $s:ident := {$s with $a:ident := $x} )
dbg_trace (prettyPrint stx)
return stx
#check Id.run do
let mut s := (1,2)
s.fst := 3
s
This prevents you from having let variables with dot in their name, but is that really a common thing to do?
Kyle Miller (Dec 08 2024 at 21:08):
You sort of beat me to it, but here's another way:
macro_rules
| `(doElem| $x:ident := $val) => do
let .str n f := x.getId | Macro.throwUnsupported
if n == .anonymous then Macro.throwUnsupported
let o := mkIdentFrom x n
let field := mkIdentFrom x (Name.mkSimple f)
`(doElem| $o:ident := {$o with $field:ident := $val})
#eval Id.run do
let mut s := (1,2)
s.fst := 3
s
Kyle Miller (Dec 08 2024 at 21:08):
I didn't know let
variables with dots in names was even supported.
Tomas Skrivan (Dec 08 2024 at 21:09):
Kyle Miller said:
(I didn't know
let
variables with dots in names was even supported.)
(I didn't test it but assumed so ... testing ... yes they are supported :)
Tomas Skrivan (Dec 08 2024 at 21:11):
Nice! I didn't realize that I don't have to define a new syntax. I had to explicitly mention Parser.Term.doReassign
to prevent an infinite loop.
Kyle Miller (Dec 08 2024 at 21:11):
There's some issue with the unused variable linter still, but at least it seems to be functional.
Kyle Miller (Dec 08 2024 at 21:14):
Tomas Skrivan said:
Unpacking structures
An issue with this idea is that it could shadow other variable names. At least with
#eval
let {..} := (1,2)
0
(which fails) you can see that fst
and snd
are in the local context with hygienic names, so the feature is sort of there.
I'd hesitate to make a rule like "..
creates non-hygienic names if it is not shadowing other names in the context" since that could lead to really confusing bugs when you copy/paste code. For example, if you add an argument to a function you could cause ..
to not add a name. Or, if ..
always shadowed names, that would open you up to bugs from adding a field to a structure.
Tomas Skrivan (Dec 08 2024 at 21:22):
I agree that it is potentially problematic but recently I wrote this code
Id.run do
let mut state := state
state.s := - (state.invH * state.g)
state.g_previous := state.g
state.dx := state.alpha • state.s
state.x_previous := state.x
state.x := state.s + state.x
state.f_x_previous := state.f_x
return state
and I feel that this would be cleaner
Id.run do
let mut ⟨..⟩ := state
s := - (invH * g)
g_previous := g
dx := alpha • s
x_previous := x
x := s + x
f_x_previous := f_x
return (pack state)
Tomas Skrivan (Dec 08 2024 at 21:23):
In in scenarios like this I think it is reasonable to do this.
Kyle Miller (Dec 08 2024 at 21:37):
Maybe you could make an elaborator pack MyStruct
that finds the names of all the fields and expands to { field1, field2, field3 }
, and then you could write let mut (pack MyStruct) := state
and end with return pack MyStruct
?
Kyle Miller (Dec 08 2024 at 21:37):
This is using the principle of "the pattern should use the same syntax as the constructor"
Tomas Skrivan (Dec 08 2024 at 21:40):
Good idea! I will try that
Mauricio Collares (Dec 09 2024 at 17:12):
Arguably related: there was an old mathlib4 PR experimenting with lenses at mathlib4#290
Last updated: May 02 2025 at 03:31 UTC