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
letvariables 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.doReassignto 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