Zulip Chat Archive

Stream: lean4

Topic: Can not unfold definition


Yanning Chen (LightQuantum) (Feb 11 2025 at 03:15):

namespace List

-- List.map, but carrying the proof that the element is in the list
@[specialize]
def mapMem (as : List α) (f : (a : α)  a  as  β) : List β := match as with
  | [] => []
  | a :: as' => f a (.head _) :: as'.mapMem (f · <| ·.tail _)

end List

namespace Std.Range

def toList (r : Range) : List Nat := if r.step = 0 then
   []
  else if r.start < r.stop then
    r.start :: toList { r with start := r.start + r.step }
  else
    []

theorem toList_append (h₁ : l  m) (h₂ : m  n) : [l:m].toList ++ [m:n].toList = [l:n].toList := by sorry

theorem toList_upper (In: i  [0:n].toList): i < n := by sorry

def constructGetN {as : List Nat} : (a : Nat)  a  [0:as.length].toList  Nat :=
  fun i H => as[i]' (toList_upper H)

theorem baseCase : ([0:0].toList.mapMem (@constructGetN [])) = [] := by
  unfold toList   -- failed

I expect this unfold to success, but lean4 says

tactic 'unfold' failed to unfold 'Std.Range.toList' at
  { start := 0, stop := 0, step := 1 }.toList.mapMem constructGetN = []

With pp.all,

tactic 'unfold' failed to unfold 'Std.Range.toList' at
  @Eq.{1} (List.{0} Nat)
    (@List.mapMem.{0, 0} Nat Nat
      (Std.Range.toList
        (Std.Range.mk (@OfNat.ofNat.{0} Nat 0 (instOfNatNat 0)) (@OfNat.ofNat.{0} Nat 0 (instOfNatNat 0))
          (@OfNat.ofNat.{0} Nat 1 (instOfNatNat 1))))
      (@Std.Range.constructGetN (@List.nil.{0} Nat)))
    (@List.nil.{0} Nat)

I don't see any reason why this fails. Could anyone help me with this? Thanks!

My lean4 version is
Lean (version 4.14.0, arm64-apple-darwin23.6.0, commit 410fab728470, Release)

Also, lean playground

Yanning Chen (LightQuantum) (Feb 11 2025 at 03:17):

And here's the proof of toList_append and toList_upper. The problem still exists if you replace the sorrys with the actual proof.

theorem toList_append (h₁ : l  m) (h₂ : m  n) : [l:m].toList ++ [m:n].toList = [l:n].toList := by
  rw [toList, if_neg Nat.one_ne_zero]
  split
  · case isTrue h =>
    apply Eq.symm
    rw [toList, if_neg Nat.one_ne_zero]
    apply Eq.symm
    simp only at *
    rw [if_pos (Nat.lt_of_lt_of_le h h₂), List.cons_append, toList_append (Nat.succ_le_of_lt h) h₂]
  · case isFalse h =>
    simp only at *
    have : l = m := match Nat.lt_trichotomy l m with
      | .inl lltm => h lltm |>.elim
      | .inr (.inl leqm) => leqm
      | .inr (.inr mltn) => Nat.not_lt_of_le h₁ mltn |>.elim
    rw [this, List.nil_append]
termination_by m - l
decreasing_by
  all_goals simp_wf
  apply Nat.sub_succ_lt_self
  assumption

theorem toList_upper (In: i  [0:n].toList): i < n := by
  induction n
  . case zero =>
    simp_all [toList]
  . case succ n l =>
    rw [<- toList_append (m := n)] at In <;> simp_all
    cases In
    . simp_all [Nat.lt_add_right]
    . case inr In =>
      simp [toList] at In
      simp_all

Matt Diamond (Feb 11 2025 at 04:06):

(deleted)

Jovan Gerbscheid (Feb 11 2025 at 14:20):

I looked into it, and this is a bug. If a constant has an "unfold" theorem (generally this is for recursive functions) then unfold will try rewriting with that theorem using simp. But simp doesn't apply simp in dependent arguments (I mean arguments for which rewriting them in a non-defeq way can cause problems) In this case List.mapMem is the function where a later argument depends on the List argument.

Jovan Gerbscheid (Feb 11 2025 at 14:27):

This also fails, as a result of the rewriting theorem being in applied form, so a loose toList won't get unfolded:

namespace List

-- List.map, but carrying the proof that the element is in the list
@[specialize]
def mapMem (as : List α) (f : (a : α)  a  as  β) : List β := match as with
  | [] => []
  | a :: as' => f a (.head _) :: as'.mapMem (f · <| ·.tail _)

end List

namespace Std.Range

def toList (r : Range) : List Nat := if r.step = 0 then
   []
  else if r.start < r.stop then
    r.start :: toList { r with start := r.start + r.step }
  else
    []

theorem baseCase : toList = toList := by
  unfold toList   -- failed

Jovan Gerbscheid (Feb 11 2025 at 14:37):

Actually, this isn't a bug in unfold. It is a result of toList being defined through well-founded recursion. And that means that replacing it by its definition doesn't hold definitionally. So it isn't safe for unfold to replace the definition in the dependent position.

Jovan Gerbscheid (Feb 11 2025 at 14:57):

We have

List.mapMem {α : Type u_1} {β : Type u_2} (as : List α) (f : (a : α)  a  as  β) : List

where f depends on as. simp is smart enought that if the f argument type were to be a proposition, it can simply insert a coercion. However, I'd argue that here, since as appears in a proposition argument of f, Lean should still be able to insert the coercion: if the argument is f, it can be replaced by fun a ha => f a (cast ha). But such an inserted lambda would look a bit strange.

Kim Morrison (Feb 12 2025 at 04:24):

@Yanning Chen (LightQuantum), just making sure you are aware of List.pmap which provides the functionality you're implementing here.

Yanning Chen (LightQuantum) (Feb 12 2025 at 19:02):

Kim Morrison said:

Yanning Chen (LightQuantum), just making sure you are aware of List.pmap which provides the functionality you're implementing here.

I had no idea I could do that! Thank you!

Yanning Chen (LightQuantum) (Feb 12 2025 at 19:26):

Jovan Gerbscheid said:

Actually, this isn't a bug in unfold. It is a result of toList being defined through well-founded recursion. And that means that replacing it by its definition doesn't hold definitionally. So it isn't safe for unfold to replace the definition in the dependent position.

Thank you for your detailed explanation! I completely missed there's a non-trivial termination proof hidden in its definition.

Jovan Gerbscheid said:

We have

List.mapMem {α : Type u_1} {β : Type u_2} (as : List α) (f : (a : α)  a  as  β) : List

where f depends on as. simp is smart enought that if the f argument type were to be a proposition, it can simply insert a coercion. However, I'd argue that here, since as appears in a proposition argument of f, Lean should still be able to insert the coercion: if the argument is f, it can be replaced by fun a ha => f a (cast ha). But such an inserted lambda would look a bit strange.

Could you please elaborate on how this cast works? I thought you could cast the entire function (not just its argument) to argue that this unfold is judgmentally equal, (morally) akin to replacing f with (cast unfold_lemma.mpr <f unfolded>).

Jovan Gerbscheid (Feb 12 2025 at 19:36):

Yes, You can use a cast to cast the type of the funtion itself. But it's generally nicer to avoid casts between types when possible.


Last updated: May 02 2025 at 03:31 UTC