Zulip Chat Archive
Stream: lean4
Topic: Help with termination_by
Marcus Rossel (Jul 04 2022 at 12:31):
I have the following type of Term
s:
inductive Term
| bool (b : Bool)
| nat (n : Nat)
| ite (cond pos neg : Term)
I want to define and evaluation function on Term
s in two steps.
First, I define a single evaluation step (which fails if we try to evaluate an ite
with a number as condition):
def eval₁ (t : Term) : Option Term :=
match t with
| bool _ => t
| nat _ => t
| ite cond pos neg =>
match cond with
| .bool .true => pos
| .bool .false => neg
| _ =>
match eval₁ cond with
| some cond' => ite cond' pos neg
| none => none
Second, I define a full evaluation by recursion on the single-step evaluation function:
def eval (t : Term) : Option Value :=
match h : t.eval₁ with
| none => none
| some (.bool b) => some (.bool b)
| some (.nat n) => some (.nat n)
| some t'@(.ite ..) => t'.eval
For the eval
function, Lean can't prove termination by itself.
I know that termination_by
exists, but how could I use that (and possible decreasing_by
) to show termination here?
Does it somehow involve SizeOf
?
Henrik Böving (Jul 04 2022 at 12:47):
You would use termination_by eval t => t
in order to indicate to Lean that you wish to proof termination by showing that t
's size decreases every step, it will then tell you that it is missing sizeOf t' < sizeOf t
in the ite case which you can give it by doing:
def eval (t : Term) : Option Value :=
match h : t.eval₁ with
| none => none
| some (.bool b) => some (.bool b)
| some (.nat n) => some (.nat n)
| some t'@(.ite ..) =>
have : sizeOf t' < sizeOf t := by sorry
t'.eval
termination_by eval t => t
Henrik Böving (Jul 04 2022 at 12:49):
However you will fail to perform this proof since your function does not terminate, consider:
#eval eval (ite (nat 12) (nat 13) (nat 14))
which will endlessly loop. This will become evident in the proof when you arrive in the case where your input expression was a ite
and the condition was a nat
, after fully simplifying it presents you with False
since you'd have to proof that sizeOf t < sizeOf t
in this special case.
Marcus Rossel (Jul 04 2022 at 12:52):
Henrik Böving said:
However you will fail to perform this proof since your function does not terminate, consider:
#eval eval (ite (nat 12) (nat 13) (nat 14))
which will endlessly loop. This will become evident in the proof when you arrive in the case where your input expression was a
ite
and the condition was anat
, after fully simplifying it presents you withFalse
since you'd have to proof thatsizeOf t < sizeOf t
in this special case.
Oh thanks! I was struggling with the proof right now :see_no_evil:
Henrik Böving (Jul 04 2022 at 12:55):
You could go full C style here and say that a nat condition evals to true if it is not equal to 0 to avoid this^^
Marcus Rossel (Jul 04 2022 at 12:55):
Henrik Böving said:
You could go full C style here and say that a nat condition evals to true if it is not equal to 0 to avoid this^^
Haha, I've decided to go with this instead:
def eval₁ (t : Term) : Option Term :=
match t with
| bool _ => t
| nat _ => t
| ite cond pos neg =>
match cond with
| .bool .true => pos
| .bool .false => neg
| .nat _ => none
| .ite .. =>
match eval₁ cond with
| some cond' => ite cond' pos neg
| none => none
Matthew Ballard (Jun 22 2023 at 17:03):
I am confused by the behavior in this example. Any help would be appreciated.
class IsWellFounded (α : Type _) (r : α → α → Prop) where
variable {α : Type _} (r r' : α → α → Prop)
theorem wf [IsWellFounded α r'] : WellFounded r := sorry
def foo (a : α) : α := sorry
variable [IsWellFounded α r']
def bar (a : α) : α :=
have : r (foo a) a := sorry
bar (foo a)
termination_by' ⟨r,wf r r'⟩ -- unknown identifier 'r''
#check r' -- α → α → Prop
Kevin Buzzard (Jun 22 2023 at 17:09):
def bar (a : α) : α :=
have : IsWellFounded α r' := inferInstance -- random extra line
have : r (foo a) a := sorry
bar (foo a)
termination_by' ⟨r,wf r r'⟩ -- now works
My guess is that in contrast to the usual situation people complain about with variables (they're dragged in when they're not supposed to be), by the time Lean is processing termination_by'
it has decided that r'
is definitely not used in the declaration so has removed it from the context?
Adam Topaz (Jun 22 2023 at 17:11):
Right, it seems that lean chucks out everything not involved in the body of the definition before the ternimation_by'.
Matthew Ballard (Jun 22 2023 at 17:12):
Thanks. That makes sense. It is even more annoying when the instance has no explicit parameters.
Matthew Ballard (Jun 22 2023 at 17:12):
class Blah (α : Type _) where
variable {α : Type _} (r r' : α → α → Prop)
theorem wf [Blah α] : WellFounded r := sorry
def foo (a : α) : α := sorry
variable [Blah α]
def bar (a : α) : α :=
-- have : Blah α := inferInstance
have : r (foo a) a := sorry
bar (foo a)
termination_by' ⟨r,wf r⟩ -- failed to synthesize instance Blah α
Adam Topaz (Jun 22 2023 at 17:13):
again, the instance is not part of the def
Adam Topaz (Jun 22 2023 at 17:13):
so it's tossed
Matthew Ballard (Jun 22 2023 at 17:14):
Yes, right
Matthew Ballard (Jun 22 2023 at 17:14):
Let me see if this "fixes" my original problem
Matthew Ballard (Jun 22 2023 at 17:15):
Yes it does
Matthew Ballard (Jun 22 2023 at 17:23):
One more question: is termination_by'
the idiomatic way to use a custom WellFoundedRelation
for termination?
Gabriel Ebner (Jun 23 2023 at 02:24):
I'd say the most idiomatic way is to add a type alias MyAlias
for your relation together with a global WellFoundedRelation MyAlias
instance. Then you can do termination_by _ => MyAlias.mk ...
to do recursion on that well-founded relation.
Gabriel Ebner (Jun 23 2023 at 02:25):
In many cases, you can also just write the termination measure directly instead of defining a custom relation. You can even use Ordinal
after termination_by _ =>
!
François G. Dorais (Jun 23 2023 at 02:42):
Do you mean WellFoundedRelation MyAlias
? I don't think WellFounded
is a class, at least not in core.
François G. Dorais (Jun 23 2023 at 02:43):
How does Ordinal
work exactly?
François G. Dorais (Jun 23 2023 at 02:47):
@Gabriel Ebner Sorry, forgot to mention you above. ^
Gabriel Ebner (Jun 23 2023 at 03:18):
Sorry, I'm always getting confused by the naming.
François G. Dorais (Jun 23 2023 at 03:23):
@Gabriel Ebner No worries, I also regularly get confused in the same way.
Gabriel Ebner (Jun 23 2023 at 03:24):
Ordinal
is "just" a very big well-order. Every well-founded relation on a type in Type u
maps into Ordinal.{u}
in an order-preserving way. So you could always replace your favorite well-founded relation r : α → α → Prop
by a function f : α → Ordinal
.
Gabriel Ebner (Jun 23 2023 at 03:25):
The point that I'm trying to get across is that termination_by
doesn't restrict you to natural numbers, if you can use well-founded recursion then you can express it with termination_by _ => someFunction a
. And at least personally, I find termination measures much easier to read than well-founded relations that are constructed using a custom bespoke API.
François G. Dorais (Jun 23 2023 at 03:41):
@Gabriel Ebner Yes, I see. I tried to do that on a project of mine yesterday but I could only make termination_by'
work (with a lot of additional work explaining what that wellfounded relation was and why). It looks like I was missing some WellFoundedRelation
instances to make termination_by
work? I think I'm just a bit confused how I should be using this termination API.
Gabriel Ebner (Jun 23 2023 at 03:45):
The most common way is to just write termination_by _ => value that gets smaller in recursive calls
. For a lot of stuff this value can simply be a natural number. You can also put a pair there, then you get the lexicographic order.
François G. Dorais (Jun 23 2023 at 03:46):
But how is the relation inferred? In my use case, the default "SizeOf" is irrelevant.
Gabriel Ebner (Jun 23 2023 at 03:48):
We didn't have termination_by
in Lean 3, so you won't find many examples in mathlib. But from what I can tell, most uses of termination_by'
would be much better served by termination_by
. Compare:
termination_by' invImage PSigma.fst ⟨_, degree_lt_wf⟩
-- vs.
termination_by _ => R.degree
or
termination_by' PSigma.lex (inferInstance) (fun _ ↦ inferInstance)
-- vs.
termination_by _ => (a, b, c)
Gabriel Ebner (Jun 23 2023 at 03:49):
François G. Dorais said:
But how is the relation inferred? In my use case, the default "SizeOf" is irrelevant.
The relation is inferred using the WellFoundedRelation (type of the termination_by value)
type class. For many types (natural numbers, pairs, ordinals) this has a sensible value. SizeOf is only used as a low-priority fallback instance.
François G. Dorais (Jun 23 2023 at 03:51):
Here is a MWE:
structure Forest (α : Type) [DecidableEq α] where
pred : α → Option α
wf : WellFounded fun x y => pred y = some x
namespace Forest
variable {α} [DecidableEq α] (forest : Forest α)
instance relWF : WellFoundedRelation α where
rel x y := forest.pred y = some x
wf := forest.wf
def rank (x : α) : Nat :=
match _hint : forest.pred x with
| some y => rank y + 1
| none => 0
termination_by' forest.relWF
How can I use termination_by
here?
Gabriel Ebner (Jun 23 2023 at 03:59):
You can use a type alias:
structure Forest (α : Type) where
pred : α → Option α
wf : WellFounded fun x y => pred y = some x
def Forest.Node (_forest : Forest α) :=
α
instance {forest : Forest α} : WellFoundedRelation forest.Node where
rel x y := forest.pred y = some x
wf := forest.wf
def Forest.rank (forest : Forest α) (x : α) : Nat :=
match _hint : forest.pred x with
| some y => forest.rank y + 1
| none => 0
termination_by _ => (id x : forest.Node)
Gabriel Ebner (Jun 23 2023 at 04:02):
You might also want to look at the WellFounded.rank
function.
François G. Dorais (Jun 23 2023 at 04:17):
Thanks for the wonderful hints!
François G. Dorais (Jun 23 2023 at 04:19):
I also realized that I have been overusing abbrev
!
This is all very enlightening!
Simon Hudon (Jul 09 2023 at 00:35):
I found this thread very useful, thank you @Gabriel Ebner and @François G. Dorais!
I thought I'd add a note in case it's useful for others: it also works with mutually recursive functions on mutually inductive types. In that case the abbrev
should be a _ ⊕ _
type and I have found it useful to define new cases for the decreasing_trivial
tactic so that I don't have to write a decreasing_by
clause for each mutually recursive function
My new cases look like
local macro_rules | `(tactic| decreasing_trivial) => `(tactic| apply StructM.branch_tl)
StructM
is an inductive relation on the _ ⊕ _
type and branch_tl
is one of the constructors.
Last updated: Dec 20 2023 at 11:08 UTC