Zulip Chat Archive

Stream: new members

Topic: Type mismatch when matching inductive dependent type


aron (Jan 24 2025 at 21:17):

I've got these two types representing allergens and snacks that may contain them. I've designed the Snack type so that any allergens inside it will appear in a dependent list in its type:

inductive Allergen
  | nuts : Allergen
  | dairy : Allergen

inductive Snack : (allergens : List Allergen)  Type u where
  | chips : Snack []
  | popcorn : Snack []
  | peanuts : Snack [.nuts]
  | chocolateCovered : (snack : Snack allgns) -> Snack (.dairy :: allgns)
  | twoSnacks : Snack allgns1 -> Snack allgns2 -> Snack (allgns1 ++ allgns2)

The idea being that if you have a function that only accepts a Snack [] that guarantees it can't accept a snack containing any allergens:

def allergyFreeSnacks (snack : Snack []) :=
  match snack with
  | .chips => true
  | .popcorn => true
  | .twoSnacks snack1 snack2 => allergyFreeSnacks snack1 && allergyFreeSnacks snack2

However I get an error on the .twoSnacks case above:

type mismatch
  snack1.twoSnacks snack2
has type
  Snack (?m.4976 ++ ?m.4980) : Type ?u.4959
but is expected to have type
  Snack [] : Type ?u.4959

How can I tell Lean that since the matched value has type Snack [] it's impossible for snack1 and snack2 to be anything other than Snack [] and since [] ++ [] = [] the pattern is fine?

Aaron Liu (Jan 24 2025 at 22:32):

In this case, I think I would use the recursor directly, but that can be slow to evaluate (there's a reason why it's noncomputable by default).

import Mathlib.Util.CompileInductive

inductive Allergen
  | nuts : Allergen
  | dairy : Allergen

inductive Snack : (allergens : List Allergen)  Type u where
  | chips : Snack []
  | popcorn : Snack []
  | peanuts : Snack [.nuts]
  | chocolateCovered : (snack : Snack allgns) -> Snack (.dairy :: allgns)
  | twoSnacks : Snack allgns1 -> Snack allgns2 -> Snack (allgns1 ++ allgns2)

compile_inductive% Snack

def allergyFreeSnacks (snack : Snack []) : Bool :=
  Snack.rec
    (motive := fun allergens snack => allergens = []  Bool)
    (chips := fun _ => true)
    (popcorn := fun _ => true)
    (peanuts := nofun)
    (chocolateCovered := nofun)
    (twoSnacks := fun fst snd ih₁ ih₂ hEq => by
      refine ih₁ ?_ && ih₂ ?_
      · simp_all
      · simp_all) snack rfl

-- show the definition is correct by proving a theorem
theorem allergyFreeSnacks_eq (snack : Snack []) : allergyFreeSnacks snack = true := by
  revert snack
  suffices h :  {l} (hEq : l = []) (snack : Snack l), allergyFreeSnacks (hEq  snack) = true from
    h rfl
  intro l hEq snack
  induction snack with
  | chips => rfl
  | popcorn => rfl
  | peanuts => contradiction
  | chocolateCovered => contradiction
  | twoSnacks fst snd ih₁ ih₂ =>
    rw [List.append_eq_nil] at hEq
    rcases hEq with ⟨⟨⟩, ⟨⟩⟩
    specialize ih₁ rfl
    specialize ih₂ rfl
    rw [allergyFreeSnacks] at ih₁ ih₂ 
    dsimp at *
    rw [ih₁, ih₂, Bool.and_true]

Here are some other options:
Have allergyFreeSnacks take both a parameter snack : Snack l and a proof h : l = [], and rewrite using h when necessary.
Have allergyFreeSnacks take a parameter snack : Snack l, and return a junk value in the case that l ≠ [].

Pawel Welsberg (Jan 24 2025 at 22:49):

Newbie here as well.
Why not just simply:

def allergyFreeSnacks (_ : Snack []) := true

Then you have following:

#eval allergyFreeSnacks Snack.chips -- true
#eval allergyFreeSnacks (Snack.twoSnacks Snack.chips Snack.popcorn) -- true
#eval allergyFreeSnacks (Snack.twoSnacks Snack.chips (Snack.twoSnacks Snack.popcorn Snack.popcorn)) -- true

