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:

grind eagerly unfolds all reducible declarations so abbrevs don't need new grind annotations

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