Zulip Chat Archive
Stream: lean4
Topic: Nested inductives - Option in custom type
Wrenna Robson (Nov 15 2024 at 17:02):
The following (which I think is a MWE) doesn't work:
inductive BTree (β : Type u) : Nat → Type u where
| leaf : (n : ℕ) → β → BTree β n
| node : {n : ℕ} → Option (BTree β n) → Option (BTree β n) → BTree β (n + 1)
I get this message:
(kernel) invalid nested inductive datatype 'Option',
nested inductive datatypes parameters
cannot contain local variables.
This seems reasonable enough: is there a way of modifying my definition to achieve this?
Wrenna Robson (Nov 15 2024 at 17:16):
Oho: this seems to work:
inductive BTree (β : Type u) : (n : Nat) → Type u where
| leaf : β → BTree β n
| node : Option (BTree β n) → Option (BTree β n) → BTree β (n + 1)
Edward van de Meent (Nov 15 2024 at 17:17):
i think it will be easier to have the Nat
not be a parameter, but rather a result of a function, similar to Vector
compared to List
. (it will help you keep out of dependent type hell).
Wrenna Robson (Nov 15 2024 at 17:17):
Hmm - I see why I'd want that, but I do wantto restrict "node" in this way... I think? Though perhaps I can simply avoid it.
Wrenna Robson (Nov 15 2024 at 17:19):
Hmm... I do think it would be much trickier to get some of the other properties I want without that.
Edward van de Meent (Nov 15 2024 at 17:20):
i'd like to point out that you will be able to construct empty node
s with arbitrary depths with the type as-is... is this what you want?
Wrenna Robson (Nov 15 2024 at 17:20):
Yep.
Edward van de Meent (Nov 15 2024 at 17:20):
huh
Edward van de Meent (Nov 15 2024 at 17:21):
what's your application, if i may ask?
Wrenna Robson (Nov 15 2024 at 17:21):
I am trying to prove something about the treeHash
function used in XMSS.
Wrenna Robson (Nov 15 2024 at 17:21):
https://www.rfc-editor.org/rfc/rfc8391#section-4.1.6
Arthur Adjedj (Nov 15 2024 at 17:22):
What you want is a nested indexed datatype. Lean currently doesn't manage those. One solution would be to (manually) convert your nested inductive type into a mutual type such as this one:
mutual
inductive OptionBTree (β : Type u) : Nat → Type u
| none : OptionBTree β n
| some : BTree β n → OptionBTree β n
inductive BTree (β : Type u) : (n : Nat) → Type u where
| leaf : β → BTree β n
| node : OptionBTree β n → OptionBTree β n → BTree β (n + 1)
end
Edward van de Meent (Nov 15 2024 at 17:23):
lean does manage it tho? you may have previously come across versions with List
s, for that matter.
Wrenna Robson (Nov 15 2024 at 17:23):
Yeah my second version above works :)
Edward van de Meent (Nov 15 2024 at 17:24):
i seem to recall it was a special implementation for only a few basic inductive types tho, but Option
is among them
Edward van de Meent (Nov 15 2024 at 17:24):
(i could be wrong)
Arthur Adjedj (Nov 15 2024 at 17:25):
Wrenna Robson said:
Yeah my second version above works :)
I get the following error on your second version (running on nightly):
inductive BTree (β : Type u) : (n : Nat) → Type u where
| leaf : β → BTree β n
| node : Option (BTree β n) → Option (BTree β n) → BTree β (n + 1)
/-(kernel) invalid nested inductive datatype 'Option', nested inductive datatypes parameters cannot contain local variables.-/
Wrenna Robson (Nov 15 2024 at 17:26):
Odd. I do get it in a fresh file, but I don't in my other one, but I really thought I wasn't using any imports
Arthur Adjedj (Nov 15 2024 at 17:27):
Edward van de Meent said:
lean does manage it tho? you may have previously come across versions with
List
s, for that matter.
Lean manages nested inductive types, and managed indexed inductive types, but it does not manage nested indexed inductive types. For example, the following fails:
inductive Foo (A : Type)
set_option inductive.autoPromoteIndices false in
inductive Bar (A : Type) : Nat → Type
| bar : Foo (Bar A n) → Bar A n
Wrenna Robson (Nov 15 2024 at 17:28):
Ah, this works:
inductive BTree {n : ℕ} (β : Type u) : (n : ℕ) → Type u where
| leaf : β → BTree β 0
| node : Option (BTree β n) → Option (BTree β n) → BTree β (n + 1)
Wrenna Robson (Nov 15 2024 at 17:28):
I had a variable
somewhere that was introducing the {n : ℕ}
Wrenna Robson (Nov 15 2024 at 17:28):
So that's not really what I want, certainly.
Arthur Adjedj (Nov 15 2024 at 17:29):
Wrenna Robson said:
Odd. I do get it in a fresh file, but I don't in my other one, but I really thought I wasn't using any imports
No imports can solve this. Are you sure you sent the "right" definition ? something like this would pass, because Nat
would be promoted to a parameter instead of an index.
inductive BTree (β : Type u) : (n : Nat) → Type u where
| leaf : β → BTree β n
| node : Option (BTree β n) → Option (BTree β n) → BTree β n --used to be BTree β (n+1)
Nevermind, you answered already
Wrenna Robson (Nov 15 2024 at 17:30):
As I say, I had variable {n : Nat}
much further above, and that was causing it to "work" (but you can see why actually it doesn't so much work)
Edward van de Meent (Nov 15 2024 at 17:30):
out of curiosity, why does removing the Nat
parameter fix this issue?
Wrenna Robson (Nov 15 2024 at 17:31):
Well, it makes my above example not work, so it doesn't so much fix it as reveal that my "fix" was not.
Wrenna Robson (Nov 15 2024 at 17:31):
The mutual definition above does work, but might be somewhat unwieldy.
Edward van de Meent (Nov 15 2024 at 17:31):
it turns out i just need to read through your message more carefully: because Nat
is an index here, and as you said, lean doesn't manage nested indexed inductives.
Wrenna Robson (Nov 15 2024 at 17:35):
Essentially the issue arises because I am constructing the tree "downwards" rather than upwards.
In the application, I am trying to prove that calculating the hash of a tree with 2^n leaf nodes in the "natural" recursive way (if n = 0, done, otherwise split into two halves, calculate their hashes, find hash of the result) is the same as using a "stack" (as the reference implementation of treeHash does).
Wrenna Robson (Nov 15 2024 at 17:36):
The difference essentially is that you massively save on storage space, because you only actually need to store ~ n things at once, rather than 2^n things.
Wrenna Robson (Nov 15 2024 at 17:36):
Basically you calculate hashes "when you need them", adding a leaf at a time.
Arthur Adjedj (Nov 15 2024 at 17:37):
I mean, to be clearer, Lean does manage some instances of nested indexed inductive types. Namely, as long as the indices appearing in the nested occurences do not contain free variables (read: variables introduced in the constructor itself), then it's all good, but it won't manage it if free variables are appearing in the indices. Since in @Wrenna Robson's example, the n
appearing in the index is introduced as a parameter, Lean doesn't complain, despite the fact that it is indeed a nested indexed inductive types.
Wrenna Robson (Nov 15 2024 at 17:37):
This turns out to sort of be analogous to binary number succession (you can model your stack as a list of Option types and basically adding a new leaf and calculating any new hashes is analogous to adding 1 to the binary number and doing the carries).
Wrenna Robson (Nov 15 2024 at 17:37):
And it's really surprisingly hard to prove them equivalent despite the fact it's "obviously" true.
Edward van de Meent (Nov 15 2024 at 17:38):
Wrenna Robson said:
Essentially the issue arises because I am constructing the tree "downwards" rather than upwards.
in that case, i still don't get why you want the "depth" value to be constant over the construction?
Wrenna Robson (Nov 15 2024 at 17:38):
I don't, I want it to descend.
Wrenna Robson (Nov 15 2024 at 17:38):
Hence the n + 1
. But it may not be the right choice.
Arthur Adjedj (Nov 15 2024 at 17:38):
If you're expected to work with 2^n leaves, wouldn't you want your node
to be of type BTree β n → BTree β n → BTree β (n + 1)
?
Wrenna Robson (Nov 15 2024 at 17:39):
Yeah so obviously that does work (and is what I had before) - the thing is that I want to allow for "truncated" trees, or unknown trees - basically to represent when I've not loaded in all the leaves.
Wrenna Robson (Nov 15 2024 at 17:40):
Essentially it doesn't seem like induction on n is ever quite enough - I would like to be able to construct my tree by adding a leaf at a time and performing cancellations as appropriate (if I have two leaves with a value specified at a node, I can replace that node with a leaf of height + 1 whose value is the hash of the other two).
Wrenna Robson (Nov 15 2024 at 17:41):
But that means I need to be able to work with, essentially, any number of leaves from 0 to 2^n (inclusive).
Wrenna Robson (Nov 15 2024 at 17:42):
I could just do something like this:
inductive BTree (β : Type u) : ℕ → Type u where
| leaf : (n : ℕ) → Option β → BTree β n
| node : {n : ℕ} → BTree β n → BTree β n → BTree β (n + 1)
Wrenna Robson (Nov 15 2024 at 17:43):
This has proven fairly fruitful, but I'm not sure it's quite right.
Edward van de Meent (Nov 15 2024 at 17:46):
i'd recommend the following (to avoid dependent type hell):
import Mathlib.Util.CompileInductive
inductive BTree (β : Type u) : Type u where
| leaf : β → BTree β
| node : Option (BTree β) → Option (BTree β) → BTree β
compile_inductive% BTree
def BTree.depth {β:Type u} (t : BTree β) : Nat :=
t.rec (fun _ => 0) (fun _ _ l r => (max l r) + 1) 0 (fun _ n => n)
def BTree' (β : Type u) (n:Nat) : Type u := {t:BTree β // t.depth = n}
Wrenna Robson (Nov 15 2024 at 17:46):
Oho
Edward van de Meent (Nov 15 2024 at 17:48):
note that this doesn't enforce the 2^n stuff, the shape can be weird. but it should at least give you a way out of having to rewrite inside the type of things
Wrenna Robson (Nov 15 2024 at 17:48):
Yeah that is always Bad.
Wrenna Robson (Nov 15 2024 at 17:49):
It does mean that if t = node l r
, you know that the depth of l and r are less than the depth of t, but no more than that I think?
Edward van de Meent (Nov 15 2024 at 17:49):
exactly that, yes
Wrenna Robson (Nov 15 2024 at 17:49):
Well, I suppose one of them at least has to have depth one less than it.
Edward van de Meent (Nov 15 2024 at 17:49):
no, both
Wrenna Robson (Nov 15 2024 at 17:50):
Well, l could have depth 0, say, no?
Wrenna Robson (Nov 15 2024 at 17:50):
And then the depth of t
will be r.depth + 1
Edward van de Meent (Nov 15 2024 at 17:51):
the depth of a .node
is the maximum of the depths of subtrees plus 1. so it is always larger than both depths.
Wrenna Robson (Nov 15 2024 at 17:52):
No I get that
Wrenna Robson (Nov 15 2024 at 17:52):
I'm just saying that the one with the larger depth (if they have different depths) will obviously be equal to the maxmimum, one less than it.
Wrenna Robson (Nov 15 2024 at 17:52):
Wheras the other can range anywhere inbetween.
Last updated: May 02 2025 at 03:31 UTC