Zulip Chat Archive
Stream: lean4
Topic: Lists with predicates and proof erasure
Wojciech Nawrocki (Jun 07 2022 at 01:29):
I am wondering about the overhead of proving things about the elements of a list in a context where we have a list l
and know ∀ x ∈ l, P x
. There are some examples of transformations on those in mathlib4. Supposing we have such a predicate, there are at least two ways to use it on every element in a map
operation. One, we can wrap the membership proof in a Sigma-type with
def List.withMems : (l : List α) → List (Σ' (x : α), x ∈ l)
| [] => []
| a :: as =>
⟨a, by simp⟩ :: as.withMems.map' fun ⟨x, hX⟩ => ⟨x, by simp [hX]⟩
and then use it in standard List.map
. For example if we know that every element is green and want to pass it through a function that has greenness as a precondition
def isGreen (n : Nat) : Prop := n = 3
def onlyGreen (n : Nat) (_ : isGreen n) : Nat := n
def greens₁ (l : List Nat) (H : ∀ x ∈ l, isGreen x) : List Nat :=
l.withMems.map fun ⟨n, hN⟩ => onlyGreen n (H n hN)
However this seems to generate suboptimal code since the Σ'
s have a different runtime representation, so withMems
is not a noop and is called first followed by map
, resulting in two loops.
def greens₁ (x_1 : obj) (x_2 : ◾) : obj :=
let x_3 : obj := List.withMems._rarg x_1;
let x_4 : obj := greens₁._closed_1;
let x_5 : obj := List.map'._rarg x_4 x_3;
ret x_5
Note that I used a custom List.map'
to avoid the tail-recursive specialization mapTR
, but this does not seem to matter.
Second, we could write a custom membership-proving map
like
def List.mapMems (l : List α) (f : (x : α) → x ∈ l → β) : List β :=
go l (fun _ => id)
where go : (l' : List α) → (∀ x ∈ l', x ∈ l) → List β
| [], _ => []
| a::as, H =>
have hA : a ∈ l := H a <| List.mem_cons_self _ _
have hAs : ∀ x ∈ as, x ∈ l := List.forall_mem_of_forall_mem_cons H
f a hA :: go as hAs
and then use it directly
def greens₂ (l : List Nat) (H : ∀ x ∈ l, isGreen x) : List Nat :=
l.mapMems fun n hN => onlyGreen n (H n hN)
The code generated here seems very close to standard List.map
and more optimal, although I didn't look at it in too much detail.
Is this problem explored in the literature on compiling dependently-typed languages? How do people deal with it? Is the mapMems
variant (perhaps with a csimp
lemma to compile it to a tail-recursive version) the right solution here? Should the Lean compiler (eventually) be able to compile withMems
to the identity function, assuming that the runtime representation of PSigma
with Prop
can be made just the representation of the value?
Leonardo de Moura (Jun 07 2022 at 01:44):
Should the Lean compiler (eventually) be able to compile withMems to the identity function, assuming that the runtime
Yes, this is on our to-do list, but for Subtype
. That is, one would have to write
def List.withMems : (l : List α) → List { x : α // x ∈ l }
| [] => []
| a :: as =>
⟨a, by simp⟩ :: as.withMems.map' fun ⟨x, hX⟩ => ⟨x, by simp [hX]⟩
Recall that { x : α // x ∈ l }
and α
already have the same representation in our runtime.
Leonardo de Moura (Jun 07 2022 at 01:57):
@Wojciech Nawrocki I just noticed you used a nested map
in your definition. When we considered a similar example, we used the following alternative definition which is easier to detect that withMems
is the identity function at the IR level.
set_option trace.compiler.ir.init true
def List.withMems (as : List α) (as' : List α) (h : (a : α) → a ∈ as' → a ∈ as) : List { x : α // x ∈ as } :=
match as' with
| [] => []
| a :: as' => ⟨a, h a (List.Mem.head ..)⟩ :: withMems as as' fun a h' => h a (List.Mem.tail _ h')
We can also write it as
def List.withMems (as : List α) : List { x : α // x ∈ as } :=
go as as fun _ h => h
where
go (as : List α) (as' : List α) (h : (a : α) → a ∈ as' → a ∈ as) : List { x : α // x ∈ as } :=
match as' with
| [] => []
| a :: as' => ⟨a, h a (List.Mem.head ..)⟩ :: go as as' fun a h' => h a (List.Mem.tail _ h')
Mario Carneiro (Jun 07 2022 at 02:03):
that go
function looks pretty similar to mathlib's docs#list.pmap which generalizes the predicate x \in as
to an arbitrary proposition (which also makes it easier to see that as
is not a computationally relevant input to the function)
Leonardo de Moura (Jun 07 2022 at 02:03):
assuming that the runtime representation of PSigma with Prop can be made just the representation of the value?
This part is not on our to-do list. We currently don't have plans for specializing parametric types. That is, PSigma
is always a pair in our runtime, even if the second argument is a Prop
.
Mario Carneiro (Jun 07 2022 at 02:04):
although I guess the problem with pmap
is that it looks like map
and is not specialized to subtype.mk
meaning that it can be recognized as the identity function
Mario Carneiro (Jun 07 2022 at 02:07):
That is, I would guess that this one would be a good candidate to be recognized as the identity
def pmap' (P : α → Prop) : (as : List α) → ((a : α) → a ∈ as → P a) → List { x : α // P x }
| [], h => []
| a :: as', h => ⟨a, h a (List.Mem.head ..)⟩ :: pmap' P as' fun a h' => h a (List.Mem.tail _ h')
Leonardo de Moura (Jun 07 2022 at 02:07):
Yes, it is.
Leonardo de Moura (Jun 07 2022 at 02:17):
@Wojciech Nawrocki Are you planning to use withMems
to justify termination when using map
and foldl
? If yes, here is another approach we have been considering https://github.com/leanprover/lean4/blob/master/tests/lean/run/combinatorsAndWF.lean
In this approach, we only need to modify the WF module, and the user would be able to just write
inductive Expr where
| app (f : String) (args : List Expr)
| var (n : String)
def Expr.numVars : Expr → Nat
| app f args => args.foldl (init := 0) fun sum arg => sum + numVars arg
| var _ => 1
Wojciech Nawrocki (Jun 07 2022 at 03:30):
Mario Carneiro said:
that
go
function looks pretty similar to mathlib's docs#list.pmap which generalizes the predicatex \in as
to an arbitrary proposition (which also makes it easier to see thatas
is not a computationally relevant input to the function)
Oh indeed, and withMems
is list.attach
.
Leonardo de Moura said:
Wojciech Nawrocki Are you planning to use
withMems
to justify termination when usingmap
andfoldl
?
No, I needed it to typecheck some array accesses using indices obtained from List.range
and a lemma ∀ x ∈ List. range n, x < n
. In this case I could also go with an alternative List.range' : (n : Nat) → List (Fin n)
. It looks like Fin n
gets the subtype treatment and is represented as just the Nat
value.
The WF feature would be great to have regardless, I have ran into the map/fold
termination problem many times.
Wojciech Nawrocki (Jun 07 2022 at 03:31):
Thank you for your answers!
Last updated: Dec 20 2023 at 11:08 UTC