Zulip Chat Archive

Stream: new members

Topic: Help with inductive family


Jesse Slater (Nov 12 2023 at 16:44):

I am looking for some design advice about using inductive families.

I have a mathematical structure like this: A binary tree where each node has two children l and r, an associated finite type T, and an associated function f : ( Sum T (Sum l.T r.T ) -> Nat. For a leaf node T is Empty.

I will use this structure in computation, and also prove things about it.

I already have a working version of my computation, based on a representation which does not enforce much of the mathematical structure in the type system.
I currently just define a tree as Sigma (numNodes : Nat), Sigma (m : Nat), Fin numNodes -> Fin m -> Nat where m is the length of all the Ts of the tree appended together, and the values of each function are zero for all of the values of Fin m that they should not be able to see.

This works fine, but I would like to build more of the tree structure into Lean's type system. The properties I want to prove depend on the fact that the function associated with each node is 0 for all m not in T or T.l or T.r, so my proof has to include a whole lot of extra stuff about ensuring indicies are correct. If I could embed the structure of my tree in the type system, I would not have to worry about this.

(Please ask for clarification if any of that was unclear. What I say there leaves out a lot of detail, since I am trying to give the minimum context required to get a good answer. However, I may have left out too much detail.)

I am trying to represent my tree with an inductive family (this may be the wrong approach). Here is what I have:

instance
  {α β γ : Type}
  [Repr (α  γ)]
  [Repr (β  γ)] : Repr (α  β  γ) where
   reprPrec f _ :=
    let l : α  γ :=
      λ a => f (Sum.inl a)
    let r : β  γ :=
      λ b => f (Sum.inr b)
    "(" ++ repr l ++ " ⊕ " ++ repr r ++ ")"

inductive _Tree : (s : Nat)  Type where
  | nil :
    _Tree 0
  | ind
    (l : _Tree j)
    (r : _Tree k)
    (f : Fin s  Fin j  Fin k  Nat)
    : _Tree s
    deriving Repr

def _nil := _Tree.nil

def _ind := _Tree.ind _nil _nil
  (λ| .inl 0 => 1
    | .inl 1 => 2
    | _ => 0
    : Fin 2  Fin 0  Fin 0  Nat)

def _ind' := _Tree.ind _ind _ind
  (λ| .inl 2 => 3
    | .inr (.inr 1) => 1
    | .inr (.inl 1) => 2
    | _ => 0
    : Fin 3  Fin 2  Fin 2  Nat)

#eval _ind'

This all seems to work, and _ind' is what I am hoping to see. However, I am having trouble defining a function on _Tree.

Here is my attempt to define a function which takes my new inductive family representation, and converts it back to my old representation (which is something I will want to be able to do).

def flatten (f : _Tree s) : Σ numNodes : Nat, Σ n : Nat, Fin numNodes  Fin n  Nat :=
  match f with
  | _Tree.nil => 0, 0, λ _ _ => 0
  | _Tree.ind l r f =>
    let lnodes, ln, lf := flatten l
    let rnodes, rn, rf := flatten r
     1 + lnodes + rnodes,
      s + ln + rn,
      λ j k =>
        let j' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc 1 lnodes rnodes) j)
          |> Sum.map id finSumFinEquiv.invFun
        let k' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc s ln rn) k)
          |> Sum.map id finSumFinEquiv.invFun
        match j', k' with
        | .inl 0, .inl s' =>
          f (.inl s')
        | .inl 0, .inr (.inl ln') =>
          if ln' < 0, sorry -- ln' < j✝
          then f (.inr (.inl (Fin.cast sorry ln')))
          else 0
        | .inl 0, .inr (.inr rn') =>
          if rn' < 0, sorry -- rn' < k✝
          then f (.inr (.inr (Fin.cast sorry rn')))
          else 0
        | .inr (.inl lnodes'), .inr (.inl ln') =>
          lf lnodes' ln'
        | .inr (.inr rnodes'), .inr (.inr rn') =>
          rf rnodes' rn'
        | _, _ => 0
      

The second two cases of the second match are giving me trouble. Those cases handle the fact that a node can see the Ts associated with it's two child nodes. Where I have ln' < 0, what it should say is ln' is less than ls, where the left child has type _Tree ls. Currently, the info view tells me that the left child has type _Tree j✝. I don't know how to name j✝ to get access to it, or if that is possible.

Is my problem clear? Please ask for follow up clarification if not.

Is there some easy solution that I am missing, or is this approach to representing my tree with an inductive family fundamentally misguided?

Jesse Slater (Nov 12 2023 at 18:07):

Ok, I seem to have gotten it working. I just needed to name j✝ and k✝ by disabling implicit arguments of _Tree.ind.

def flatten (t : _Tree s) : Σ numNodes : Nat, Σ n : Nat, Fin numNodes  Fin n  Nat :=
  match t with
  | _Tree.nil => 0, 0, λ _ _ => 0
  | @_Tree.ind ls rs _ l r f =>
    let lnodes, ln, lf := flatten l
    let rnodes, rn, rf := flatten r
     1 + lnodes + rnodes,
      s + ln + rn,
      λ j k =>
        let j' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc 1 lnodes rnodes) j)
          |> Sum.map id finSumFinEquiv.invFun
        let k' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc s ln rn) k)
          |> Sum.map id finSumFinEquiv.invFun
        match j', k' with
        | .inl 0, .inl s' =>
          f (.inl s')
        | .inl 0, .inr (.inl ln') =>
          if h : ln' < ls -- ln' < j✝
          then f (.inr (.inl (Fin.castLT ln' h)))
          else 0
        | .inl 0, .inr (.inr rn') =>
          if h : rn' < rs -- rn' < k✝
          then f (.inr (.inr (Fin.castLT rn' h)))
          else 0
        | .inr (.inl lnodes'), .inr (.inl ln') =>
          lf lnodes' ln'
        | .inr (.inr rnodes'), .inr (.inr rn') =>
          rf rnodes' rn'
        | _, _ => 0
      

#eval flatten _ind'

This seems to work how I would expect.

I would still like design advice. Is this a good way to represent this structure? Are there any problems I am going to run into when I try to prove things about this representation?

Jesse Slater (Nov 12 2023 at 18:13):

Also, I am a bit concerned that there is something I am not understanding. What is the difference between these two definitions?

def flatten (t : _Tree s) : Σ numNodes : Nat, Σ n : Nat, Fin numNodes  Fin n  Nat :=
  match t with
  | _Tree.nil => 0, 0, λ _ _ => 0
  | @_Tree.ind ls rs _ l r f =>
    let lnodes, ln, lf := flatten l
    let rnodes, rn, rf := flatten r
     1 + lnodes + rnodes,
      s + ln + rn,
      λ j k =>
        let j' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc 1 lnodes rnodes) j)
          |> Sum.map id finSumFinEquiv.invFun
        let k' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc s ln rn) k)
          |> Sum.map id finSumFinEquiv.invFun
        match j', k' with
        | .inl 0, .inl s' =>
          f (.inl s')
        | .inl 0, .inr (.inl ln') =>
          if h : ln' < ls -- ln' < j✝
          then f (.inr (.inl (Fin.castLT ln' h)))
          else 0
        | .inl 0, .inr (.inr rn') =>
          if h : rn' < rs -- rn' < k✝
          then f (.inr (.inr (Fin.castLT rn' h)))
          else 0
        | .inr (.inl lnodes'), .inr (.inl ln') =>
          lf lnodes' ln'
        | .inr (.inr rnodes'), .inr (.inr rn') =>
          rf rnodes' rn'
        | _, _ => 0
      

def flatten' (t : _Tree __) : Σ numNodes : Nat, Σ n : Nat, Fin numNodes  Fin n  Nat :=
  match t with
  | _Tree.nil => 0, 0, λ _ _ => 0
  | @_Tree.ind ls rs s l r f =>
    let lnodes, ln, lf := flatten l
    let rnodes, rn, rf := flatten r
     1 + lnodes + rnodes,
      s + ln + rn,
      λ j k =>
        let j' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc 1 lnodes rnodes) j)
          |> Sum.map id finSumFinEquiv.invFun
        let k' :=
          finSumFinEquiv.invFun (Fin.cast (Nat.add_assoc s ln rn) k)
          |> Sum.map id finSumFinEquiv.invFun
        match j', k' with
        | .inl 0, .inl s' =>
          f (.inl s')
        | .inl 0, .inr (.inl ln') =>
          if h : ln' < ls -- ln' < j✝
          then f (.inr (.inl (Fin.castLT ln' h)))
          else 0
        | .inl 0, .inr (.inr rn') =>
          if h : rn' < rs -- rn' < k✝
          then f (.inr (.inr (Fin.castLT rn' h)))
          else 0
        | .inr (.inl lnodes'), .inr (.inl ln') =>
          lf lnodes' ln'
        | .inr (.inr rnodes'), .inr (.inr rn') =>
          rf rnodes' rn'
        | _, _ => 0
      
#eval flatten _ind'
#eval flatten' _ind'

They appear to do the same thing. Does it change anything that I name s in different places? I feel like I am missing something about how inductive families work.


Last updated: Dec 20 2023 at 11:08 UTC