Zulip Chat Archive
Stream: new members
Topic: Tactic hierarchies in Lean/Mathlib
Isak Colboubrani (Jan 30 2024 at 21:44):
For educational purposes, I am exploring the relationships between various tactics in Lean/Mathlib, particularly looking at situations where certain tactics can be considered as encompassing the functionalities of others. This involves identifying scenarios where one tactic may act as a "superset" in terms of its capabilities relative to another tactic.
A trivial yet possibly far-fetched example to illustrate this point (assuming my understanding of these tactics is correct):
If a single remaining goal can be successfully closed by employing any tactic from the following list, then it's guaranteed that all tactics listed above it can also close the goal:
aesop
(Mathlib)- …
simp_all
(Lean)- …
simp
(Lean)- …
rfl
(Lean)
Put differently, if a comprehensive tactic like aesop
, considered the most encompassing in this hierarchy, fails to resolve a single remaining goal, it's unnecessary to attempt using the tactics that rank lower in this list (to close the goal in one go). (This is based on the understanding that aesop
incorporates strategies equivalent to simp_all
, among other more sophisticated methods of course.)
Is this correct?
Are there other similar tactic hierarchies?
Bolton Bailey (Jan 30 2024 at 23:21):
Interesting initiative. Another tactic hierarchy I might think of for this would involve apply
/convert
and maybe some other tactics, though those tactics take arguments.
Damiano Testa (Jan 30 2024 at 23:48):
A small correction: simp
does not "imply" rfl
.
def myZero : Nat := 0
-- fails
example : myZero = 0 := by simp
-- works
example : myZero = 0 := by rfl
Damiano Testa (Jan 31 2024 at 01:26):
In fact, you can also trick simp_all
into not working, when simp
does work:
import Mathlib.Data.Nat.Basic
import Mathlib.Algebra.GroupWithZero.Basic
@[simp]
theorem ohNo (m n : Nat) : m = n ↔ 2 * m = 2 * n := by
simp only [mul_eq_mul_left_iff, Nat.succ_ne_zero, or_false]
example (n : Nat) (h : n = 2 * n) : True := by
simp_all -- times out, while `simp` works.
Although this seems a little contrived and #lint
complains that ohNo
is not great as a simp
lemma!
Isak Colboubrani (Jan 31 2024 at 08:32):
Thanks!
It might be more accurate to represent the relationships between these tactics using a tree structure, where each parent node represents a tactic that is more comprehensive or powerful (in the sense outlined above) than its child nodes (descendants).
In this structure:
aesop
├─ simp_all
├─ trivial
├─ apply True.intro
├─ apply And.intro <;> trivial
├─ assumption
├─ contradiction
├─ decide
├─ rfl
├─ eq_refl
├─ simp
Is this representation more accurate, or are there counterexamples that challenge it?
Yaël Dillies (Jan 31 2024 at 08:48):
Again, there are some cases that aesop
can't do which rfl
can
Damiano Testa (Jan 31 2024 at 08:54):
While I agree that for pedagogical purposes this is a good place to start, in any "real-word" situation, unless a tactic is essentially defined as "try one tactic first and then do something else" it is almost never true that one tactic is strictly more powerful than another.
Martin Dvořák (Jan 31 2024 at 12:10):
I am always a bit surprised when rfl
closes a goal that simp
cannot.
Isak Colboubrani (Jan 31 2024 at 21:02):
Yaël Dillies said:
Again, there are some cases that
aesop
can't do whichrfl
can
This is quite unexpected to me (possibly due to my novice status in this field!). Would anyone be able to illustrate such a case, especially one a student might realistically encounter?"
Damiano Testa (Jan 31 2024 at 21:08):
Here is a simple example:
import Mathlib.Tactic
@[irreducible]
def m : Nat := 0
example : m = 0 := by aesop -- fails
example : m = 0 := by rfl -- works
Damiano Testa (Jan 31 2024 at 21:15):
Here is a small tour over the transparencies:
import Mathlib.Tactic
def noSimp : Nat := 0
example : noSimp = 0 := by aesop
example : noSimp = 0 := by simp -- fails
example : noSimp = 0 := by rfl
@[irreducible] def onlyRfl : Nat := 0
example : onlyRfl = 0 := by aesop -- fails
example : onlyRfl = 0 := by simp -- fails
example : onlyRfl = 0 := by rfl
abbrev allWork : Nat := 0
example : allWork = 0 := by aesop
example : allWork = 0 := by simp
example : allWork = 0 := by rfl
Isak Colboubrani (Jan 31 2024 at 21:30):
@Damiano Testa Thank you! Your examples have been enlightening, and they've guided me towards further reading.
Based on what I've learned today, here's my revised representation of the "tactic hierarchy" introduced above:
aesop
├─ trivial
├─ apply True.intro
├─ apply And.intro <;> trivial
├─ assumption
├─ contradiction
├─ decide
simp
simp_all
rfl
├─ eq_refl
Is this structure accurate, or are there still counterexamples that could potentially challenge this arrangement?
Damiano Testa (Jan 31 2024 at 21:36):
I do not think that aesop
uses decide
:
import Mathlib.Tactic
example : ∀ n, n ≤ 0 → Nat.divisors n ≠ {n} := by
--aesop -- fails to close goal
decide -- works, as does `trivial`
Damiano Testa (Jan 31 2024 at 21:42):
Maybe I can say here something that I tell my students.
Having a rough mental picture of a partial ordering of the tactics is certainly very useful and also close to how my intuition works. However, "proving theorems" is undecidable, so no algorithm can consistently close all goals. And in particular, no algorithm can do everything that all algorithms can do. So, pushing too far this partial order is not too useful and I would even say it is undesirable: having many different algorithms, means having more theorems they will prove collectively!
So, having tactics that do not supersede each other is a goal to try to achieve, in my opinion!
Damiano Testa (Jan 31 2024 at 22:54):
Here is another example showing that you can "trick" even the tactics that are defined in terms of each other from really being "stronger":
import Mathlib.Tactic
example (h : ∀ n, n ≤ 160 → ¬ 161 ∣ n) : ∀ n, n ≤ 160 → ¬ 161 ∣ n := by
--trivial -- max recursion
--decide -- max recursion
assumption -- works
Damiano Testa (Jan 31 2024 at 23:00):
In fact, in this last example, if you reduce the imports to just import Mathlib.Data.Nat.Basic
, then trivial
works!
I'll leave you to figure out why this is the case! :wink:
Yaël Dillies (Jan 31 2024 at 23:12):
I think it's much better to emphasize what kinds of goals each tactic can close
Last updated: May 02 2025 at 03:31 UTC