Zulip Chat Archive

Stream: lean4

Topic: Meta-functions: meta programming for everyone


Tomas Skrivan (Mar 23 2025 at 14:47):

I have developed a mechanism to write "functions" that depend on the syntactic structure of their arguments. I'm curious if others would find this useful and what additional applications could be explored.

Here are a few examples:

1. Finding Concrete Bounds for a Function

-- 224
#eval getNaiveBoundOn (fun x :  => (x + 3)*(x - 4)*x) (Icc (-3) 4)

This "function" computes the following:
|(x + 3)*(x - 4)*x| ≤ |x + 3|*|x - 4|*|x| ≤ (4 + 3)*(4 + 4)*4 = 224.

2. Left-Associating Elements of a Product

-- (1, 2, 3)
#eval (lassoc ((1, 2), (), 3))

3. Lifting Elements of a Ring

-- "((#0 * #1) + ((#0 * #0) * #1))"
#eval ringToString [a, b] (a * b + a * a * b)

This is useful as the first step in writing other tactics like ring, module, or polyrith, where you need to transform an element (e.g., from a ring) into a form that other tactics can process.

4. Computing Derivatives

-- (1 * x + x * 1) * x + x * x * 1 + (0 * x + 3 * 1) + (1 * x + x * 1)
#eval_meta_fun derivAt (fun x => x * x * x + 3 * x + x * x) x

All these functions evaluate so-called meta-functions during elaboration. For example, getNaiveBoundOn is defined as follows:

def getNaiveBoundOn (f :   ) (s : Set ) {n} (h : NaiveBoundOn f s n := by data_synth) := n

Here, NaiveBoundOn is the meta-function, and data_synth is the new tactic that solves goals like NaiveBoundOn f s ?n, i.e., it finds n and demonstrates that it bounds f on s. All this is motivated by solving HasFDerivAt f ?f' x as I want to compute derivatives.

I’ve created two new commands, meta_fun_decl and meta_fun_def, to help define these new meta-functions:

meta_fun_decl naiveBoundOn (f :   ) (s : Set ) : 
satisfying
  ( x  s, |f x|  naiveBoundOn)

This declares a "function" NaiveBoundOn : (ℤ → ℤ) → Set ℤ → ℤ → Prop. In reality, NaiveBoundOn f s n is just a Prop structure with a single field ∀ x ∈ s, |f x| ≤ n.

With meta_fun_def, you can define its "values":

meta_fun_def naiveBoundOnIdentity (a b : ) :
  naiveBoundOn (fun x => x) (Icc a b) := max |a| |b|
satisfied_by
  ...

This can be recursively "called":

meta_fun_def naiveBoundOnAdd (f g :   ) (a b : ) :
  naiveBoundOn (fun x => f x + g x) (Icc a b) :=
    let bf := naiveBoundOn f (Icc a b)
    let bg := naiveBoundOn g (Icc a b)
    bf + bg
satisfied_by
  ...

This definition translates to the theorem:

@[data_synth]
theorem naiveBoundOnAdd (f g :   ) (a b : ) (bf : ) (hf : NaiveBoundOn f (Icc a b)) (bg : ) (hg : NaiveBoundOn g (Icc a b)) : NaiveBoundOn (f + g) (Icc a b) (bf + bg) := ...

Whether you prefer writing theorems like this or using the meta_fun_def macro is a matter of taste.

Another Example: Left Associating Product Types

This meta-function returns a type along with an equivalence:

meta_fun_decl prodAssoc (X : Type) : Type
satisfying
  X  prodAssoc

When reassociating ((X × Y) × Z), we call prodAssoc on itself:

meta_fun_def (priority := high-1) prodReassocMain (X Y Z : Type) :
  prodAssoc ((X × Y) × Z) := prodAssoc ((prodAssoc X) × Y × Z)
satisfied_by
  intro X' f XYZ' g
  constructor
  exact (Equiv.prodAssoc X Y Z
    |>.trans (Equiv.prodCongr f.1 (Equiv.refl _))
    |>.trans g.1)

