Zulip Chat Archive
Stream: Is there code for X?
Topic: Deriving dot notation from class instances
Fabrizio Montesi (Nov 08 2025 at 11:32):
I'd like an attribute (or something similar) for instances that generates appropriate abbreviations for dot notation. For example:
structure Str where
n : Nat
@[dotify Str]
instance : ToString Str where
toString str := ToString.toString str.n
-- The use of dotify above generates:
-- def Str.toString : Str → String := ToString.toString
#eval { n := 3 : Str }.toString -- This works because of the generated abbrev. Otherwise, it'd throw an error as usual.
Does this exist already perhaps? It look feasible but nontrivial, as identifying the definitions that should be abbreviated requires some care. I guess one could also provide a parameter to force the inclusion/exclusion of some definitions.
We could really use this in CSLib, to avoid a proliferation of manually-written abbreviations.
Aaron Liu (Nov 08 2025 at 11:34):
how do you generate the name? Does it only do one-field classes
Fabrizio Montesi (Nov 08 2025 at 11:35):
You mean Str? I passed it as an argument to the annotation.
Aaron Liu (Nov 08 2025 at 11:35):
Fabrizio Montesi said:
Does this exist already perhaps? It look feasible but nontrivial, as identifying the definitions that should be abbreviated requires some care. I guess one could also provide a parameter to force the inclusion/exclusion of some definitions.
This probably doesn't exist but probably isn't too hard to make
Aaron Liu (Nov 08 2025 at 11:35):
Fabrizio Montesi said:
You mean
Str? I passed it as an argument to the annotation.
I mean the name Str.toString
Fabrizio Montesi (Nov 08 2025 at 11:38):
Here's a first informal idea:
Say I use @[dotify T].
This looks for all names under the class namespace (this would find toString), filters to get only those whose signature starts by taking an explicit parameter of type T, and then produces an abbreviation for each under T's namespace.
Aaron Liu (Nov 08 2025 at 11:41):
ok
Aaron Liu (Nov 08 2025 at 11:42):
that's an algorithm I can follow
Robin Arnez (Nov 08 2025 at 13:54):
Something like this?
import Mathlib
open Lean Meta
partial def dotify (inst : Name) (target : Name) : MetaM Unit := do
let val ← getConstVal inst
let doCompile := !isNoncomputable (← getEnv) inst
forallTelescopeReducing val.type (whnfType := true) fun xs body => do
let instApp := mkAppN (.const inst (val.levelParams.map Level.param)) xs
let rec traverse (instApp : Expr) (ty : Expr) : MetaM Unit := do
ty.withApp fun fn args => do
let .const nm us := fn | throwError "not a class: {body}"
unless isClass (← getEnv) nm do throwError "not a class: {body}"
let info := getStructureInfo (← getEnv) nm
for field in info.fieldInfo do
let projApp : Expr := .app (mkAppN (.const field.projFn us) args) instApp
let projType ← inferType projApp
if field.subobject?.isSome then
traverse projApp (← whnf projType)
continue
let name := target ++ field.fieldName
if (← getEnv).contains name then
continue
let type ← mkForallFVars xs projType
let value ← mkLambdaFVars xs projApp
if ← isProp projType then
addDecl <| .thmDecl {
name, type, value
levelParams := val.levelParams
}
else
let decl : Declaration := .defnDecl {
name, type, value
levelParams := val.levelParams
hints := .abbrev
safety := .safe
}
addDecl decl
if doCompile then
compileDecl decl
traverse instApp body
structure Str where
n : Nat
instance instToStringStr : ToString Str where
toString str := ToString.toString str.n
#eval! dotify ``instToStringStr `Str
#print Str.toString
def RealClone := Real
noncomputable instance : ConditionallyCompleteLinearOrderedField RealClone := inferInstanceAs (ConditionallyCompleteLinearOrderedField Real)
#eval! dotify ``instConditionallyCompleteLinearOrderedFieldRealClone `RealClone
#print RealClone.one_mul
... although I didn't do the "filters ..." part
Fabrizio Montesi (Nov 08 2025 at 18:03):
I need to read up some more about macro development to fully understand it, but the effect looks pretty good, thanks!
I've found a few discussions with desiderata that could be (at least partially) addressed by such an annotation:
https://leanprover-community.github.io/archive/stream/270676-lean4/topic/class.20dot.20notation.html
#new members > Why Dot notation doesn't work for instance methods?
#new members > Why is dot-notation not working here?
Hence the question: where should this (or some version of this) go? I'd like to be able to use it in CSLib, but maybe it could be good to put it in batteries? I don't know if there are plans to include a feature like this in Lean.
Ching-Tsun Chou (Nov 08 2025 at 18:23):
Suppose the dot-notation discussed above works, in the sense that one can use it in an expression and the expression compiles. But will all the theorems reachable via the dot-notation also be available to tactics like grind and simp? In the particular application that motivates this thread, we have a class like Acceptor below. Will the instances of language and mem_language be also be available to grind because of their grind annotations? If I still have to make instances of them and put the grind annotation on them, what exactly does the dot-notation buy me?
/-- An `Acceptor` is a machine that recognises strings (lists of symbols in an alphabet). -/
class Acceptor (α : Type _) (Symbol : outParam (Type _)) where
/-- Predicate that establishes whether a string `xs` is accepted. -/
Accepts (a : α) (xs : List Symbol) : Prop
namespace Acceptor
variable {Symbol : Type _}
/-- The language of an `Acceptor` is the set of strings it `Accepts`. -/
@[scoped grind .]
def language [Acceptor α Symbol] (a : α) : Language Symbol :=
{ xs | Accepts a xs }
/-- A string is in the language of an acceptor iff the acceptor accepts it. -/
@[scoped grind =]
theorem mem_language [Acceptor α Symbol] (a : α) (xs : List Symbol) :
xs ∈ language a ↔ Accepts a xs := Iff.rfl
Fabrizio Montesi (Nov 08 2025 at 18:25):
They might, I tried with manually-written abbrevs and it worked once (that's why I wanted abbrevs). But it needs further testing. If it doesn't work out of the box, we might need to replicate the grind annotations.
Ching-Tsun Chou (Nov 08 2025 at 18:40):
If I need to replicate the grind annotation for each instance of Acceptor, then what does Acceptor buy me?
Robin Arnez (Nov 08 2025 at 18:42):
grind eagerly unfolds all reducible declarations so abbrevs don't need new grind annotations
Fabrizio Montesi (Nov 08 2025 at 18:42):
For the sake of the argument: If we can't get grind to work I wouldn't use abbreviations for Acceptor at all then, or it'd kill the point of code deduplication. It'd become a choice between code deduplication (Acceptor) or having dot notation (copy defs for all instances).
But there's a good chance it'll work, we should check.
Fabrizio Montesi (Nov 08 2025 at 18:43):
Robin Arnez said:
grindeagerly unfolds all reducible declarations soabbrevs don't need newgrindannotations
Precisely this.
Ching-Tsun Chou (Nov 08 2025 at 18:57):
@Robin Arnez Just to be clear: Are you saying that if language above is declared using abbrev instead of def, then for every instance of Acceptor, grind will find the corresponding instances of language and mem_language? Note that the latter two are only in the namespace of Acceptor, not in the class declaration proper.
Fabrizio Montesi (Nov 08 2025 at 18:58):
No, the proxy def should be an abbrev (for example, DA.FinAcc.language in our current case in cslib).
Fabrizio Montesi (Nov 08 2025 at 18:59):
(and the macro we're discussing produces these proxy defs automatically)
Ching-Tsun Chou (Nov 08 2025 at 19:18):
So you are saying that I still have to make a abbrev definition for DA.FinAcc.language but I don't have to repeat the mem_language theorem for that instance? If so, is the saving (which is 4 lines of code) worth the introduction of an "acceptor" concept and the need for extending Lean? Note that the introduction of a new concept is not free either. It's one additional of indirection for a human reader of the code to comprehend and one additional file for Lean to load.
Fabrizio Montesi (Nov 08 2025 at 19:20):
No, you wouldn't need language either. You would just need the instance as we have now (annotated with @[dotify] or whatever we end up calling it).
Ching-Tsun Chou (Nov 08 2025 at 19:30):
OK, we saved another 4 lines of code. But it still seems to me that the concepts being abstracted into Acceptor are so meager that the saving from code deduplication does not justify the additional level of indirection and the need for extending Lean.
Chris Henson (Nov 08 2025 at 19:33):
Maybe let's discuss further the particular application to CSlib in that channel as to not get too off-topic for this thread.
Tomas Skrivan (Nov 10 2025 at 05:28):
Recently I wanted something similar. One field structures are used to do function overloading so I decided to streamline the notation for it and it supports dot notation too.
You first declare the function
declfun foo {α} (a : α) : α
then define it for some types
defun foo (x : Float) := 2*x
defun foo (x : Nat) := 10*x
or for structures
structure Vector2 where
(x y : Float)
deriving Repr
namespace Vector2
defun foo (x : Vector2) : Vector2 := ⟨foo x.1, foo x.2⟩
end Vector2
Now you can call
-- 100
#eval foo 10
-- 20
#eval foo 10.0
-- { x := 2.000000, y := 4.000000 }
#eval (Vector2.mk 1.0 2.0).foo
For structures either foo (Vector2.mk 1.0 2.0) or (Vector2.mk 1.0 2.0).foo works. The later is exactly the problem in question here.
code
Last updated: Dec 20 2025 at 21:32 UTC