-
Notifications
You must be signed in to change notification settings - Fork 449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: consistent (fine-grained?) equational lemmas #3983
Comments
I haven't though very hard about it yet but that sounds like the least surprising solution to me. |
I agree, I think only |
Thanks for confirming my worries. I have now split the proposal into two, variant A would split the equational lemma only along Next question: Should it split only tail-recursive def drop (n : Nat) (xs : List α) : List α :=
if n = 0 then xs else
match xs with
| [] => []
| _ :: xs => drop (n-1) xs
/--
info: drop.eq_2.{u_1} {α : Type u_1} (n : Nat) (head : α) (xs_2 : List α) :
drop n (head :: xs_2) = if n = 0 then head :: xs_2 else drop (n - 1) xs_2
-/
#guard_msgs in
#check drop.eq_2 The existing equational theorems in a way lift the
in the sense that I have to split |
Another question: Which match expressions should be split? Clearly those that match on the functions’s parameters, or on variables introduced by such matches. But judging from the existing code, it will also try to split matches that match on other, possibly complicated, expressions, which would lead to similarly conditional equations as splitting on I think I have seen this in the wild recently, although I can't quite reproduce it now. I tried to make it split such a match in
but that merely shows the match-duplication behavior discussed in the previous comment. (And, incidentially, |
Possibly (depending how we treat
to add only the fine-grained equations to the default simp set (those that match on the constructor). |
After a discussion with Leo, in particular about the semantics of
|
@leodemoura pointed me to Using mathlib’s
(corresponding to the status quo) takes 3166 heartbeats, and
takes 3152 heartbeats. I did include run
before so that the lemma generation does not play a role here. It goes in the right direction but doesn’t look like a gamechanger just yet. Incidentially, I had to update leansat for my PR. Changes at leanprover/leansat@main...nomeata:leansat:ssft24. Noteworthy observations:
|
by removing the `tryRefl` variation between the two. Part of #3983
This is part of #3983. After #4154 introduced equational lemmas for non-recursive functions and #5055 unififed the lemmas for structural and wf recursive funcitons, this now disables the special handling of recursive functions in `findMatchToSplit?`, so that the equational lemmas should be the same no matter how the function was defined. The new option `eqns.deepRecursiveSplit` can be disabled to get the old behavior. This can break existing code, as there now can be extra equational lemmas: * Explicit uses of `f.eq_2` might have to be adjusted if the numbering changed. * Uses of `rw [f]` or `simp [f]` may no longer apply if they previously matched (and introduced a `match` statement), when the equational lemmas got more fine-grained. In this case either case analysis on the parameters before rewriting helps, or setting the option `opt.deepRecursiveSplit false` while defining the function
This is part of #3983. Fine-grained equational lemmas are useful even for non-recursive functions, so this adds them. The new option `eqns.nonrecursive` can be set to `false` to have the old behavior. ### Breaking channge This is a breaking change: Previously, `rw [Option.map]` would rewrite `Option.map f o` to `match o with … `. Now this rewrite will fail because the equational lemmas require constructors here (like they do for, say, `List.map`). Remedies: * Split on `o` before rewriting. * Use `rw [Option.map.eq_def]`, which rewrites any (saturated) application of `Option.map` * Use `set_option eqns.nonrecursive false` when *defining* the function in question. ### Interaction with simp The `simp` tactic so far had a special provision for non-recursive functions so that `simp [f]` will try to use the equational lemmas, but will also unfold `f` else, so less breakage here (but maybe performance improvements with functions with many cases when applied to a constructor, as the simplifier will no longer unfold to a large `match`-statement and then collapse it right away). For projection functions and functions marked `[reducible]`, `simp [f]` won’t use the equational theorems, and will only use its internal unfolding machinery. ### Implementation notes It uses the same `mkEqnTypes` function as for recursive functions, so we are close to a consistency here. There is still the wrinkle that for recursive functions we don't split matches without an interesting recursive call inside. Unifying that is future work.
#5129) This is part of #3983. After #4154 introduced equational lemmas for non-recursive functions and #5055 unififed the lemmas for structural and wf recursive funcitons, this now disables the special handling of recursive functions in `findMatchToSplit?`, so that the equational lemmas should be the same no matter how the function was defined. The new option `eqns.deepRecursiveSplit` can be disabled to get the old behavior. ### Breaking change This can break existing code, as there now can be extra equational lemmas: * Explicit uses of `f.eq_2` might have to be adjusted if the numbering changed. * Uses of `rw [f]` or `simp [f]` may no longer apply if they previously matched (and introduced a `match` statement), when the equational lemmas got more fine-grained. In this case either case analysis on the parameters before rewriting helps, or setting the option `opt.deepRecursiveSplit false` while defining the function
These three PRs implement the core of this proposal, and provide uniform equational theorems across all three kinds of functions (non-rec, wf, structural), so closing this as completed.
I did not change the logic of deciding how to split the equations; if we get more evidence that we need to change something here that’ll be a new RFC. |
leanprover#5129) This is part of leanprover#3983. After leanprover#4154 introduced equational lemmas for non-recursive functions and leanprover#5055 unififed the lemmas for structural and wf recursive funcitons, this now disables the special handling of recursive functions in `findMatchToSplit?`, so that the equational lemmas should be the same no matter how the function was defined. The new option `eqns.deepRecursiveSplit` can be disabled to get the old behavior. ### Breaking change This can break existing code, as there now can be extra equational lemmas: * Explicit uses of `f.eq_2` might have to be adjusted if the numbering changed. * Uses of `rw [f]` or `simp [f]` may no longer apply if they previously matched (and introduced a `match` statement), when the equational lemmas got more fine-grained. In this case either case analysis on the parameters before rewriting helps, or setting the option `opt.deepRecursiveSplit false` while defining the function
In this issue I’d like to flesh out and describe our plans for how Lean should generate equational lemmas.
Status quo
The status quo is unsatisfactory in a few ways, at least according to my current understanding:
The logic for creating the equational lemmas (in
Lean.Elab.Eqns.mkEqnTypes
) is basically “split until it holds byrfl
, and only near recursive calls”. This is somewhat hard to predict for users, and may (potentially, not sure) lead to different results depending on how the recursion is implemented.Because it splits as little as possible, we may get equational lemmas that have “big” case expressions on the right. When they are used by the simplifier to reduce applications to constructors, the term will become large, only to be made smaller again because of the case-of-constructor. More fine-grained equational lemmas would simplify more efficiently.
The equational lemmas do not necessarily correspond to the cases of the functional induction theorem, violating the principle of least surprise.
Equational lemmas do not really exist for non-recursive functions (they are always just the unfolding lemma), but would sometimes be useful there as well. Especially now that we have “lazyily defined theorems” by way of reserved names,
Sometimes the equational lemmas are suitable for
dsimp
, sometimes they are not.Right now, for structural recursion, first the equational lemmas are generated, and then the unfolding lemma, while for well-founded recursion, first the unfolding lemma is derived from
fix_eq
, and then the equational lemmas follow.Proposal (variant A)
Splitting heuristics
We want uniform equational lemams, so the same heuristics is used for non-recursive, structurally recursive and well-founded recursive lemmas. In particular
rfl
to stop early.This should split more than before, which should imply that where previously an equational lemma was by
rfl
, it will still be byrfl
, and where possible, we do userfl
in the proof to make themdsimp
lemmas. (TODO: not quite true: there are somematches
where splitting them introduces assumptions, and breaks therfl
-property. Should we not split those?)This splitting does not necessarily yield consistency with the
foo.induct
lemma. This should be fine as long as the induction cases are more specialized than the equations, i.e. in each induction case there is one equational lemma that will apply.Simp API
What does it mean to write
simp [f]
?Ideally, the user can understand the semantics of
simp [f]
as iff
was expanded to a list of “normal” theorems, and nothing else behaves differently.This points to a design where
simp [f]
is equivalent tosimp [f.eq_1, f.eq_2,…]
. If the user wants to unfoldf
more aggressively, revealing thematch
es on the RHS, they can writesimp [f.eq_def]
.DSimp interaction
A wrinkle here is that
simp
also callsdsimp
: How shoulddsimp
behave?Note that right now,
simp [some_lemma]
will usesome_lemma
also indsimp
if it happens to be byrfl
. We can use the same rule her as well:simp [f]
issimp [f.eq_1, f.eq_2,…]
and will allowdsimp
to use thosef.eq_n
that are byrfl
.We may want to special case the use of
simp [f.eq_def]
: Here the user is really asking to unfoldf
aggressively, so it seems reasonable to try do that indsimp
as much as possible. So even iff.eq_def
isn’t arfl
-theorem (e.g. due to structural recursion), this should enable the use of “smart unfolding”, so thatdsimp
unfoldsf
whenever it can.Variants
Also splitting
if-then-else
The above is less bold about making the equational theorems fine-grained and consistent with the induction principle, by not splitting
if
-expressions.One could go further and do that. This would bring equational theorems into one-to-one correspondance with the induction principle cases, but it has costs:
Right now
simp [foo]
will use local facts to discharge the side-conditoins of equational lemmas, without using[*]
. By splittingif
expressions, the shape of these side-conditions will not be restricted to those generated by the match compiler due to overlaps, but could be arbitrary conditions. We’d need a way to recognize them reliably (not using the shape) and discharge them.It might break even more code if that code relies on
foo.eq_<n>
being more coarse-grained. In particular if a function has anif
-expression on the rhs the equational theorms may be harder to apply.What I am most worried about: Will the users expect that? And if they have
foo x
, how can they make progress, without manually writingcases_on (copy of condition in definition of foo)
? Note thatsimp [foo.eq_def]
will make progress, but will likely loop. Would we needa
functional split
tactic that looks forfoo x
and then splitsx
as needed?Impact
Add 👍 to issues you consider important. If others benefit from the changes in this proposal being added, please ask them to add 👍 to it.
The text was updated successfully, but these errors were encountered: