Zulip Chat Archive
Stream: general
Topic: When is it acceptable to use tactics outside of theorems?
Niels Voss (Jun 14 2025 at 22:55):
I'm thinking about adding a section to #general > Documenting lean pitfalls: feedback requested about using tactics outside of theorem. Can someone remind me if it is considered acceptable to:
- Use tactics to produce terms of arbitrary types, when constraining the list of tactics to "simple" tactics like
apply,have, andinfer_instance? - Use tactics to produce terms of non-definitional subsingletons, such as
Decidable,Fintype, andSquash/Trunc? - Use tactics to produce noncomputable terms of non-definitional subsingletons?
- Use tactics to produce
terms of typeproofs of terms of typePropProp, but inside of adefinstead of atheorem? These aren't particularly common, but they do exist, likeAcc.inv. - Use tactics inside a
defto construct something that is part data, part proof (e.g. an element of a subtype)?
suhr (Jun 14 2025 at 23:01):
But why would you prefer using tactics over terms in the first place?
Kenny Lau (Jun 14 2025 at 23:02):
I personally use refine where I provide all the data, and then use ?_ for any argument that is a proof. For a minimised example:
def Nat.Itself (n : Nat) : Type :=
{ m // m = n }
def me (n : Nat) : n.Itself := by
refine ⟨n, ?_⟩
· -- insert very long proof
rw [← Nat.add_zero n, ← Nat.add_comm]
Other people are probably in better positions than me to answer your questions, but I will try nevertheless (feel free to treat these as just my opinion and wait for another answer):
apply,exact, andrefineare fine, and other tactics require more caution.it is fine for(edit: see following discussion)DecidableandFintypebecause terms of those types are never unfolded. In other words, those terms are "irrelevant" (in the technical sense like "proof irrelevance")- whether it's noncomputable or not isn't the issue, the issue is whether those definitions will ever be unfolded and reasoned about.
- I'm not sure how you can produce a term of type
Propinside a theorem in the first place. But I would argue that one should still keep tactics to a minimum just like defining any other term of any data types. - see my example above.
Eric Wieser (Jun 14 2025 at 23:03):
Kenny Lau said:
- it is fine for
DecidableandFintypebecause terms of those types are never unfolded. In other words, those terms are "irrelevant" (in the technical sense like "proof irrelevance")
unfolding these instances is precisely how decide works
Niels Voss (Jun 14 2025 at 23:03):
suhr said:
But why would you prefer using tactics over terms in the first place?
Well #5 is done all the time in mathlib, but for the other ones, I'm not completely sure. A common pattern in mathlib (maybe this isn't the case anymore) is to use by classical; exact ... when defining something
Eric Wieser (Jun 14 2025 at 23:03):
Niels Voss said:
- Use tactics to produce terms of type
Prop,
Do you really mean a term ?P : Prop, and not a term ?h : P where P : Prop?
Kenny Lau (Jun 14 2025 at 23:04):
Eric Wieser said:
Kenny Lau said:
- it is fine for
DecidableandFintypebecause terms of those types are never unfolded. In other words, those terms are "irrelevant" (in the technical sense like "proof irrelevance")unfolding these instances is precisely how
decideworks
Does decide fail if there are complicated terms using Eq.mpr (aka the triangle)?
Niels Voss (Jun 14 2025 at 23:04):
Eric Wieser said:
Niels Voss said:
- Use tactics to produce terms of type
Prop,Do you really mean a term
?P : Prop, and not a term?h : PwhereP : Prop?
Oops, you're right, I meant proofs of propositions
Kenny Lau (Jun 14 2025 at 23:04):
proofs of prositions shouldn't be def, right?
Kenny Lau (Jun 14 2025 at 23:05):
Niels Voss said:
suhr said:
But why would you prefer using tactics over terms in the first place?
Well #5 is done all the time in mathlib, but for the other ones, I'm not completely sure. A common pattern in mathlib (maybe this isn't the case anymore) is to use
by classical; exact ...when defining something
I think I personally have been doing open Classical in for this case
Eric Wieser (Jun 14 2025 at 23:05):
@Kenny Lau, that's certainly the fear. Either way, "are never unfolded" is definitely wrong in your statement, so I encourage you to replace it with a new one about "Eq.mpr doesn't actually matter" if you can convince yourself this is true.
Eric Wieser (Jun 14 2025 at 23:07):
Kenny Lau said:
I personally use
refinewhere I provide all the data, and then use?_for any argument that is a proof. For a minimised example:
This is certainly safe, though in my opinion it's not great style. Mainly this style is compelling when the use is actually refine ⟨n, ?_, ?_, ?_, ?_, ?_⟩ <;> simp or similar.
Niels Voss (Jun 14 2025 at 23:07):
Kenny Lau said:
proofs of prositions shouldn't be
def, right?
They need to be defs in certain rare situations, because theorem is kind of like opaque and prevents you from unfolding them.
Niels Voss (Jun 14 2025 at 23:08):
Eric Wieser said:
Kenny Lau said:
I personally use
refinewhere I provide all the data, and then use?_for any argument that is a proof. For a minimised example:This is certainly safe, though in my opinion it's not great style. Mainly this style is compelling when the use is actually
refine ⟨n, ?_, ?_, ?_, ?_, ?_⟩ <;> simpor similar.
Is the actual term produced by refine considered part of refine's public interface, or does the public interface only include what subgoals goals it generates?
Kenny Lau (Jun 14 2025 at 23:08):
Eric Wieser said:
Kenny Lau, that's certainly the fear. Either way, "are never unfolded" is definitely wrong in your statement, so I encourage you to replace it with a new one about "Eq.mpr doesn't actually matter" if you can convince yourself this is true.
I have strucken out \#2
Eric Wieser (Jun 14 2025 at 23:10):
Niels Voss said:
They need to be
defs in certain rare situations, becausetheoremis kind of likeopaqueand prevents you from unfolding them.
I wonder with the recent changes to well-founded recursion if this is actually still something that matters.
Eric Wieser (Jun 14 2025 at 23:11):
Niels Voss said:
- Use tactics to produce noncomputable terms of non-definitional subsingletons?
This used to be very common as by classical exact term, but an alternate spelling is open scoped classical in term which avoids tactics.
Robert Maxton (Jun 15 2025 at 23:07):
Personally, my rule of thumb is that you don't want anyone to have to see a 'bare' cast, i.e. ▸ or _root_.cast or an Eq.mp/Eq.mpr, because when you generate such with tactics you are almost always throwing away important type information without which the expanded definition cannot be reduced in the wild.
I'll use tactics primarily to help me manage elaboration order and the like, so that I have to type out fewer explicit type annotations.
Also, anyone checking this thread should be aware of the by? command, which works like by but then gives you a Try this with an attempt at producing a term equivalent to your tactic. Best used with set_option pp.proofs true so that it doesn't throw away proofs necessary to produce your term, and it doesn't do the same elaboration-order-manipulation that the corresponding tactics do so it doesn't always work, but it's a good thing to try before leaving a tactic-created-term in your final result.
Thomas Zhu (Jun 17 2025 at 03:41):
I think this isn't mentioned yet: it should be acceptable to use specific tactics to defer evaluation (usually for default parameters), such as in by exact decl_name%, by volume_tac.
Mario Carneiro (Jun 19 2025 at 01:19):
another reason to use tactics in definitions is simply for formatting reasons; I find it easier to lay out an expression with nesting in weird places using a sequence of calls to refine instead of working out how to do the indentation for the term mode "proof". But I would restrict such usage to things like refine, apply, exact and constructor, and avoid rw, simp or any other high power tactics, at least until you have reduced the goal to something in a Prop.
Mario Carneiro (Jun 19 2025 at 01:25):
I would say that Kenny's example is a case of this. You could certainly write ⟨n, by ...⟩ but the hanging ⟩ is just much more awkward to format if the ... is a long proof
Yaël Dillies (Jun 19 2025 at 06:52):
Mario's point above is strong. I myself have started to push people towards the following style:
def foo : Bar := by
refine ⟨data1, data2, ?proof1, ?proof2⟩
· proof1
· proof2
Eric Wieser (Jun 19 2025 at 11:41):
I'd probably prefer .mk n <| by for Kenny's example
Eric Wieser (Jun 19 2025 at 11:41):
Or where notation, which also solves the trailing } issue
Yaël Dillies (Jun 19 2025 at 11:44):
Sorry, I meant to give an example with a non-standard constructor:
def foo : Bar := by
refine .nonStandardMk data1 data2 ?proof1 ?proof2
· proof1
· proof2
Yaël Dillies (Jun 19 2025 at 11:45):
... the point being that where notation only applies to the standard constructor
Matthew Ballard (Jun 19 2025 at 12:09):
I thought Eric meant
def Nat.Itself (n : Nat) : Type :=
{ m // m = n }
def me (n : Nat) : n.Itself :=
⟨n, prf⟩
where prf := by -- insert very long proof
rw [← Nat.add_zero n, ← Nat.add_comm]
Kenny Lau (Jun 19 2025 at 12:27):
wait, u can do that?
Weiyi Wang (Jun 19 2025 at 13:10):
This is where I need a list of "lean syntax you probably don't know yet"
Eric Wieser (Jun 19 2025 at 14:54):
I did not mean that, but I should have
Eric Wieser (Jun 19 2025 at 14:54):
Though I think that makes prf a def not a theorem, which flags the linter?
Jz Pan (Jun 19 2025 at 15:15):
Matthew Ballard said:
I thought Eric meant
def Nat.Itself (n : Nat) : Type := { m // m = n } def me (n : Nat) : n.Itself := ⟨n, prf⟩ where prf := by -- insert very long proof rw [← Nat.add_zero n, ← Nat.add_comm]
I think this will introduce a me.prf in the global context, which is not good I think.
Kyle Miller (Jun 19 2025 at 15:16):
I've thought that named arguments were a reasonable compromise.
def foo : Bar :=
.nonStandardMk data1 data2
(commociative := by
tac1
tac2)
(ramitated := by
tac1
tac2)
It's not as sleek as where for structures, but to me it seems more structured than using refine.
Matthew Ballard (Jun 19 2025 at 16:06):
Eric Wieser said:
Though I think that makes
prfa def not a theorem, which flags the linter?
whatsnew reports theorem me.prf
Matthew Ballard (Jun 19 2025 at 16:07):
Jz Pan said:
I think this will introduce a
me.prfin the global context, which is not good I think.
Actually this might help avoid unfolding the proofs.
suhr (Jun 19 2025 at 16:08):
Kenny Lau said:
wait, u can do that?
Yes, but where has limitations compared to refine.
Niels Voss (Jun 19 2025 at 17:07):
Given all the discussion above, is there a consensus on whether:
- The terms generated by
exact,refine,construct,intro,let, andapplyis to be considered part of the tactic's public interface? - The terms generated by advanced tactics like
rw,simp, andaesopare considered part of the tactic's public interface?
It seems to me that the answer to 1. is yes and 2. is no, but where do we draw the line between "simple" and "complex" tactics?
Robin Arnez (Jun 19 2025 at 18:02):
I think it's just that some tactics are "term" tactics:
exactandrefineliterally produce exactly what you give it with maybe some subgoals (emulatingapplyis basically function applicationconstructoris also function application or, well, structure instance notationintrois basically lambda abstractionletandhaveare, well,letandhaveby_casesis if-then-elsematchis amatchexpressioninductionis recursor applicationinfer_instanceisinferInstancesorryissorry
Other tactics are more trying to do something specific to goal (e.g. solve it or rewrite something) instead of being tied to a term and should therefore be avoided for definitions.
Kyle Miller (Jun 19 2025 at 18:20):
I worry about saying that any tactic has any guarantee about what terms they produce.
For example, intro also has a feature to do pattern matching. It's currently implemented with a match term, but is that in the interface? What if intro gains rfl patterns? Then that means intro can effectively wrap terms in rewrites, which is one of the main goals of avoiding rw and simp.
The induction tactic does make use of the recursor, but maybe one day it will have some capacity for dependent elimination like cases, which people periodically ask for? That introduces rewrites. Furthermore, using induction means your definition won't have equation lemmas for each induction case.
The by_cases is if-then-else, but it's free to pull in Classical. Even the if _ then _ else _ tactic will do this.
That all said, it's hard to imagine that exact/refine/apply will ever do anything except use the term you provide it. (I've been kicking around an idea to make apply t be something like refine apply% t, so that it's even more nothing out of the ordinary.)
I'm focusing on rewrites here because those are what tend to make tactic-defined definitions fail to have good definitional equality properties.
Last updated: Dec 20 2025 at 21:32 UTC