Traditionally, this kind of function is implemented using a typeclass and a collection of instances. Similarly to instances, we can control which "evaluation" rule is applied using priorities.

You can find more details on how other meta-functions are defined in this file.


Naming Help: I currently use the term meta-function and the tactic data_synth, but neither term is perfect. I could rename the tactic to meta_fun_eval, but "meta-function" already refers to a function that operates on Lean.Expr. Suggestions are welcome.

Aaron Liu (Mar 23 2025 at 15:23):

I don't see how this is any different from writing meta-functions that act on Lean.Expr.

Tomas Skrivan (Mar 23 2025 at 15:26):

The point is user extensibility, imagine changing simp's code each time you add a new theorem.

Aaron Liu (Mar 23 2025 at 15:28):

I feel like that can be done the same way simp did it - with attributes. No need for a type class and fake function that don't respect equality.

Patrick Massot (Mar 23 2025 at 15:33):

Anything that makes it easier to build such tactics seem very useful to me.

Patrick Massot (Mar 23 2025 at 15:33):

Side-note: the tests in that file again tell us we should really fix that max delaboration issue…

Tomas Skrivan (Mar 23 2025 at 15:35):

Aaron Liu said:

I feel like that can be done the same way simp did it - with attributes. No need for a type class and fake function that don't respect equality.

The whole thing is based on the tactic data_synth I wrote. You can directly write theorems like:

@[data_synth]
theorem naiveBoundOnAdd (f g :   ) (a b : ) (bf : ) (hf : NaiveBoundOn f (Icc a b)) (bg : ) (hg : NaiveBoundOn g (Icc a b)) : NaiveBoundOn (f + g) (Icc a b) (bf + bg) := ...

In a way, these theorems are defining a function on Lean.Expr so the meta_fun_decl and meta_fun_defmake it more explicit and a lot easier to use for people not familiar writing meta level code.

Tomas Skrivan (Mar 23 2025 at 15:36):

Patrick Massot said:

Side-note: the tests in that file again tell us we should really fix that max delaboration issue…

Huh? What are you talking about? I'm not aware of this.

Mario Carneiro (Mar 23 2025 at 15:36):

max is being printed as \sqcup in the tests

Kyle Miller (Mar 23 2025 at 15:37):

This is really creative and neat! It's not every day that you see a new (meta)programming idiom

Mario Carneiro (Mar 23 2025 at 15:39):

Tomas Skrivan said:

Naming Help: I currently use the term meta-function and the tactic data_synth, but neither term is perfect. I could rename the tactic to meta_fun_eval, but "meta-function" already refers to a function that operates on Lean.Expr. Suggestions are welcome.

I think I would call this a (bounded) nondeterministic function instead of a meta-function

Tomas Skrivan (Mar 23 2025 at 15:52):

Yeah that would be more technically correct but is a bit of a mouthful.

Aaron Liu (Mar 23 2025 at 21:57):

Tomas Skrivan said:

Aaron Liu said:

I feel like that can be done the same way simp did it - with attributes. No need for a type class and fake function that don't respect equality.

The whole thing is based on the tactic data_synth I wrote. You can directly write theorems like:

@[data_synth]
theorem naiveBoundOnAdd (f g :   ) (a b : ) (bf : ) (hf : NaiveBoundOn f (Icc a b)) (bg : ) (hg : NaiveBoundOn g (Icc a b)) : NaiveBoundOn (f + g) (Icc a b) (bf + bg) := ...

In a way, these theorems are defining a function on Lean.Expr so the meta_fun_decl and meta_fun_defmake it more explicit and a lot easier to use for people not familiar writing meta level code.

I'm still against getNativeBoundOn and ringToString, because this goes against my model of how typeclasses are meant to be used.

Asei Inoue (Mar 24 2025 at 01:02):

very nice!!!

Eric Wieser (Mar 24 2025 at 01:35):

Kyle Miller said:

It's not every day that you see a new (meta)programming idiom

Isn't the "abuse a default argument to fill a metavariable" an old idiom? It's how we wrote reassoc_of and the matrix "simprocs" in Lean 3.


Last updated: May 02 2025 at 03:31 UTC