Zulip Chat Archive
Stream: new members
Topic: Showing termination with high-order inductive
Jakub Nowak (Dec 01 2025 at 04:20):
Is it possible to show that this terminates in Lean?
inductive ListSchema : Type → Type 1 where
| nil : ListSchema α
| cons (x : α) (xs : ListSchema α) : ListSchema α
| fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β
mutual
def ListSchema.eval_flatten : List (ListSchema α) → List α
| [] => []
| xs :: xss => List.append xs.eval (eval_flatten xss)
def ListSchema.eval : ListSchema α → List α
| nil => []
| cons x xs => x :: xs.eval
| fmap xs f => eval_flatten (xs.eval.map f)
end
-- example
def range : Nat → ListSchema Nat
| 0 => .nil
| n + 1 => .cons n (range n)
def ex : ListSchema Nat := .fmap (.cons 3 (.cons 4 .nil)) range
#eval! ex.eval
Jakub Nowak (Dec 01 2025 at 04:34):
I think, this requires defining size measure such that for x ∈ xs.eval we have sizeOf (f x) < sizeOf (fmap xs f).
Jakub Nowak (Dec 01 2025 at 04:51):
This kinda works, but requires explicitly stating upper bound for fmap:
inductive ListSchema' : Type → Nat → Type 1 where
| nil : ListSchema' α 0
| cons (x : α) (xs : ListSchema' α n) : ListSchema' α n
| fmap (xs : ListSchema' α n) (f : α → ListSchema' β m) : ListSchema' β (n + m + 1)
mutual
def ListSchema'.eval_flatten : List (ListSchema' α n) → List α
| [] => []
| xs :: xss => List.append xs.eval (eval_flatten xss)
def ListSchema'.eval : ListSchema' α n → List α
| nil => []
| cons x xs => x :: xs.eval
| fmap xs f => eval_flatten (xs.eval.map f)
end
structure ListSchema (α : Type) : Type 1 where
n : Nat
val : ListSchema' α n
def ListSchema.nil : ListSchema α where
n := 0
val := .nil
def ListSchema.cons (x : α) (xs : ListSchema α) : ListSchema α where
n := xs.n
val := .cons x xs.val
-- doesn't work
-- is it possible to define this?
def ListSchema.fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β where
n := xs.n + _ + 1
val := .fmap xs.val (fun x => ListSchema.val (f x))
-- example
def range : Nat → ListSchema' Nat 0
| 0 => .nil
| n + 1 => .cons n (range n)
def ex : ListSchema' Nat 1 := .fmap (.cons 3 (.cons 4 .nil)) range
#eval ex.eval
Jakub Nowak (Dec 01 2025 at 04:57):
So I think I need to somehow tell Lean, that f will be called finitely many times, each call can return ListSchema with some value of n. And it's fine, because maximum of finitely many finite numbers is finite.
(That argument also requires some kind of ListSchema' α n → ListSchema' α (n + m) lifting)
Aaron Liu (Dec 01 2025 at 11:19):
I think this doesn't let you do certain infinitely wide trees that were possible before
Jakub Nowak (Dec 01 2025 at 11:29):
Aaron Liu said:
I think this doesn't let you do certain infinitely wide trees that were possible before
I was thinking the same, but now I'm not so sure. Certainly what I defined above is limited, but with additional lifting, like below, I think it's fine? Can you show an example?
inductive ListSchema' : Type → Nat → Type 1 where
| nil : ListSchema' α 0
| cons (x : α) (xs : ListSchema' α n) : ListSchema' α n
| fmap (xs : ListSchema' α n) (f : α → ListSchema' β m) : ListSchema' β (max n m + c + 1)
Jakub Nowak (Dec 01 2025 at 11:31):
I don't think you can create infinite tree with this?
Aaron Liu (Dec 01 2025 at 11:34):
yeah ListSchema can do certain infinite trees that ListSchema' can't do
Mirek Olšák (Dec 01 2025 at 11:34):
Is this what you want?
inductive ListSchema : Type → Type 1 where
| nil : ListSchema α
| cons (x : α) (xs : ListSchema α) : ListSchema α
| fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β
def ListSchema.eval (l : ListSchema α) : List α := match l with
| nil => []
| cons x xs => x :: xs.eval
| fmap xs f => List.flatten (xs.eval.map (fun x ↦ eval (f x)))
-- example
def range : Nat → ListSchema Nat
| 0 => .nil
| n + 1 => .cons n (range n)
def ex : ListSchema Nat := .fmap (.cons 3 (.cons 4 .nil)) range
#eval! ex.eval
Mirek Olšák (Dec 01 2025 at 11:41):
To be honest, I am not sure how termination proving works with mutual recursion. Also, infinite-branching trees are tricky for well-founded recursion because the default sizeOf mechanism can only handle natural numbers, and well founded recursion on infinite-branching trees require ordinals. Fortunatelly, Lean managed to figure out a way how to capture the definition above as a structural recursion.
Mirek Olšák (Dec 01 2025 at 11:43):
It doesn't mean it is impossible, only that there seems to be some missing infrastructure to prove it conveniently.
Jakub Nowak (Dec 01 2025 at 11:56):
Mirek Olšák said:
Is this what you want?
inductive ListSchema : Type → Type 1 where | nil : ListSchema α | cons (x : α) (xs : ListSchema α) : ListSchema α | fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β def ListSchema.eval (l : ListSchema α) : List α := match l with | nil => [] | cons x xs => x :: xs.eval | fmap xs f => List.flatten (xs.eval.map (fun x ↦ eval (f x))) -- example def range : Nat → ListSchema Nat | 0 => .nil | n + 1 => .cons n (range n) def ex : ListSchema Nat := .fmap (.cons 3 (.cons 4 .nil)) range #eval! ex.eval
Indeed, thanks! I wonder, why this works, and my implementation doesn't, they look pretty much the same. Is it possible to somehow print what terminating_by measure did Lean use, or have Lean somehow explain its reasoning in some other way? I'm particulary interested how Lean proved that eval (f x) call is decreasing.
Aaron Liu (Dec 01 2025 at 11:56):
it's structural recursion
Jakub Nowak (Dec 01 2025 at 11:57):
Is it? There's eval (f x), for an arbitrary x, is that structural recursion too?
Aaron Liu (Dec 01 2025 at 11:58):
since f comes from the argument it has to give a structurally smaller value for any x
Jakub Nowak (Dec 01 2025 at 12:00):
If I add
termination_by sizeOf l
then Lean fails to prove that eval (f x) call is decreasing. How can I perform the proof manually?
Aaron Liu (Dec 01 2025 at 12:01):
Jakub Nowak said:
Is it possible to somehow print what
terminating_bymeasure did Lean use, or have Lean somehow explain its reasoning in some other way?
I have two ways
inductive ListSchema : Type → Type 1 where
| nil : ListSchema α
| cons (x : α) (xs : ListSchema α) : ListSchema α
| fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β
/--
info: Try this:
[apply] termination_by structural l
-/
#guard_msgs in
def ListSchema.eval (l : ListSchema α) : List α := match l with
| nil => []
| cons x xs => x :: xs.eval
| fmap xs f => List.flatten (xs.eval.map (fun x ↦ eval (f x)))
termination_by?
or you can print it directly
inductive ListSchema : Type → Type 1 where
| nil : ListSchema α
| cons (x : α) (xs : ListSchema α) : ListSchema α
| fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β
def ListSchema.eval (l : ListSchema α) : List α := match l with
| nil => []
| cons x xs => x :: xs.eval
| fmap xs f => List.flatten (xs.eval.map (fun x ↦ eval (f x)))
/--
info: def ListSchema.eval : {α : Type} → ListSchema α → List α :=
fun {α} l =>
ListSchema.brecOn l fun {α} l f =>
(match (motive := (l : ListSchema α) → ListSchema.below l → List α) l with
| ListSchema.nil => fun x => []
| ListSchema.cons x xs => fun x_1 => x :: x_1.1
| xs.fmap f => fun x => (List.map (fun x_1 => (x.2 x_1).1) x.1.1).flatten)
f
-/
#guard_msgs in
#print ListSchema.eval
Mirek Olšák (Dec 01 2025 at 12:01):
sizeOf is a wrong measure
Aaron Liu (Dec 01 2025 at 12:02):
you can't show it's decreasing with sizeOf
Aaron Liu (Dec 01 2025 at 12:02):
there's not enough natural numbers to fit all the kinds of infinite trees that can be constructed
Jakub Nowak (Dec 01 2025 at 12:03):
I see, the error message that shows with termination_by l says to try sizeOf. I didn't know about structural, maybe the error message should mention it?
Jakub Nowak (Dec 01 2025 at 12:04):
Isn't structural always better choice than sizeOf?
Aaron Liu (Dec 01 2025 at 12:05):
sometimes you have to jump through hoops to make it structural
Aaron Liu (Dec 01 2025 at 12:05):
but generally I think it's usually always better when it works
Mirek Olšák (Dec 01 2025 at 12:05):
No, well founded recursion is in general much stronger
Jakub Nowak said:
Isn't
structuralalways better choice thansizeOf?
Jakub Nowak (Dec 01 2025 at 12:07):
Hm, I guess when you decrease one argument of constructor but increase another than structural fails, but sizeOf could work.
Mirek Olšák (Dec 01 2025 at 12:12):
import Mathlib
inductive ListSchema : Type → Type 1 where
| nil : ListSchema α
| cons (x : α) (xs : ListSchema α) : ListSchema α
| fmap (xs : ListSchema α) (f : α → ListSchema β) : ListSchema β
noncomputable
def ListSchema.ordinalLevel {α : Type} (l : ListSchema α) : Ordinal.{0} := match l with
| nil => 0
| cons _ xs => xs.ordinalLevel + 1
| fmap xs f => max (xs.ordinalLevel + 1) (⨆ a, ordinalLevel (f a) + 1)
theorem ListSchema.cons.ordinalLevel_gt (a : α) (l : ListSchema α) :
l.ordinalLevel < (ListSchema.cons a l).ordinalLevel := by
conv => rhs; unfold ordinalLevel
simp
example (a b : Ordinal.{0}) (h : a < b) : a+1 ≤ b := by exact Order.add_one_le_iff.mpr h
theorem ListSchema.fmap.ordinalLevel_gt1 (xs : ListSchema α) (f : α → ListSchema β) :
xs.ordinalLevel < (ListSchema.fmap xs f).ordinalLevel := by
conv => rhs; unfold ordinalLevel
simp
theorem ListSchema.fmap.ordinalLevel_gt2 (xs : ListSchema α) (f : α → ListSchema β) (a : α) :
(f a).ordinalLevel < (ListSchema.fmap xs f).ordinalLevel := by
conv => rhs; unfold ordinalLevel
apply Order.add_one_le_iff.mp
apply le_sup_of_le_right
exact Ordinal.le_iSup ..
def ListSchema.eval (l : ListSchema α) : List α := match l with
| nil => []
| cons x xs => x :: xs.eval
| fmap xs f => List.flatten (xs.eval.map (fun x ↦ eval (f x)))
termination_by l.ordinalLevel
decreasing_by
· apply ListSchema.cons.ordinalLevel_gt
· apply ListSchema.fmap.ordinalLevel_gt2
· apply ListSchema.fmap.ordinalLevel_gt1
Mirek Olšák (Dec 01 2025 at 12:15):
Afaik, Lean doesn't generate ordinalLevel automatically like sizeOf. I am not sure if there is any infrastructure for it, so I did it manually.
Jakub Nowak (Dec 01 2025 at 12:15):
Thanks! That looks very useful.
Aaron Liu (Dec 01 2025 at 12:15):
you also have to import ordinals
Aaron Liu (Dec 01 2025 at 12:16):
unlike Nat they don't come with Lean
Mirek Olšák (Dec 01 2025 at 12:18):
The minimal import is
import Mathlib.SetTheory.Ordinal.Family
of course importing whole Mathlib works too.
Mirek Olšák (Dec 01 2025 at 12:20):
The idea is that I defined ListSchema.ordinalLevel using structural recursion, and since ordinals have a canonical WellFoundedRelation, it is later possible to use it for well-founded recursion.
Mirek Olšák (Dec 01 2025 at 12:21):
Under the hood, sizeOf does the same idea, only that the measure is done by natural numbers, so it is not able to measure infinite trees.
Last updated: Dec 20 2025 at 21:32 UTC