Zulip Chat Archive
Stream: general
Topic: Autogenerating recursive calls
Wojciech Nawrocki (Jan 10 2020 at 01:24):
Hello! Something I would really like for working with types that have many constructors is some kind of code generator for structural recursion in which I only want to touch some subsets of the constructors. For example, consider a simple type of formulas:
inductive formula | var: string → formula | plus: formula → formula → formula | mult: formula → formula → formula
for which I need to write a rename-variables function that changes all 'x's to 'y's:
def rename (x: string) (y: string): formula → formula | (var nm) := if nm = x then (var y) else (var nm) | (plus f₁ f₂) := plus (rename f₁) (rename f₂) | (mult f₁ f₂) := mult (rename f₁) (rename f₂)
Most of this function is trivial and simply calls itself recursively on occurrences of the same type within constructors. Writing such functions is boring and most of the code is boilerplate. Instead, I would like a way to simply say:
@[recurse] def rename (x: string) (y: string): formula → formula | (var nm) := if nm = x then (var y) else (var x)
or something similarly short and have Lean autogenerate the rest. Does anyone know if something like or close to it is possible? Has it been done in another language?
(This seems like the kind of thing some variation on lenses could work for but I'm not quite sure.)
Simon Hudon (Jan 10 2020 at 01:34):
I think I would break down the problem into generating a replace
function which looks like:
def replace (f: formula → option formula): formula → formula | x := match f x with | some y := y | none := match x with | var v := var v | plus x y := plus (replace x) (replace y) | mult x y := mult (replace x) (replace y) end end
(not sure if that compiles) and then define your recursive function (by hand or automatically) as:
def rename (x: string) (y: string) : formula → option formula | (var nm) := some $ if nm = x then (var y) else (var x) | _ := none def rename' (x: string) (y: string) : formula → formula := replace (rename x y)
Simon Hudon (Jan 10 2020 at 01:35):
The code for replace
can be generated using the induction
tactic.
Tim Daly (Jan 10 2020 at 04:50):
Actually, no. All the hash function is doing is checking that the proof object is unchanged. Lean could concatenate a version number to the proof it gives to Axiom (for the GCD, for example). When Axiom returns the GCD computation, Lean has to decide to trust the result. So Axiom provides the proof that Lean initially provided for GCD (which Axiom stored a long time ago) and the hash code. Lean can then concatenate the version number of the proof checker to the proof source code, compute the hash and compare it to the prior hash. If either the version number is wrong or the proof text has changed then the hash won't match.
A hash mismatch due to a version number change on Lean's proof checker means that Lean needs to fully check the provided proof. If the version number hasn't changed then the proof text must have changed and Lean needs to fully check the provided proof. But if the hash matches then nothing has changed and there is evidence that the GCD proof previously passed Lean's proof checker so it doesn't need to be re-checked, saving a lot of work.
The hash has nothing to do with the actual method of proof.
The problem to solve is to scale proof checking and trusted computation to larger computations. A Groebner basis can have many steps using other algorithms and each proof of each sub-algorithm needs to be provided to Lean to be checked. A hash checker would make this a very low cost but very trustable operation.
Tim Daly (Jan 10 2020 at 05:00):
The cryptographic idea that directly applies is called HMAC.key-hashed message authentication codes.
Wojciech Nawrocki (Jan 10 2020 at 11:30):
Thanks Simon! replace
doesn't compile but a version where the structural recursion is top-level does:
def replace₂ (f: formula → option formula): formula → formula | z@(var v) := match f z with | some y := y | none := z end | z@(plus x y) := match f z with | some y := y | none := plus (replace₂ x) (replace₂ y) end | z@(mult x y) := match f z with | some y := y | none := mult (replace₂ x) (replace₂ y) end
It works for the simple case, however I should have been more specific in that I also want to consider functions which may include arbitrary processing of recursive results, e.g.
@[recurse] def rename (x: string) (y: string): formula → formula | (var nm) := if nm = x then (var y) else (var nm) | (plus f₁ f₂) := mult (rename f₁) (rename f₂)
which I'm not sure generalize into a single higher-order function since then one cannot call replace
from rename
. This is the reason why I asked about code generation. I guess that's what tactics do and induction
is a good shout for going some way towards shorter definitions.
Simon Hudon (Jan 10 2020 at 12:55):
Let's try this again, then.
def rewrite (f: formula → formula): formula → formula | e@(var _) := f e | (plus e e') := f (plus (rewrite e) (rewrite e')) | (mult e e') := f (mult (rewrite e) (rewrite e'))
Then you should be able to write rename
so that, together with rewrite
it will form rename_all
. What do you think?
Chris B (Jan 10 2020 at 13:01):
I think this is also one of the big selling points of recursion schemes (ala Haskell) if you're interested, but I haven't actually studied them enough to know if you could implement them in Lean. @Simon Hudon Do you know if anything like Haskell's Fix
can be done with Lean?
Simon Hudon (Jan 10 2020 at 13:14):
I also love recursion schemes. Fix
is hard to encode directly but there are ways to get close. The simplest approach would be to define it as:
inductive fix (F : Type -> Type) : Type 1 | intro (a : Type) : F a -> (a -> fix) -> fix
It's not quite perfect though. For one thing, fix
lies in a different universe than F
so F fix
is not type correct. More type trickery can still help here.
In another direction, @Mario Carneiro, @Jeremy Avigad and I wrote a paper last year defining quotients of polynomial functors and using them to construct broader classes of inductive and coinductive data types in Lean. Fix
is a direct result of that theory QPF.
Wojciech Nawrocki (Jan 10 2020 at 13:33):
Then you should be able to write
rename
so that, together withrewrite
it will formrename_all
. What do you think?
Indeed that should work, but it will not if I modify the function a bit:
def weird_rename (x: string) (y: string): formula → formula | (var nm) := if nm = x then (var y) else (var nm) | (plus f₁ f₂) := mult f₁ (weird_rename f₂)
To express this, rewrite
would have to pass both rewritten and not-rewritten results, which can be done but I think it would have to make the resulting rewrite'
very specific. On first glance recursion schemes do seem to be solving a similar problem, thanks for the link!
Simon Hudon (Jan 10 2020 at 14:19):
No worries
Simon Hudon (Jan 10 2020 at 14:19):
Consider the following, I think it helps with your weird case:
open formula inductive formula_struct : formula → formula → Prop | plus₁ (f₁ f₂) : formula_struct f₁ (plus f₁ f₂) | plus₂ (f₁ f₂) : formula_struct f₂ (plus f₁ f₂) | mult₁ (f₁ f₂) : formula_struct f₁ (mult f₁ f₂) | mult₂ (f₁ f₂) : formula_struct f₂ (mult f₁ f₂) def weird_rename (x: string) (y: string): Π f : formula, (Π f', formula_struct f' f → formula) → option formula | (var nm) rec := some $ if nm = x then (var y) else (var nm) | (plus f₁ f₂) rec := some $ mult f₁ (rec f₂ $ by constructor) | (mult _ _) _ := none lemma well_founded_struct : well_founded formula_struct := sorry variable step : Π f : formula, (Π f', formula_struct f' f → formula) → option formula notation `♯` := by constructor def rewrite : formula → formula := well_founded.fix well_founded_struct $ λ x, match x with | e@(var v) := λ rec, match step e rec with | some e' := e' | none := e end | e@(plus x y) := λ rec, match step e rec with | some e' := e' | none := plus (rec x ♯) (rec y ♯) end | e@(mult x y) := λ rec, match step e rec with | some e' := e' | none := mult (rec x ♯) (rec y ♯) end end def weird_rename_all (x: string) (y: string): formula → formula := rewrite (weird_rename x y)
Simon Hudon (Jan 10 2020 at 14:19):
(it compiles)
Simon Hudon (Jan 10 2020 at 14:20):
But the proof of well-foundedness of the structural ordering is left as an exercise
Simon Hudon (Jan 10 2020 at 14:20):
You can also opt for a different well-founded relation
Wojciech Nawrocki (Jan 10 2020 at 15:05):
Thanks Simon, this is quite an interesting solution! By the way, is the generation of a formula_struct
roughly similar to what the equation compiler does?
Simon Hudon (Jan 10 2020 at 19:06):
By the way, is the generation of a formula_struct roughly similar to what the equation compiler does?
No. When declaring formula
, an axiom is added to the system:
#check @formula.rec -- formula.rec : -- Π {C : formula → Sort u_1}, -- (Π (a : string), C (formula.var a)) → -- (Π (a a_1 : formula), C a → C a_1 → C (formula.plus a a_1)) → -- (Π (a a_1 : formula), C a → C a_1 → C (formula.mult a a_1)) → -- Π (n : formula), C n
This is called a recursor. It is expressive enough to encode structural recursion and induction.
Wojciech Nawrocki (Jan 10 2020 at 23:43):
Right, weird_rename
is indeed expressible via formula.rec
. I was wondering more whether the equation compiler is capable of generating well-foundedness relations similar to formula_struct
in the wf_rec
module when structural recursion fails, as is the case for rewrite
but to think of it now that seems to be asking for way too much.
Simon Hudon (Jan 10 2020 at 23:49):
We could get formula_struct
generated ourselves
Last updated: Dec 20 2023 at 11:08 UTC