#eval allergyFreeSnacks Snack.peanuts -- type error
#eval allergyFreeSnacks (Snack.chocolateCovered Snack.chips) -- type error
#eval allergyFreeSnacks (Snack.twoSnacks Snack.chips Snack.peanuts) -- type error

aron (Jan 24 2025 at 22:55):

@Aaron Liu hmm ok I've only seen recursors mentioned in this page and I didn't find the explanations very clear to understand what they are or how they work.

But your implementation gives me something concrete to play around with and hopefully understand recursors a bit better. Thanks!

aron (Jan 24 2025 at 22:56):

@Pawel Welsberg, ha yeah that would actually be quite an elegant solution! But this code snippet is just a minimal reproduction of a more complex domain where I do need to return different values for different cases. Which means I can't skip the pattern matching unfortunately :(

Pawel Welsberg (Jan 24 2025 at 23:23):

@aron :big_smile: I understand you now. Fingers crossed for you to find the solution.
I'd thought you could use something like this:

def isAllergyFreeSnack (snack : Snack allergens) : Prop :=
  match snack with
  | Snack.chips => True
  | Snack.popcorn => True
  | Snack.twoSnacks snack1 snack2 => isAllergyFreeSnack snack1  isAllergyFreeSnack snack2
  | _ => False

def allergyFreeSnacks (snack : Snack allgns) (h: isAllergyFreeSnack snack) :=
  match snack with
  | Snack.chips => true
  | Snack.popcorn => true
  | Snack.twoSnacks snack1 snack2 => sorry

but it errors with:

type mismatch
  Snack.chips
has type
  Snack [] : Type ?u.1731
but is expected to have type
  Snack allgns : Type ?u.1731

On the first match in allergyFreeSnacks.

Pawel Welsberg (Jan 24 2025 at 23:46):

What about this approach:

def isAllergyFreeSnack (snack : Snack allergens) : Prop :=
  match snack with
  | Snack.chips => True
  | Snack.popcorn => True
  | Snack.twoSnacks snack1 snack2 => isAllergyFreeSnack snack1  isAllergyFreeSnack snack2
  | _ => False

def allergyFreeSnacks (snack : Snack allergens) (h: isAllergyFreeSnack snack) :=
match allergens with
| [] => true
| _ => match snack with
  | Snack.twoSnacks snack1 snack2 => allergyFreeSnacks snack1 h.left && allergyFreeSnacks snack2 h.right
  | _ => false

Kyle Miller (Jan 24 2025 at 23:59):

Here's a possibility:

inductive Allergen
  | nuts : Allergen
  | dairy : Allergen

inductive Snack : (allergens : List Allergen)  Type where
  | chips : Snack []
  | popcorn : Snack []
  | peanuts : Snack [.nuts]
  | chocolateCovered : (snack : Snack allgns) -> Snack (.dairy :: allgns)
  | twoSnacks : Snack allgns1 -> Snack allgns2 -> Snack (allgns1 ++ allgns2)

def allergyFreeSnacks {allergens} (snack : Snack allergens) (h : allergens = [] := by rfl) :=
  match allergens, h, snack with
  | _, _, .chips => true
  | _, _, .popcorn => true
  | _, _, .twoSnacks snack1 snack2 =>
    allergyFreeSnacks snack1 (by simp_all) && allergyFreeSnacks snack2 (by simp_all)

#eval allergyFreeSnacks .chips
-- true
#eval allergyFreeSnacks .peanuts
/-
tactic 'rfl' failed, the left-hand side
  [Allergen.nuts]
is not definitionally equal to the right-hand side
  []
-/

Aaron Liu (Jan 25 2025 at 04:52):

aron said:

Aaron Liu hmm ok I've only seen recursors mentioned in this page and I didn't find the explanations very clear to understand what they are or how they work.

But your implementation gives me something concrete to play around with and hopefully understand recursors a bit better. Thanks!

@aron note that what I wrote is equivalent to

def allergyFreeSnacks (snack : Snack []) : Bool :=
  aux snack rfl
where
  aux {l} : Snack l  l = []  Bool
  | .chips => fun _ => true
  | .popcorn => fun _ => true
  | .peanuts => nofun
  | .chocolateCovered base => nofun
  | .twoSnacks fst snd => fun hEq => by
    refine aux fst ?_ && aux snd ?_
    · simp_all
    · simp_all

