Zulip Chat Archive

Stream: new members

Topic: Tree with finite map of children


Jules Jacobs (Feb 23 2024 at 23:26):

I would like to define a tree data type where a node consists of a finite map from naturals to children:

inductive tree where
  | leaf : Bool  tree
  | node : finmap Nat tree  tree

A finmap K V should be like K → Option V but with the constraint that only finitely many K map to Some. I am having trouble arranging this in such a way that I can write functions by recursion and prove theorems by induction. For example, a function that negates all the booleans at the leaves, and prove that doing this twice is the identity. What is the right approach in this situation?

Matt Diamond (Feb 24 2024 at 03:02):

maybe something like this?

inductive tree where
  | leaf (b : Bool) : tree
  | node {n} (f : Fin n  tree) : tree

Matt Diamond (Feb 24 2024 at 03:33):

import Mathlib

inductive tree where
| leaf (b : Bool) : tree
| node {n} (f : Fin n  tree) : tree

def negate : tree  tree
| .leaf b => .leaf !b
| .node f => .node (fun n => negate (f n))

example : (negate  negate) = id := by
  ext t
  rw [Function.comp_apply, id_eq]
  induction' t with l n f IH
  · simp [negate]
  · simp [negate, IH]

Timo Carlin-Burns (Feb 24 2024 at 04:45):

If you really need Nat -> Option Tree it's going to be a pain. Lean's limited support for nested inductive types does allow you to write | node : (Nat → (Option Tree)) → Tree, but it doesn't allow you to add any further arguments which depend on that function, so you'll have trouble adding a finiteness condition. In the future, hopefully we'll have a proper datatypes package which supports use cases like this, but for now we're stuck with some ugly workarounds. The best way I know is to create an initial inductive type without the side conditions and then define Tree as a subtype of that.

-- A tree data type which doesn't quite meet the specification - each node can have potentially infinite children
inductive PreTree where
  | leaf : Bool  PreTree
  | node : (Nat  Option PreTree)  PreTree

def IsFinmap (f : Nat  Option α) : Prop :=
   n,  i, i  n  f i = none

namespace PreTree

inductive IsFinite : PreTree  Prop
| leaf (b : Bool) : IsFinite (leaf b)
| node {f : Nat  Option PreTree} : IsFinmap f  ( i c, f i = some c  IsFinite c)  IsFinite (node f)

end PreTree

structure Tree where
  toPreTree : PreTree
  isFinite : toPreTree.IsFinite

Matt Diamond (Feb 24 2024 at 05:01):

I suspect the Option requirement is an #xy problem... if the requirement is only "a finite map from naturals to children" then there are better ways to handle it, I think

Timo Carlin-Burns (Feb 24 2024 at 05:13):

If you do need nested Option Tree, the other annoying thing is that it becomes impossible to use structural recursion. The typical way of doing recursive definitions with a nested inductive type is to establish a well-founded relation like so:

import Std

inductive PreTree where
  | leaf : Bool  PreTree
  | node : (Nat  Option PreTree)  PreTree

namespace PreTree

def IsChildOf (x y : PreTree) : Prop :=
   f, y = node f   i, x  f i

@[simp]
theorem not_isChildOf_leaf (x : PreTree) (b : Bool) : ¬x.IsChildOf (leaf b) := by
  simp [IsChildOf]

@[simp]
theorem isChildOf_node (x : PreTree) (f : Nat  Option PreTree) : x.IsChildOf (node f)   i, x  f i := by
  simp [IsChildOf]

#check PreTree.rec -- Nested inductive types have recurses with multiple motives

-- Luckily, once we define this well-founded relation, we'll never have to use the recursor again.
theorem wellFounded_IsChildOf : WellFounded IsChildOf := by
  constructor
  intro x
  apply PreTree.rec (motive_1 := Acc IsChildOf) (motive_2 := fun o : Option PreTree   y  o, Acc IsChildOf y)
  · intro b
    constructor
    simp
  · intro f ih
    constructor
    intro y h
    suffices  i, y  f i by
      rcases this with i, hi
      exact ih i y hi
    simpa using h
  · simp
  · simp

instance : WellFoundedRelation PreTree where
  rel := IsChildOf
  wf := wellFounded_IsChildOf

def flip : PreTree  PreTree
  | leaf b => leaf !b
  | node f => node fun i 
      match h : f i with
      | some c =>
        have :  i, c  f i := i, h
        some (flip c)
      | none => none
termination_by t => t

theorem flip_flip : (t : PreTree)  flip (flip t) = t
  | leaf b => by simp [flip]
  | node f => by
    simp [flip]
    funext i
    match h : f i with
    | some c =>
      have :  i, c  f i := i, h
      simp [flip_flip c]
    | none => simp
termination_by t => t

Timo Carlin-Burns (Feb 24 2024 at 05:52):

Another option if you need the mapping from naturals to children to be sparse is to split the Nat -> Option Tree into two components apply : Nat -> Tree and contains : Nat -> Bool. Then apply i is a garbage value whenever contains i is false.

inductive PreTree where
  | leaf : Bool  PreTree
  | node' (apply : Nat  PreTree) (contains : Nat  Bool) (isFinite :  n,  i, i  n  contains i = ff)

This avoids nested inductives so gives you nice structural recursion again. You could then define Tree as a quotient of PreTree to ignore the garbage values.

Jules Jacobs (Feb 24 2024 at 06:06):

Thanks for the helpful answer, that's exactly what I needed!


Last updated: May 02 2025 at 03:31 UTC