Zulip Chat Archive
Stream: lean4
Topic: nested inductives in the kernel
Joachim Breitner (Jun 30 2024 at 15:47):
(This is mostly an inner monologue, I mostly answer my own questions as I go.)
I was, for some reason, under the impression that the Lean4 kernel has support for mutual inductives, but not nested inductive, and that the fronend elaborates nested inductives such as
inductive Tree where
| node : List Tree → Tree
to mutual inductives such as
mutual
inductive Tree where
| node : ListTree → Tree
inductive ListTree where
| nil : ListTree
| cons : Tree → ListTree → ListTree
end
so that the kernel sees the latter only.
But if I do the nested induction and look at the type of the recursor, I get
recursor Tree.rec.{u} : {motive_1 : Tree → Sort u} →
{motive_2 : List Tree → Sort u} →
((a : List Tree) → motive_2 a → motive_1 (Tree.node a)) →
motive_2 [] →
((head : Tree) → (tail : List Tree) → motive_1 head → motive_2 tail → motive_2 (head :: tail)) →
(t : Tree) → motive_1 t
and it seems that this is is indeed “primitive” (it’s a recursor
, not a def
), and that thus the kernel somehow was able to make sense of List Tree
without making an artificial copy of the LIst
data type.
I tried to consult “The Type Theory of Lean”, but (maybe because that is about lean3?) it says that even mutual induction is not supported.
Looking at the kernel code I see
/* Eliminate nested inductive datatypes by creating a new (auxiliary) declaration which contains and inductive types in `d`
and copies of the nested inductive datatypes used in `d`. For each nested occurrence `I Ds is` where `I` is a nested inductive
datatype and `Ds` are the parametric arguments and `is` the indices, we create an auxiliary type `Iaux` in the (mutual) inductive
declaration `d`, and replace `I Ds is` with `Iaux As is` where `As` are `d`'s parameters.
Moreover, we add the pair `(I Ds, Iaux)` to `nested_aux`.
Note that, `As` and `Ds` may have a different sizes. */
struct elim_nested_inductive_fn {
so maybe this means that there is this copying-construction, but it happens purely in the kernel, in a way that the recursor still looks “primitive” to the outside (as opposed to be derived from a primitive constructor that mentiones mentions the copy of List
).
Ah, looking some more, it seems that within the kernel, the copied data constructors are used, but then the resulting .rec
values and constructors are post-processed and in their types (and rules) the auxillary data types are written back to List Tree
instead (using res.restore_nested
), and thus outside the kernel these are never seen.
That answers my questions; maybe someone else finds this an interesting investigation.
Jason Rute (Jun 30 2024 at 16:14):
Does @Sebastian Ullrich‘s dissertation clarify this at all? My understanding was that his dissertation is the Lean 4 update to Mario’s “The Type Theory of Lean” (among other things).
Henrik Böving (Jun 30 2024 at 16:18):
The part right before 3.3 does explain that this encoding happens in the kernel yes.
Joachim Breitner (Jun 30 2024 at 17:18):
Ok, follow up thought.
It feels somehow “wrong” that although the Tree
type above uses the List
type former in the types, and the List
constructors in recursor, it doesn’t connect to List.rec
. Without hard evidence yet I am curious if, if it would, we might have an easier time defining structurally recursive nested functions.
More concretely, right now you get this reduction rule for the Tree.rec
recursor:
inductive Tree where
| node : List Tree → Tree
noncomputable def Tree.rec_1'.{u}
{motive_1 : Tree → Sort u} {motive_2 : List Tree → Sort u}
(node : (a : List Tree) → motive_2 a → motive_1 (Tree.node a)) (nil : motive_2 [])
(cons : (head : Tree) → (tail : List Tree) → motive_1 head → motive_2 tail → motive_2 (head :: tail))
(ts : List Tree) : motive_2 ts
:=
@List.rec Tree _ nil
(fun head tail ih_tail => cons head tail (@Tree.rec motive_1 motive_2 node nil cons head) ih_tail)
ts
theorem Tree.rec_1_eq_rec1'.{u}
{motive_1 : Tree → Sort u} {motive_2 : List Tree → Sort u}
(node : (a : List Tree) → motive_2 a → motive_1 (Tree.node a)) (nil : motive_2 [])
(cons : (head : Tree) → (tail : List Tree) → motive_1 head → motive_2 tail → motive_2 (head :: tail))
(ts : List Tree) :
@Tree.rec_1 motive_1 motive_2 node nil cons ts = @Tree.rec_1' motive_1 motive_2 node nil cons ts := by
induction ts
next => rfl
next x xs ih => dsimp; congr
theorem Tree.rec_rule' {motive_1 : Tree → Sort u} {motive_2 : List Tree → Sort u}
(node : (a : List Tree) → motive_2 a → motive_1 (Tree.node a)) (nil : motive_2 [])
(cons : (head : Tree) → (tail : List Tree) → motive_1 head → motive_2 tail → motive_2 (head :: tail))
(ts : List Tree) :
@Tree.rec motive_1 motive_2 node nil cons (.node ts) =
node ts (@Tree.rec_1' motive_1 motive_2 node nil cons ts) := by dsimp; rw [Tree.rec_1_eq_rec1']
(This on its won probably doesn’t suffice to be able to do nested recursion through, say, List.map
.)
Last updated: May 02 2025 at 03:31 UTC