Here aux is replacing the recursor.

aron (Jan 25 2025 at 11:20):

@Kyle Miller mm yeah that does work but I don't understand why.
Why are we matching on allergens and h if we're not pattern matching on them at all?
How does simp_all know to solve that allgns1✝ = []?

aron (Jan 25 2025 at 11:23):

btw it seems like we don't need to pass allergens into the match after all. This works:

def allergyFreeSnacks {allergens} (snack : Snack allergens) (h : allergens = [] := by rfl) :=
  match h, snack with
  | _, .chips => true
  | _, .popcorn => true
  | _, .twoSnacks snack1 snack2 =>
    allergyFreeSnacks snack1 (by simp_all) && allergyFreeSnacks snack2 (by simp_all)

But we do still need h or it stops working. I don't get what the h is doing in there though, since we're not pattern matching against it

Kyle Miller (Jan 26 2025 at 17:53):

match has a feature where if you pass something like h it automatically adds the arguments it depends on as additional discriminants (allergens in this case). It's ok to omit it for brevity.

The key to why adding h works is that (1) Eq is an inductive type, so match can reason about its constructors and (2) this allows match to see that the three cases actually do exhaust all the possibilities.

aron (Jan 26 2025 at 18:06):

Mm ok. That's interesting to know, but feels quite 'magic'y so I don't really know how to leverage that for myself in the future

Kyle Miller (Jan 26 2025 at 18:06):

This is telling match that allergens = []

aron (Jan 28 2025 at 11:13):

I'm trying to do this now

inductive TypeContainer (uv tv : Type u) : (typeVars : Set tv)  Type u where
  | primitive : (p : Primitive)  TypeContainer uv tv 
  | option : TypeContainer uv tv tvs  TypeContainer uv tv tvs
  | result : (ok : TypeContainer uv tv okTvs) 
    (err : TypeContainer uv tv errTvs) 
    (combinedTvs = okTvs  errTvs) 
    TypeContainer uv tv combinedTvs
  | uniVar : (v : uv)  TypeContainer uv tv 
  | typeVar : (v : tv)  TypeContainer uv tv {v}

def ConcreteType := TypeContainer Empty Empty 

def f (x : ConcreteType) : Bool :=
  match x with
  | .primitive _ => true
  | .option t => f t
  | .result ok err tvPrf =>
    by
    have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
    rewrite [tvs.left] at ok
    rewrite [tvs.right] at err
    exact f ok && f err
  | .uniVar t => Empty.elim t
  | .typeVar t => Empty.elim t
--  ^^^^^^^^^^
-- type mismatch
--   TypeContainer.typeVar t
-- has type
--   TypeContainer ?m.8363 ?m.8360 {t} : Type ?u.8362
-- but is expected to have type
--   ConcreteType : Type

I get errors both if I include the .typeVar case and if I don't include it.

This code snippet is similar to my earlier example, but because I'm using the self contained ConcreteType type alias here I can't use the approach of separating the proof that typeVars set is empty into its own parameter.

And because ConcreteType is a type alias and not its own inductive type, I can't use compile_inductive% on it either.

How can I pattern match on ConcreteType without errors?

aron (Jan 28 2025 at 11:30):

I'm trying a cases tactic to see if that'll work better

def f (x : ConcreteType) : Bool :=
  by
  cases h : x with
  | primitive p => exact false
  | option t => exact f t
  | result ok err tvPrf =>
    have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
    rewrite [tvs.left] at ok
    rewrite [tvs.right] at err
    exact f ok && f err
  | uniVar t => exact Empty.elim t

this doesn't let me match on typeVar at all, but it has this error instead

dependent elimination failed, failed to solve equation
  (fun x => False) = fun b => b = v

Kyle Miller (Jan 29 2025 at 04:31):

"dependent elimination failed" has to do with a step where cases tries to eliminate some auxiliary equalities that it inserts to deal with type mismatches. The way it eliminates them is by using constructor injectivity and substitution. This equation does not involve constructors, and it fails.

The equality insertion is like the equality I added manually in an earlier example. It "relaxes" the problem so that the dependent types don't need to line up with definitional equality, just propositional equality.

