Zulip Chat Archive

Stream: lean4

Topic: Array.Mem


Joe Hendrix (Nov 09 2023 at 18:10):

I saw a curious use of a structure on the Array Membership instance:

/-- `a ∈ as` is a predicate which asserts that `a` is in the array `a s`. -/
-- NB: This is defined as a structure rather than a plain def so that a lemma
-- like `sizeOf_lt_of_mem` will not apply with no actual arrays around.
structure Mem (a : α) (as : Array α) : Prop where
  val : a  as.data

instance : Membership α (Array α) where
  mem a as := Mem a as

...

theorem sizeOf_lt_of_mem [SizeOf α] {as : Array α} (h : a  as) : sizeOf a < sizeOf as := by
  ...

Is this use of a structure still needed? I noticed other Membership instances don't always use a structure.

Mario Carneiro (Nov 09 2023 at 18:10):

This was a recent change

Mario Carneiro (Nov 09 2023 at 18:11):

lean4#2774

Joe Hendrix (Nov 09 2023 at 18:12):

Thanks. I should have checked git blame before asking.

Notification Bot (Nov 09 2023 at 18:12):

Joe Hendrix has marked this topic as resolved.

Mario Carneiro (Nov 09 2023 at 18:13):

I'm still not sure whether a structure is the right solution here, we don't normally use structures for this

Mario Carneiro (Nov 09 2023 at 18:13):

e.g. instance : LT (Fin n) is just a def

Notification Bot (Nov 09 2023 at 18:13):

Joe Hendrix has marked this topic as unresolved.

Joe Hendrix (Nov 09 2023 at 18:15):

I just stumbled across this class while working through potential definitions for a Vector class, so it's not blocking me, but it'd be good to understand the issue a bit better so I do the right thing for Vector.

Mario Carneiro (Nov 09 2023 at 18:15):

The rationale given is:

The definition for a ∈ as is intentionally not defeq to a ∈ as.data so that the termination tactics for Arrays don’t spuriously apply when recursing through lists.

but I don't see why this is a problem, and/or why fixing the relevant tactics doesn't work

Mario Carneiro (Nov 09 2023 at 18:15):

@Joachim Breitner

Joe Hendrix (Nov 09 2023 at 18:16):

There's a comment in lean4#2774 with a bit more detail, but I think it'd be great to have some clarity.

Joachim Breitner (Nov 09 2023 at 18:16):

Maybe fixing the tactic would work; I didn’t dare to touch the List stuff when I was dabbling with Array. If someone has a better plan, please by all means implement it.

Joachim Breitner (Nov 09 2023 at 18:18):

The problem was that

