Zulip Chat Archive

Stream: Program verification

Topic: Lists Without Duplicates


Henning Dieterichs (Dec 30 2020 at 20:24):

I want to state that (f x) ++ (g x): List α has no duplicates and that ((f x) ++ (g x).to_finset = h x (maybe there is some notion of a disjoint_finset?).
Later, I would like to argue that if g x = y::ys, then y \not \in ys ++ f x.
What is the best way to formalize that? I thought about stating (f x ++ g x).length = (h x).card. Is there a better way?

Kevin Buzzard (Dec 30 2020 at 20:27):

This is a question about lists, right? Nothing to do with finsets. The way you prove the theorem will depend on how you are saying that L++M has no dupes.

Kevin Buzzard (Dec 30 2020 at 20:30):

Then you just need theorems like nodupes(L++M) iff nodupes(M++L) and nodupes(a::L) -> a not in L.

Henning Dieterichs (Dec 30 2020 at 20:30):

I would like to optimize the formalization for later arguments, not for provability of the theorem itself (I guess it will be easy to prove, but I have to use this argument many times). Well, it is about lists, but there is a connection to finsets. h x is a finset, but I need the order in f x.

Kevin Buzzard (Dec 30 2020 at 20:31):

Sure but h x never occurred in your question

Yakov Pechersky (Dec 30 2020 at 20:31):

Use can use nodup for the first constraint.

Yakov Pechersky (Dec 30 2020 at 20:32):

And there a lot of lemmas about how nodup interfaces with append, concatenate, and mem.

Kevin Buzzard (Dec 30 2020 at 20:32):

Great, if there's a docs#list.nodup that would be perfect

Henning Dieterichs (Dec 30 2020 at 20:33):

oh, nice thanks! Sorry that I missed that.

Kevin Buzzard (Dec 30 2020 at 20:35):

You could look for the lemmas I'm suggesting with library_search and then prove nodup (L++(m::M)) implies m isn't in M++L, although I guess I'm a bit surprised that you have swapped L and M.

Yakov Pechersky (Dec 30 2020 at 20:38):

I haven't had good luck finding the right mem lemmas via library_search, but have found them via suggest.

Henning Dieterichs (Dec 30 2020 at 20:46):

In my case, library_searched worked great!

Henning Dieterichs (Dec 30 2020 at 20:48):

I guess this is the difference between math and program verification:
image.png :D

Kevin Buzzard (Dec 30 2020 at 20:53):

We get really long goals like that, it's just that we don't notice they're long because everything is folded up into typeclasses.

Henning Dieterichs (Dec 30 2020 at 20:57):

How do you stay concentrated when looking at such goals? I tend to get lost in the sheer amount of identifiers. If I'm not very careful, I lose the idea of the proof and it gets very ugly.

Marc Huisinga (Dec 30 2020 at 21:33):

@Henning Dieterichs i ran into a similar problem during my bsc thesis (which was also cs-oriented), also struggling to keep up with the goals.
i found that two things help:

  • when defining some inductive/definition, start by proving "obvious" facts about the inductive/definition, regardless of whether you think you will need it down the line.
  • work through longer proofs with pen and paper and denote intuitive subgoals in your paper proof as lemmas, proving them separately. then, write out the big proof in dependence of all these sorry'd lemmas.

your pen and paper proofs should be precise so that you don't need to do so much work for the formalized proof.
in my experience, without such a pen and paper proof, you can get lost very easily, especially when you start applying theorems and simplification like a human pattern matcher without looking at the bigger picture.
meanwhile, the first bullet point means that you won't have to unfold as many layers, leading to more concise goals.

Marc Huisinga (Dec 30 2020 at 21:34):

in the end, i didn't follow those tips nearly well enough myself, which is part of the reason why i couldn't conclude the proof in its entirety.

Henning Dieterichs (Dec 30 2020 at 21:43):

thanks for your advice! Except for the pen&paper proof, I'm already trying to do that. I'm very optimistic to be able to close all my goals, but I'm sure many of my proofs are very bloated and look like brute force.

Yakov Pechersky (Dec 30 2020 at 21:43):

Also, let ... in ... don't simplify cleanly often, so I just avoid using them in definitions often

Yakov Pechersky (Dec 30 2020 at 21:45):

Here, you have a big goal because it's not simplifying the defeq structure record accession

Reid Barton (Dec 30 2020 at 23:00):

This screenshot looks like either not enough abstraction (e.g. some append operation on whatever the structures are) or too much unfolding rather than applying lemmas

Eric Wieser (Dec 31 2020 at 09:56):

You have a lot of subexpressions there of the form {field1 := f, ...}.field1, which simplify to f (perhaps via dsimp only)

Henning Dieterichs (Dec 31 2020 at 15:21):

I struggled to solve this goal with the definition of nodup:

disjoint: disjoint ant_tr1.leaves ant_tr2.leaves,
ant_ih_tr1 :
  ((R ant_tr1).acc ++ (R ant_tr1).inacc ++ (R ant_tr1).red).to_finset = ant_tr1.leaves 
    ((R ant_tr1).acc ++ (R ant_tr1).inacc ++ (R ant_tr1).red).nodup,
ant_ih_tr2 :
  ((R ant_tr2).acc ++ (R ant_tr2).inacc ++ (R ant_tr2).red).to_finset = ant_tr2.leaves 
    ((R ant_tr2).acc ++ (R ant_tr2).inacc ++ (R ant_tr2).red).nodup
 ((R ant_tr1).acc ++ (R ant_tr2).acc ++ ((R ant_tr1).inacc ++ (R ant_tr2).inacc) ++
     ((R ant_tr1).red ++ (R ant_tr2).red)).nodup

There is list.nodup_append_comm, but the simplifier couldn't make use of it. Manually applying the lemmas to rearrange the terms was too hard for me.

This custom theorem helped a lot: theorem nodup_iff_card { α: Type } [decidable_eq α] (a: list α): a.nodup ↔ a.length = a.to_finset.card. Maybe it's a good candidate for mathlib? There is list.to_finset_card_of_nodup, but it is one direction only.
It was much easier to show (a ++ b ++ c ++ ...).length = (a ++ b ++ c ++ ...).to_finset.card, since nat.add and finset.union are commutative and the simplifier can solve goals where only assoc and comm rules are needed.

Henning Dieterichs (Dec 31 2020 at 15:21):

@Eric Wieser dsimp only helps! I just used unfold before.

Henning Dieterichs (Dec 31 2020 at 15:29):

Reid Barton said:

This screenshot looks like either not enough abstraction (e.g. some append operation on whatever the structures are) or too much unfolding rather than applying lemmas

I felt the same (not only here but for many of my proofs). However, I have no clue how to improve that. At that point, I didn't unfold much and I don't know what new lemmas could help me to get more insight.

This is an except of what I'm working on:

structure LeafPartition := mk :: (acc : list Leaf) (inacc : list Leaf) (red : list Leaf)

inductive Ant (α: Type)
| leaf (a: α) (leaf: Leaf): Ant
| branch (tr1: Ant) (tr2: Ant): Ant
| diverge (a: α) (tr: Ant): Ant

def R : Ant bool  LeafPartition
| (Ant.leaf can_prove_empty n) := if can_prove_empty then  [], [], [n]  else  [n], [], [] 
| (Ant.diverge can_prove_empty tr) :=
    match R tr, can_prove_empty with
    |  [], [], m :: ms ⟩, ff :=  [], [m], ms 
    | r, _ := r
    end
| (Ant.branch tr1 tr2) :=
    let r1 := R tr1, r2 := R tr2 in
         r1.acc ++ r2.acc, r1.inacc ++ r2.inacc, r1.red ++ r2.red 

I want to prove that the result is indeed a partition of all leaves, if the leaves of every branch in ant are disjoint.
The goal above is for the branch case.

Yakov Pechersky (Dec 31 2020 at 15:30):

What does using finset give you? That two lists are the same if you remove duplicates and forget about order?

Yakov Pechersky (Dec 31 2020 at 15:38):

What's Leaf defined as?

Henning Dieterichs (Dec 31 2020 at 15:38):

Leaf is an arbitrary Type

Yakov Pechersky (Dec 31 2020 at 15:40):

The code above currently gives me

don't know how to synthesize placeholder
context:
 Ant bool  Type

in the definition of R

Henning Dieterichs (Dec 31 2020 at 15:41):

Sorry, it is not a MWE... Creating a MWE would be hard. The code was just to give context.
Maybe adding [decidable_eq Leaf] to R fixes this, I have this globally.

Henning Dieterichs (Dec 31 2020 at 15:41):

What does using finset give you? That two lists are the same if you remove duplicates and forget about order?

I know that as ++ bs ++ cs ++ ds has no duplicates. I want to show that ds ++ bs ++ cs ++ as has no duplicates (it's just a permutation - that should be trivial to show).
However, this is difficult, as I cannot automatically rearrange the terms - after all, permutating the lists changes them. (This is a step of my proof in the branch case)

BUT, it is easy to prove with lean that (ds ++ bs ++ cs ++ as).length = (as ++ bs ++ cs ++ ds).length and that (ds ++ bs ++ cs ++ as).to_finset = (as ++ bs ++ cs ++ ds).to_finset! (Since length and to_finset is some kind of homomorphism into a commutative monoid)

Yakov Pechersky (Dec 31 2020 at 15:45):

docs#list.perm.nodup_iff

Henning Dieterichs (Dec 31 2020 at 15:47):

How difficult would it be to convince lean that as ++ bs ++ cs ++ ds is a permutation of ds ++ bs ++ cs ++ as?

example { α: Type } (as bs cs ds: list α): (as ++ bs ++ cs ++ ds) ~ (ds ++ bs ++ cs ++ as) :=
begin
    sorry,
end

Yakov Pechersky (Dec 31 2020 at 15:48):

One min, checking that

Henning Dieterichs (Dec 31 2020 at 15:50):

I think lean's automation really likes equalities. In my limited experience, definitions that are made out of equalities are much easier to reason about than definitions that involve recursively defined predicates

Eric Wieser (Dec 31 2020 at 16:06):

I assume you've already found docs#list.perm_append_comm

Henning Dieterichs (Dec 31 2020 at 16:07):

there is also comm for nodup directly, but is hard to apply manually

Henning Dieterichs (Dec 31 2020 at 16:12):

I think the key to this kind of problem is that

a~a' -> b ~ b' -> (a ++ b) ~ (a' ++ b')

(otherwise you could not really apply perm_append_comm recursively)
...which states that ++ is well-defined on the equivalence class of permutations. But this just yields union on multisets.
And if you remove duplicates, you get finsets, so maybe it might be easier to work with finsets directly?

Eric Wieser (Dec 31 2020 at 16:12):

Working with finsets directly is definitely the way to go here

Eric Wieser (Dec 31 2020 at 16:13):

That lemma is docs#list.perm.append

Yakov Pechersky (Dec 31 2020 at 16:15):

Yeah but that lemma won't work because of how the terms are associated.

Yakov Pechersky (Dec 31 2020 at 16:15):

and there's a missing @[trans] lemma for perms.

Kevin Buzzard (Dec 31 2020 at 16:18):

I would just use the comm and assoc lemmas and swap stuff by hand. You could even train the simplifier to do this

Eric Wieser (Dec 31 2020 at 16:19):

Here's a full proof using those:

example { α: Type } (as bs cs ds: list α) : (as ++ bs ++ cs ++ ds) ~ (ds ++ bs ++ cs ++ as) :=
calc (as ++ bs ++ cs ++ ds) ~ as ++ (bs ++ cs ++ ds) : by simp only [list.append_assoc]
                        ... ~ (bs ++ cs ++ ds) ++ as : list.perm_append_comm
                        ... ~ (bs ++ (cs ++ ds)) ++ as : by simp only [list.append_assoc]
                        ... ~ (bs ++ (ds ++ cs)) ++ as : list.perm.append_right _ $ list.perm.append_left _ list.perm_append_comm
                        ... ~ (bs ++ ds ++ cs) ++ as : by simp only [list.append_assoc]
                        ... ~ (ds ++ bs ++ cs ++ as) : list.perm.append_right _ $ list.perm.append_right _ list.perm_append_comm

Kevin Buzzard (Dec 31 2020 at 16:19):

Now get the simplifier to do it!

Eric Wieser (Dec 31 2020 at 16:20):

But the simplifier can't solve associativity / commutativity goals?

Henning Dieterichs (Dec 31 2020 at 16:20):

In my case though, I have 6 variables (⟨ r1.acc ++ r2.acc, r1.inacc ++ r2.inacc, r1.red ++ r2.red ⟩) :D So without automation it gets really hard

Eric Wieser (Dec 31 2020 at 16:20):

That's what tactic#ac_refl is for, right?

Eric Wieser (Dec 31 2020 at 16:20):

Except that only works for equality

Kevin Buzzard (Dec 31 2020 at 16:20):

I would definitely try the simplifier.

Yakov Pechersky (Dec 31 2020 at 16:20):

It's just key to prove

lemma perm_foldl_append_iff {b : list α} {l l' : list (list α)} :
  foldl append b l ~ foldl append b l'  l ~ l' :=

Eric Wieser (Dec 31 2020 at 16:20):

You can take the quotients of lists under permutations to turn your ~ into an =

Eric Wieser (Dec 31 2020 at 16:20):

But then you have multisets

Kevin Buzzard (Dec 31 2020 at 16:21):

You tell it comm and assoc and left_comm and it will then just put things into its favourite order. I don't know if it'll work but it's worth a try.

Kevin Buzzard (Dec 31 2020 at 16:23):

Failing that, abel should be generalised to all operations, not just addition, and perhaps it should do this too

Marc Huisinga (Dec 31 2020 at 16:25):

not sure if relevant, but i've found docs#list.perm_iff_count very useful for working with perm

Eric Wieser (Dec 31 2020 at 16:27):

Here's how to use abel:

example { α: Type } (as bs cs ds: list α) : (as ++ bs ++ cs ++ ds) ~ (ds ++ bs ++ cs ++ as) :=
begin
  rw multiset.coe_eq_coe,
  simp only [multiset.coe_add],
  abel,
end

Eric Wieser (Dec 31 2020 at 16:30):

I wonder if docs#multiset.coe_add simplifies in the wrong direction

Eric Wieser (Dec 31 2020 at 16:30):

It seems like it's much easier to work with multisets than permutations of lists

Mario Carneiro (Dec 31 2020 at 16:36):

example {α} (as bs cs ds : list α) : (as ++ bs ++ cs ++ ds).nodup  (ds ++ bs ++ cs ++ as).nodup :=
begin
  apply list.perm.nodup_iff,
  rw  multiset.coe_eq_coe,
  simp only [ multiset.coe_add, add_left_comm, add_comm],
end

Mario Carneiro (Dec 31 2020 at 16:36):

oops, too late

Mario Carneiro (Dec 31 2020 at 16:39):

I don't think coe_add is the wrong direction. Usually you would use the simplifier on a multiset goal after rcases to make it about lists and it will turn everything into lists

Mario Carneiro (Dec 31 2020 at 16:40):

this problem is a little unusual in that you want to add an additional wrapping layer rather than remove one

Henning Dieterichs (Dec 31 2020 at 16:41):

Mario Carneiro said:

example {α} (as bs cs ds : list α) : (as ++ bs ++ cs ++ ds).nodup  (ds ++ bs ++ cs ++ as).nodup :=
begin
  apply list.perm.nodup_iff,
  rw  multiset.coe_eq_coe,
  simp only [ multiset.coe_add, add_left_comm, add_comm],
end

Which is almost the same as this:

theorem nodup_iff_card { α: Type } [decidable_eq α] (a: list α): a.nodup  a.length = a.to_finset.card :=
begin
    sorry,
end

example {α} [decidable_eq α] (as bs cs ds : list α) : (as ++ bs ++ cs ++ ds).nodup  (ds ++ bs ++ cs ++ as).nodup :=
begin
  rw nodup_iff_card,
  rw nodup_iff_card,
  simp [finset.union_comm, finset.union_left_comm, nat.add_comm, nat.add_left_comm],
end

Mario Carneiro (Dec 31 2020 at 16:43):

except it has one fewer sorry :)

Henning Dieterichs (Dec 31 2020 at 16:45):

I'm calling it a day now. Thank you for all your help in 2020 and see you in 2021! I wish you a happy new year ;)

Kevin Buzzard (Dec 31 2020 at 16:50):

You won't be doing Lean to summon in the new year??

Henning Dieterichs (Dec 31 2020 at 16:50):

sorry,

Yakov Pechersky (Dec 31 2020 at 17:28):

@Henning Dieterichs
These are the sorts of definitional lemmas I would make:

import data.list.basic

variables {Leaf : Type} [decidable_eq Leaf]

structure LeafPartition (α : Type) :=
(acc : list α := [])
(inacc : list α := [])
(red : list α := [])

inductive Ant (α β : Type)
| leaf (a : α) (β : Leaf) : Ant
| branch (tr1 : Ant) (tr2 : Ant) : Ant
| diverge (a : α) (tr : Ant) : Ant

open Ant

def R : Ant bool Leaf  LeafPartition Leaf
| (leaf bb l) := ite bb {red := [l], ..} {acc := [l], ..}
| (diverge bb tr) := ite bb (R tr) (
    match R tr with
      | ⟨[], [], m :: ms := {inacc := [m], red := ms, ..}
      | _ := R tr
    end)
| (branch x y) := {
    acc := (R x).acc ++ (R y).acc,
    inacc := (R x).inacc ++ (R y).inacc,
    red := (R x).red ++ (R y).red
  }

lemma R_leaf_true (l : Leaf) : R (leaf tt l) = {red := [l], ..} := rfl
lemma R_leaf_false (l : Leaf) : R (leaf ff l) = {acc := [l], ..} := rfl
lemma R_branch (x y : Ant bool Leaf) : R (branch x y) =
  { acc := (R x).acc ++ (R y).acc, inacc := (R x).inacc ++ (R y).inacc,
    red := (R x).red ++ (R y).red } := rfl
lemma R_diverge_true (tr : Ant bool Leaf) : R (diverge tt tr) = R tr := rfl
lemma R_diverge_false_cons {tr : Ant bool Leaf} {m : Leaf} {ms : list Leaf}
  (h : R tr = {red := m :: ms, ..}) :
  R (diverge ff tr) = {inacc := [m], red := ms, ..} :=
by simp [R, h]
lemma R_diverge_false_not {tr : Ant bool Leaf} {m : Leaf} {ms : list Leaf}
  (h : (R tr).red = []  (R tr).acc  []  (R tr).inacc  []) :
  R (diverge ff  tr) = R tr :=
begin
  rcases hr : (R tr) with _ | hac, tlac⟩, _ | hin, tlin⟩, _ | hre, tlre⟩⟩;
  simpa [hr] using h <|> simp [R, hr],
end

Mario Carneiro (Dec 31 2020 at 17:49):

you should use tt and ff instead of true and false

Yakov Pechersky (Dec 31 2020 at 17:52):

Fixed. In general, there'd likely be some way to have it be over Prop and not bool, right?

Mario Carneiro (Dec 31 2020 at 17:53):

Well R itself is over bool so the lemmas should be too

Mario Carneiro (Dec 31 2020 at 17:53):

if it used Prop though your cases should use p -> ... and \neg p -> ... instead of true and false

Yakov Pechersky (Dec 31 2020 at 17:59):

Right, I meant that R itself might be over Prop. Since the original example had the bools named as can_prove_empty

Mario Carneiro (Dec 31 2020 at 18:00):

Ant has three type arguments btw, that seems odd

Mario Carneiro (Dec 31 2020 at 18:00):

the beta argument is unused

Yakov Pechersky (Dec 31 2020 at 18:11):

Ah, I meant

inductive Ant (α β : Type)
| leaf (a : α) (b : β) : Ant
| branch (tr1 : Ant) (tr2 : Ant) : Ant
| diverge (a : α) (tr : Ant) : Ant

Yakov Pechersky (Dec 31 2020 at 18:12):

Anyway, it was an attempt to make the example shared by the other poster above compile, just to show examples of the structure default values and the definitional lemmas.

Mario Carneiro (Dec 31 2020 at 18:12):

Here's a proof of something that I think roughly approximates the goal:

import data.multiset.basic

variables {Leaf : Type}

structure LeafPartition (α : Type) :=
(acc : list α := [])
(inacc : list α := [])
(red : list α := [])

inductive Ant (α β : Type)
| leaf (a : α) (b : β) : Ant
| branch (tr1 : Ant) (tr2 : Ant) : Ant
| diverge (a : α) (tr : Ant) : Ant

open Ant

def R : Ant bool Leaf  LeafPartition Leaf
| (leaf bb l) := ite bb {red := [l], ..} {acc := [l], ..}
| (diverge bb tr) := ite bb (R tr) (
    match R tr with
      | ⟨[], [], m :: ms := {inacc := [m], red := ms, ..}
      | _ := R tr
    end)
| (branch x y) := {
    acc := (R x).acc ++ (R y).acc,
    inacc := (R x).inacc ++ (R y).inacc,
    red := (R x).red ++ (R y).red
  }

def leaves : Ant bool Leaf  list Leaf
| (leaf bb l) := [l]
| (diverge bb tr) := leaves tr
| (branch x y) := leaves x ++ leaves y

section
local attribute [simp] leaves R add_left_comm add_comm
theorem R_perm (a : Ant bool Leaf) : leaves a ~ (R a).acc ++ (R a).inacc ++ (R a).red :=
begin
  rw  multiset.coe_eq_coe, simp [ multiset.coe_add],
  induction a,
  { simp; split_ifs; simp },
  { simp [ multiset.coe_add, *] },
  { simp [-multiset.coe_eq_coe], split_ifs; simp [ multiset.coe_add, *],
    rcases hr : (R a_tr) with _ | hac, tlac⟩, _ | hin, tlin⟩, _ | hre, tlre⟩⟩;
    refl }
end
end

Mario Carneiro (Dec 31 2020 at 18:13):

the proof is a bit bombastic, it just cases on everything

Henning Dieterichs (Jan 01 2021 at 11:25):

Mario Carneiro said:

Here's a proof of something that I think roughly approximates the goal:

...
theorem R_perm (a : Ant bool Leaf) : leaves a ~ (R a).acc ++ (R a).inacc ++ (R a).red :=
begin
  rw  multiset.coe_eq_coe, simp [ multiset.coe_add],
  induction a,
  { simp; split_ifs; simp },
  { simp [ multiset.coe_add, *] },
  { simp [-multiset.coe_eq_coe], split_ifs; simp [ multiset.coe_add, *],
    rcases hr : (R a_tr) with _ | hac, tlac⟩, _ | hin, tlin⟩, _ | hre, tlre⟩⟩;
    refl }
end
...

Nice :flushed: I didn't think such a short proof could do it.
I wonder though, I thought simp should, in general, be only used for closing goals?

Marc Huisinga (Jan 01 2021 at 14:04):

Henning Dieterichs said:

I wonder though, I thought simp should, in general, be only used for closing goals?

if you want to use simp in the middle of a proof (e.g. because the subgoal is extremely verbose), one way to make simp less brittle is to run squeeze_simp instead and then paste the output for a simp only call. this can also greatly boost performance (or at least it used to).

Henning Dieterichs (Jan 01 2021 at 18:33):

Thanks for pointing that out. I often wondered which lemmas simp used.

Bryan Gin-ge Chen (Jan 01 2021 at 18:50):

For more low-level output you can also turn on set_option trace.simplify true, see the start of the community page on the simplifier.


Last updated: Dec 20 2023 at 11:08 UTC