aron (Jan 31 2025 at 21:16):

I'm trying this again using the strategy that @Aaron Liu used in the aux example, but I'm getting a fail to show termination error. Here's my code:

def f (x : ConcreteType) : Bool :=
  aux x rfl
where
  aux {s : Set Empty} : (x : TypeContainer Empty Empty s)  (s = )  Bool
    | .primitive _ => fun _ => true
    | .option t => fun prf => aux t prf
    | .result ok err tvPrf =>
      fun prf =>
        by
        rewrite [prf] at tvPrf
        have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
        rewrite [tvs.left] at ok
        rewrite [tvs.right] at err
        exact aux ok rfl && aux err rfl

    | .list t => fun prf => aux t prf
    | .arrow from' to' tvPrf =>
      fun prf =>
        by
        rewrite [prf] at tvPrf
        have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
        rewrite [tvs.left] at from'
        rewrite [tvs.right] at to'
        exact aux from' rfl && aux to' rfl

    | .uniVar t => fun prf => Empty.elim t
    | .typeVar t => nofun

This does seem to work – there are no errors on any of the cases! – except for this error:

fail to show termination for
  f.aux
with errors
failed to infer structural recursion:
Not considering parameter s of f.aux:
  its type is not an inductive
Cannot use parameter #2:
  failed to eliminate recursive application
    f.aux (⋯.mp ok) 


failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal

But there don't seem to be any unsolved goals in any of the cases, so I don't know what the problem is or how to solve it. And anyway, why can't it see that since I'm always recursing on a smaller part of the input that it by necessity will terminate?

Aaron Liu (Jan 31 2025 at 21:44):

Instead of rewriting the hypotheses, try rewriting the conclusion. Lean can't see that Eq.mp ⋯ ok is smaller than .result ok err tvPrf.

aron (Jan 31 2025 at 21:47):

Sorry what is the conclusion in this context? And I'm assuming the hypothesis is that s = ∅?

Also I've implemented a SizeOf instance in case that would help. Can I use that to help here?

def TypeContainer.sizeOf (tc : TypeContainer uv tv tvs) :=
    match tc with
    | .primitive _ => 1
    | .option t => sizeOf t + 1
    | .result ok err prfTvs =>
      1 + max (sizeOf ok) (sizeOf err)
    | .list t => sizeOf t + 1
    | .arrow from' to' prfTvs =>
      1 + max (sizeOf from') (sizeOf to')
    | .uniVar _ => 1
    | .typeVar _ => 1

instance : SizeOf (TypeContainer uv tv tvs) where
  sizeOf := TypeContainer.sizeOf

Aaron Liu (Jan 31 2025 at 21:47):

Also, it is discouraged to use tactics like rewrite and simp to construct data, since you cannot control the kinds of terms these tactics output.

Aaron Liu (Jan 31 2025 at 21:47):

If you give me a #mwe, I can show you, but right now your code does not work for me.

Aaron Liu (Jan 31 2025 at 21:49):

The hypotheses are ok and err.

aron (Jan 31 2025 at 21:50):

Alright here's all the code for a #mwe:

import Mathlib