macro "array_mem_dec" : tactic =>
  `(tactic| first
    | apply Array.sizeOf_lt_of_mem; assumption; done
    | apply Nat.lt_trans (Array.sizeOf_lt_of_mem ?h)
      case' h => assumption
      simp_arith)

should look for a ∈ as in the context. If that’s defeq to a ∈ as.data, it would match x ∈ xs (here xs being a normal list, no arrays around)

Mario Carneiro (Nov 09 2023 at 18:18):

More specifically, is there a test case that fails without this?

Joachim Breitner (Nov 09 2023 at 18:18):

Yes, otherwise I wouldn't even have noticed :-)

Mario Carneiro (Nov 09 2023 at 18:19):

was it checked in?

Joachim Breitner (Nov 09 2023 at 18:19):

It was already there

Joachim Breitner (Nov 09 2023 at 18:19):

tests/lean/run/wfForIn.lean for example, based on my comments at the PR

Joachim Breitner (Nov 09 2023 at 18:20):

Maybe I did something wrong, of course. OR maybe a def with some annotation that prevents reduction would be more idiomatic.

Mario Carneiro (Nov 09 2023 at 18:25):

MWE:

structure Array' (α : Type u) where
  data : List α

namespace Array'

def size (as : Array' α) : Nat := as.data.length
def get (as : Array' α) (i : Fin as.size) : α := as.data.get i

def Mem (a : α) (as : Array' α) : Prop := a  as.data

instance : Membership α (Array' α) where
  mem a as := Mem a as

theorem sizeOf_lt_of_mem [SizeOf α] {as : Array' α} (h : a  as) : sizeOf a < sizeOf as :=
  sorry

@[simp] theorem sizeOf_get [SizeOf α] (as : Array' α) (i : Fin as.size) : sizeOf (as.get i) < sizeOf as :=
  sorry

macro "array'_mem_dec" : tactic =>
  `(tactic| first
    | apply Array'.sizeOf_lt_of_mem; assumption; done
    | apply Nat.lt_trans (Array'.sizeOf_lt_of_mem ?h)
      case' h => assumption
      simp_arith)

macro_rules | `(tactic| decreasing_trivial) => `(tactic| array'_mem_dec)

inductive Term where
  | app (f : String) (args : List Term)

def printFns : Term  IO Unit
  | Term.app f args => do
    IO.println f
    for h : arg in args do
      have : sizeOf arg < 1 + sizeOf f + sizeOf args := Nat.lt_trans (List.sizeOf_lt_of_mem h) (by simp_arith)
      printFns arg

Joachim Breitner (Nov 09 2023 at 18:28):

Thanks for MWE’ing.

@[irreducible]
def Mem (a : α) (as : Array' α) : Prop := a  as.data

seems to help. Is that preferable?

Mario Carneiro (Nov 09 2023 at 18:29):

Alternatively:

macro "array'_mem_dec" : tactic =>
  `(tactic| first
    | apply Array'.sizeOf_lt_of_mem; assumption; done
    | with_reducible apply Nat.lt_trans (Array'.sizeOf_lt_of_mem _›)
      simp_arith)

Eric Wieser (Nov 09 2023 at 18:33):

Should that be with_reducible assumption?

Mario Carneiro (Nov 09 2023 at 18:35):

you could also put with_reducible first ...

Mario Carneiro (Nov 09 2023 at 18:35):

I figure for the first case it doesn't matter though since if it closed the goal then no problemo

Joe Hendrix (Nov 09 2023 at 18:36):

What sort of example is in mind for the Nat.lt_trans case?

Mario Carneiro (Nov 09 2023 at 18:37):

the example at the end is pretty similar to what kind of thing would require that case

Mario Carneiro (Nov 09 2023 at 18:37):

you prove sizeOf arg < 1 + sizeOf f + sizeOf args by transitivity on sizeOf arg < sizeOf args and sizeOf args <= 1 + sizeOf f + sizeOf args and solve the latter with simp_arith

Joe Hendrix (Nov 09 2023 at 18:37):

Yep that makes sense.

Joe Hendrix (Nov 09 2023 at 18:39):

I think ideally there would be some sort of chaining where a linear arithmetic procedure queried for additional facts about the terms it treats as uninterpreted variable, but that's out of scope for now.

Mario Carneiro (Nov 09 2023 at 18:40):

I think what happens when this mistakenly applies in the list case is that you instead get the subgoal sizeOf { data := args } <= 1 + sizeOf f + sizeOf args (where the LHS was able to unify because of eta for structures) and sizeOf { data := args } is larger, I think 1 + sizeOf args, which makes it harder to prove

Mario Carneiro (Nov 09 2023 at 18:41):

IMO simp_arith is overkill here, it should be something more tailored to this kind of goal

Joe Hendrix (Nov 09 2023 at 18:41):

I think for now, I'd recommend using @Mario Carneiro's tactic. That's what I'll add for Vector.

Mario Carneiro (Nov 09 2023 at 18:42):

the lean3 version of this tactic would just try x <= x + y and y <= x + y with backtracking

Mario Carneiro (Nov 09 2023 at 18:43):

but maybe simp_arith is better suited to the case where the user overrides the decreasing metric and now you have more interesting and arbitrary goals

Joachim Breitner (Nov 09 2023 at 18:57):

My (preliminary) impression is that the termination tactics are not quite compositional, and may need an overhaul. For example, what if I nest multiple data structures; list of list of list. I’d expect the default tactic setup to use all the List.Mem assumptions and still arrive at the result. But unless I am mistaken, the current setup of decreasing_trivial is that is handles a finite set of typical cases, but only if it can solve them completely.

Or maybe once we have omega the the tactic will first make all the < facts known (by changing List.Mem assumptions to <) and then simply call omega.

Joe Hendrix (Nov 09 2023 at 18:59):

Yeah, I think the current approach is too specific. It'd be great to have a fast omega tactic with a mechanism for pulling < facts out of the environment (using lemmas such as sizeOf_lt_of_mem automatically).

Joe Hendrix (Nov 09 2023 at 19:04):

At least in the example above, forward chaining would probably provide be a good approach by seeing arg ∈ args in the hypotheses and automatically synthesizing sizeOf arg < sizeOf args.

Are there existing general forward chaining tactics in Lean?

Mario Carneiro (Nov 09 2023 at 19:10):

there is apply at in mathlib, we could use apply thm at * to mean forward chaining

Mario Carneiro (Nov 09 2023 at 19:12):

usually have is used for forward chaining but it isn't easy to say

for $h in context do
  try
    have := thm $h

Last updated: Dec 20 2023 at 11:08 UTC