Zulip Chat Archive

Stream: lean4

Topic: Behaviour of `cases` depends on type constructor args' order


Yicheng Qian (Nov 07 2023 at 12:38):

The behaviour of cases tactic depends on the order of the type constructor's arguments, even if the two types are isomorphic:

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) : Foo (f n) false

def match_Foo (h : Foo 2 true) : False := by
  cases h -- Fails

inductive Bar : Bool  Nat  Type
  | ctor (f : Nat  Nat) (n : Nat) : Bar false (f n)

def match_Bar (h : Bar true 2) : False := by
  cases h

Here Foo and Bar are essentially the same, but cases hwf fails on match_Foo which succeeds on match_Bar.

Context: Here is a minimized example where the above issue becomes somewhat annoying. Switching arg order does not work because no matter what the order of argument, at least one constructor causes trouble.

inductive Ctx : Bool  Nat  Type
  | ctor1 (f : Nat  Nat) (n : Nat) : Ctx false (f n)
  | ctor2 (f : Bool  Bool) (n : Bool) : Ctx (f n) 0

def match_Ctx (h : Ctx true 2) : False := by
  cases h -- Fails

def match_Ctx' (h : Ctx true 2) : False := by
  revert h; generalize h₁ : 2 = a; generalize h₂ : true = b
  intro h; cases h
  case ctor1 => cases h₂
  case ctor2 => cases h₁

Yicheng Qian (Nov 07 2023 at 12:43):

I'm not sure if there's some tactic in lean/mathlib that can succeed in the case of match_Foo.

Yicheng Qian (Nov 07 2023 at 12:44):

Coq's inversion can succeed in both cases:

Inductive Foo : nat -> bool -> Type :=
  | ctorF (f : nat -> nat) (n : nat) : Foo (f n) false.

Theorem match_Foo (h : Foo 2 true) : False.
Proof.
  inversion h.
Qed.

Inductive Bar : bool -> nat -> Type :=
  | ctorB (f : nat -> nat) (n : nat) : Bar false (f n).

Theorem match_Bar (h : Bar true 2) : False.
Proof.
  inversion h.
Qed.

Version: Coq-Platform~8.15~2022.04.

Jannis Limperg (Nov 07 2023 at 12:55):

For cases to work seamlessly, you must avoid arbitrary functions in the indices of constructors. If you write your type like this, everything should work (untested):

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) (x : Nat) (h : x = f n) : Foo x false

More generally, constructor indices may contain variables and constructors of other data types, but nothing else. For such indices, the unification algorithm used by cases is complete.

(This rule should really be documented somewhere, and maybe linted for.)

Jannis Limperg (Nov 07 2023 at 12:56):

Actually, f is also a variable. Let me actually test my solution, lest I spout nonsense...

Jannis Limperg (Nov 07 2023 at 12:58):

Okay, it does work. :relieved: I guess the rule is that indices may contain first-order variables, constructors and nothing else.

Yicheng Qian (Nov 07 2023 at 13:02):

inductive Foo' : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) (x : Nat) (h : x = f n) : Foo' x false

Yes, this indeed works, but it brings out another issue: If I write a function f that matches on Foo', and tries to prove properties of f, then the cases or match on arguments of f will fail with motive not type correct, while if I use

inductive Bar : Bool  Nat  Type
  | ctor (f : Nat  Nat) (n : Nat) : Bar false (f n)

cases and match will succeed.

Yicheng Qian (Nov 07 2023 at 13:03):

I'm not sure whether the above description is accurate, because I barely remember having this issue. I'll try to get a mwe.

Yicheng Qian (Nov 07 2023 at 13:08):

Yicheng Qian said:

I'm not sure if there's some tactic in lean/mathlib that can succeed in the case of match_Foo.

I can confirm that induction h and induction' h does not work.

Jannis Limperg (Nov 07 2023 at 13:09):

Yicheng Qian said:

I can confirm that induction h and induction' h does not work.

This is expected: cases, induction and the match compiler all use the same algorithm (I think).

Yicheng Qian (Nov 07 2023 at 15:18):

Yicheng Qian said:

I'm not sure whether the above description is accurate, because I barely remember having this issue. I'll try to get a mwe.

I failed to get a mwe, and remembered that the issue I had wasn't related to this one ... I'll probably start a new topic for that issue.

Yicheng Qian (Nov 07 2023 at 15:20):

Anyway, the unification algorithm of cases could have done better, since in

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) : Foo (f n) false

def match_Foo (h : Foo 2 true) : False := by
  cases h -- Fails

It should have delayed the unification of 2 =? f n, and then it would spot that false =? true can't be unified.

Doing

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) (x : Nat) (h : x = f n) : Foo x false

is essentially telling cases that 2 =? f n should be ignored.

Jannis Limperg (Nov 07 2023 at 17:19):

Could be implemented, but I doubt it would be worth it. The current algorithm is quite nice: simple, linear in the number of indices and complete for a well-defined fragment. Introducing postponement would increase the complexity quite a bit, for not that much gain. In particular, you would likely still run into trouble with the data type as you initially defined it; postponement would only address this specific class of examples.

Kyle Miller (Nov 07 2023 at 17:52):

Here's a standard sort of workaround for the very first example. You use generalize to turn indices into variables.

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) : Foo (f n) false

def match_Foo (h : Foo 2 true) : False := by
  generalize hn : 2 = n at h
  cases h -- succeeds

Kyle Miller (Nov 07 2023 at 17:53):

It'd be neat if cases/induction had an extra clause for generalizing indices (maybe you give the names of the new variables and the names of their equalities?)

Kyle Miller (Nov 07 2023 at 17:53):

Oh, I missed that you already mentioned generalize in that message, sorry.

Timo Carlin-Burns (Nov 07 2023 at 21:16):

It seems like the behavior of cases could be improved here without regressing from linear time. Why couldn't it detect that ctor has one of these ill-behaved function application indices and then automatically call generalize on the term being supplied for that index?

Jannis Limperg (Nov 10 2023 at 11:17):

Doesn't sound unreasonable. I would still prefer a linter that discourages the problematic inductive type definitions, since that's the rule that Conor McBride, the inventor of the cases algorithm, has been advocating for since forever. ("No green goo" is the slogan.)

Yicheng Qian (Nov 10 2023 at 13:02):

Timo Carlin-Burns said:

It seems like the behavior of cases could be improved here without regressing from linear time. Why couldn't it detect that ctor has one of these ill-behaved function application indices and then automatically call generalize on the term being supplied for that index?

This would be equivalent to "ignore" instead of "postpone" the unsolvable unification problems, I think.

Yicheng Qian (Nov 10 2023 at 13:06):

It would also be equivalent to translating the "problematic" inductive types into "non-problematic" ones, because turning

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) : Foo (f n) false

into

inductive Foo : Nat  Bool  Type
  | ctor (f : Nat  Nat) (n : Nat) (x : Nat) (h : x = f n) : Foo x false

is essentially telling cases to "ignore" the unification problem x =? f n


Last updated: Dec 20 2023 at 11:08 UTC