inductive TypeContainer (uv tv : Type u) : (typeVars : Set tv)  Type u where
  | primitive : (p : Primitive)  TypeContainer uv tv 
  | option : TypeContainer uv tv tvs  TypeContainer uv tv tvs
  | result : (ok : TypeContainer uv tv okTvs) 
    (err : TypeContainer uv tv errTvs) 
    (combinedTvs = okTvs  errTvs) 
    TypeContainer uv tv combinedTvs
  | list : TypeContainer uv tv tvs  TypeContainer uv tv tvs
  | arrow : (from' : TypeContainer uv tv fromTvs) 
    (to' : TypeContainer uv tv toTvs) 
    (combinedTvs = fromTvs  toTvs) 
    TypeContainer uv tv combinedTvs
  | uniVar : (v : uv)  TypeContainer uv tv 
  | typeVar : (v : tv)  TypeContainer uv tv {v}

def ConcreteType := TypeContainer Empty Empty 

def TypeContainer.sizeOf (tc : TypeContainer uv tv tvs) :=
    match tc with
    | .primitive _ => 1
    | .option t => sizeOf t + 1
    | .result ok err prfTvs =>
      1 + max (sizeOf ok) (sizeOf err)
    | .list t => sizeOf t + 1
    | .arrow from' to' prfTvs =>
      1 + max (sizeOf from') (sizeOf to')
    | .uniVar _ => 1
    | .typeVar _ => 1

instance : SizeOf (TypeContainer uv tv tvs) where
  sizeOf := TypeContainer.sizeOf



def f (x : ConcreteType) : Bool :=
  aux x rfl
where
  aux {s : Set Empty} : TypeContainer Empty Empty s  s =   Bool
    | .primitive _ => fun _ => true
    | .option t => fun prf => aux t prf

    | .result ok err tvPrf =>
      fun prf =>
        by
        rewrite [prf] at tvPrf
        have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
        rewrite [tvs.left] at ok
        rewrite [tvs.right] at err
        exact aux ok rfl && aux err rfl

    | .list t => fun prf => aux t prf
    | .arrow from' to' tvPrf =>
      fun prf =>
        by
        rewrite [prf] at tvPrf
        have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
        rewrite [tvs.left] at from'
        rewrite [tvs.right] at to'
        exact aux from' rfl && aux to' rfl

    | .uniVar t => fun prf => Empty.elim t
    | .typeVar t => nofun

aron (Jan 31 2025 at 21:53):

Aaron Liu said:

Also, it is discouraged to use tactics like rewrite and simp to construct data, since you cannot control the kinds of terms these tactics output.

Yeah I don't like it either but I don't know how else to tell Lean that okTvs✝ = ∅ without using a by block :/

Aaron Liu (Jan 31 2025 at 21:58):

This works

def f (x : ConcreteType) : Bool :=
  aux x rfl
where
  aux {s : Set Empty} : TypeContainer Empty Empty s  s =   Bool
    | .primitive _ => fun _ => true
    | .option t => fun prf => aux t prf

    | .result ok err tvPrf => by
      -- `refine` is okay because it doesn't do anything complicated, just makes holes into new goals
      refine fun prf => aux ok ?_ && aux err ?_
      -- now we are using tactics to construct proof terms
      · subst prf
        exact (Set.union_empty_iff.mp tvPrf.symm).left
      · subst prf
        exact (Set.union_empty_iff.mp tvPrf.symm).right

    | .list t => fun prf => aux t prf
    | .arrow from' to' tvPrf => by
      refine fun prf => aux from' ?_ && aux to' ?_
      · subst prf
        exact (Set.union_empty_iff.mp tvPrf.symm).left
      · subst prf
        exact (Set.union_empty_iff.mp tvPrf.symm).right

    | .uniVar t => fun prf => Empty.elim t
    | .typeVar t => nofun

This works because the arguments to aux in the recursive branches have not been modified by rewrite.

Aaron Liu (Jan 31 2025 at 21:59):

Your code didn't work because you were feeding the recursive call arguments which had been rewritten, and so were not obviously subterms anymore.

aron (Jan 31 2025 at 22:09):

Ahhh ok I see... I think that makes sense. Btw between you saying that I should avoid using rewrite and simp on the hypotheses and you posting your solution I managed to do this:

def f (x : ConcreteType) : Bool :=
  aux x rfl
where
  aux {s : Set Empty} : TypeContainer Empty Empty s  s =   Bool
    | .primitive _ => fun _ => true
    | .option t => fun prf => aux t prf

    | .result ok err tvPrf =>
      fun prf =>
      by
      rewrite [prf] at tvPrf
      have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
      refine aux ok ?one && aux err ?two
      · exact tvs.left
      · exact tvs.right

    | .list t => fun prf => aux t prf
    | .arrow from' to' tvPrf =>
      fun prf =>
        by
        rewrite [prf] at tvPrf
        have tvs := Set.union_empty_iff.mp (Eq.symm tvPrf)
        refine aux from' ?_ && aux to' ?_
        · exact tvs.left
        · exact tvs.right

    | .uniVar t => fun prf => Empty.elim t
    | .typeVar t => nofun

Which also works! :tada:

aron (Jan 31 2025 at 22:11):

Thanks so much for your help man :raised_hands: :pray:


Last updated: May 02 2025 at 03:31 UTC