Library PLF.Maps
Maps: Total and Partial Maps
The Coq Standard Library
From Stdlib Require Import Arith.
From Stdlib Require Import Bool.
From Stdlib Require Export Strings.String.
From Stdlib Require Import FunctionalExtensionality.
From Stdlib Require Import List.
Import ListNotations.
Set Default Goal Selector "!".
Documentation for the standard library can be found at
https://rocq-prover.org/doc/V9.0.0/stdlib/index.html.
The Search command is a good way to look for theorems involving
objects of specific types. See Lists for a reminder of how
to use it.
If you want to find out how or where a notation is defined, the
Locate command is useful. For example, where is the natural
addition operation defined in the standard library?
Locate "+".
(There are several uses of the + notation, but only one for
naturals.)
Print Init.Nat.add.
We'll see some more uses of Locate in the Imp chapter.
Identifiers
We will often use a few basic properties of string equality...
Check String.eqb_eq :
∀ n m : string, (n =? m)%string = true ↔ n = m.
Check String.eqb_neq :
∀ n m : string, (n =? m)%string = false ↔ n ≠ m.
Check String.eqb_spec :
∀ x y : string, reflect (x = y) (String.eqb x y).
∀ n m : string, (n =? m)%string = true ↔ n = m.
Check String.eqb_neq :
∀ n m : string, (n =? m)%string = false ↔ n ≠ m.
Check String.eqb_spec :
∀ x y : string, reflect (x = y) (String.eqb x y).
Total Maps
Intuitively, a total map over an element type A is just a
function that can be used to look up strings, yielding As.
The function t_empty yields an empty total map, given a default
element; this map always returns the default element when applied
to any string.
More interesting is the map-updating function, which (as always)
takes a map m, a key x, and a value v and returns a new map
that takes x to v and takes every other key to whatever m
does. The novelty here is that we achieve this effect by wrapping
a new function around the old one.
Definition t_update {A : Type} (m : total_map A)
(x : string) (v : A) :=
fun x' ⇒ if String.eqb x x' then v else m x'.
This definition is a nice example of higher-order programming:
t_update takes a function m and yields a new function
fun x' ⇒ ... that behaves like the desired map.
For example, we can build a map taking strings to bools, where
"foo" and "bar" are mapped to true and every other key is
mapped to false, like this:
Next, let's introduce some notations to facilitate working with
maps.
First, we use the following notation to represent an empty total
map with a default value.
Notation "'_' '!->' v" := (t_empty v)
(at level 100, right associativity).
Example example_empty := (_ !-> false).
(at level 100, right associativity).
Example example_empty := (_ !-> false).
We next introduce a convenient notation for extending an existing
map with a new binding.
The examplemap above can now be defined as follows:
This completes the definition of total maps. Note that we
don't need to define a find operation on this representation of
maps because it is just function application!
Example update_example1 : examplemap' "baz" = false.
Proof. reflexivity. Qed.
Example update_example2 : examplemap' "foo" = true.
Proof. reflexivity. Qed.
Example update_example3 : examplemap' "quux" = false.
Proof. reflexivity. Qed.
Example update_example4 : examplemap' "bar" = true.
Proof. reflexivity. Qed.
When we use maps in later chapters, we'll need several fundamental
facts about how they behave.
Even if you don't bother to work the following exercises,
make sure you thoroughly understand the statements of the
lemmas!
(Some of the proofs require the functional extensionality axiom,
which was discussed in the Logic chapter.)
First, the empty map returns its default element for all keys:
Exercise: 1 star, standard, optional (t_apply_empty)
☐
Next, if we update a map m at a key x with a new value v
and then look up x in the map resulting from the update, we
get back v:
Exercise: 2 stars, standard, optional (t_update_eq)
☐
On the other hand, if we update a map m at a key x1 and then
look up a different key x2 in the resulting map, we get the
same result that m would have given:
Exercise: 2 stars, standard, optional (t_update_neq)
Theorem t_update_neq : ∀ (A : Type) (m : total_map A) x1 x2 v,
x1 ≠ x2 →
(x1 !-> v ; m) x2 = m x2.
Proof.
Admitted.
☐
If we update a map m at a key x with a value v1 and then
update again with the same key x and another value v2, the
resulting map behaves the same (gives the same result when applied
to any key) as the simpler map obtained by performing just
the second update on m:
Exercise: 2 stars, standard, optional (t_update_shadow)
Lemma t_update_shadow : ∀ (A : Type) (m : total_map A) x v1 v2,
(x !-> v2 ; x !-> v1 ; m) = (x !-> v2 ; m).
Proof.
Admitted.
☐
Given strings x1 and x2, we can use the tactic
destruct (eqb_spec x1 x2) to simultaneously perform case
analysis on the result of String.eqb x1 x2 and generate
hypotheses about the equality (in the sense of =) of x1 and
x2. With the example in chapter IndProp as a template,
use String.eqb_spec to prove the following theorem, which states
that if we update a map to assign key x the same value as it
already has in m, then the result is equal to m:
Exercise: 2 stars, standard (t_update_same)
☐
Similarly, use String.eqb_spec to prove one final property of
the update function: If we update a map m at two distinct
keys, it doesn't matter in which order we do the updates.
Exercise: 3 stars, standard, especially useful (t_update_permute)
Theorem t_update_permute : ∀ (A : Type) (m : total_map A)
v1 v2 x1 x2,
x2 ≠ x1 →
(x1 !-> v1 ; x2 !-> v2 ; m)
=
(x2 !-> v2 ; x1 !-> v1 ; m).
Proof.
Admitted.
☐
Partial maps
Definition partial_map (A : Type) := total_map (option A).
Definition empty {A : Type} : partial_map A :=
t_empty None.
Definition update {A : Type} (m : partial_map A)
(x : string) (v : A) :=
(x !-> Some v ; m).
We introduce a similar notation for partial maps:
Notation "x '|->' v ';' m" := (update m x v)
(at level 0, x constr, v at level 200, right associativity).
(at level 0, x constr, v at level 200, right associativity).
We can also hide the last case when it is empty.
Notation "x '|->' v" := (update empty x v)
(at level 0, x constr, v at level 200).
Definition examplepmap :=
("Church" |-> true ; "Turing" |-> false).
(at level 0, x constr, v at level 200).
Definition examplepmap :=
("Church" |-> true ; "Turing" |-> false).
We now straightforwardly lift all of the basic lemmas about total
maps to partial maps.
Lemma apply_empty : ∀ (A : Type) (x : string),
@empty A x = None.
Proof.
intros. unfold empty. rewrite t_apply_empty.
reflexivity.
Qed.
Lemma update_eq : ∀ (A : Type) (m : partial_map A) x v,
(x |-> v ; m) x = Some v.
Proof.
intros. unfold update. rewrite t_update_eq.
reflexivity.
Qed.
The update_eq lemma is used very often in proofs. Adding it to
Coq's global "hint database" allows proof-automation tactics such
as auto to find it.
#[global] Hint Resolve update_eq : core.
Theorem update_neq : ∀ (A : Type) (m : partial_map A) x1 x2 v,
x2 ≠ x1 →
(x2 |-> v ; m) x1 = m x1.
Proof.
intros A m x1 x2 v H.
unfold update. rewrite t_update_neq.
- reflexivity.
- apply H.
Qed.
Lemma update_shadow : ∀ (A : Type) (m : partial_map A) x v1 v2,
(x |-> v2 ; x |-> v1 ; m) = (x |-> v2 ; m).
Proof.
intros A m x v1 v2. unfold update. rewrite t_update_shadow.
reflexivity.
Qed.
Theorem update_same : ∀ (A : Type) (m : partial_map A) x v,
m x = Some v →
(x |-> v ; m) = m.
Proof.
intros A m x v H. unfold update. rewrite <- H.
apply t_update_same.
Qed.
Theorem update_permute : ∀ (A : Type) (m : partial_map A)
x1 x2 v1 v2,
x2 ≠ x1 →
(x1 |-> v1 ; x2 |-> v2 ; m) = (x2 |-> v2 ; x1 |-> v1 ; m).
Proof.
intros A m x1 x2 v1 v2. unfold update.
apply t_update_permute.
Qed.
Theorem update_neq : ∀ (A : Type) (m : partial_map A) x1 x2 v,
x2 ≠ x1 →
(x2 |-> v ; m) x1 = m x1.
Proof.
intros A m x1 x2 v H.
unfold update. rewrite t_update_neq.
- reflexivity.
- apply H.
Qed.
Lemma update_shadow : ∀ (A : Type) (m : partial_map A) x v1 v2,
(x |-> v2 ; x |-> v1 ; m) = (x |-> v2 ; m).
Proof.
intros A m x v1 v2. unfold update. rewrite t_update_shadow.
reflexivity.
Qed.
Theorem update_same : ∀ (A : Type) (m : partial_map A) x v,
m x = Some v →
(x |-> v ; m) = m.
Proof.
intros A m x v H. unfold update. rewrite <- H.
apply t_update_same.
Qed.
Theorem update_permute : ∀ (A : Type) (m : partial_map A)
x1 x2 v1 v2,
x2 ≠ x1 →
(x1 |-> v1 ; x2 |-> v2 ; m) = (x2 |-> v2 ; x1 |-> v1 ; m).
Proof.
intros A m x1 x2 v1 v2. unfold update.
apply t_update_permute.
Qed.
One last thing: For partial maps, it's convenient to introduce a
notion of map inclusion, stating that all the entries in one map
are also present in another:
We can then show that map update preserves map inclusion -- that is:
Lemma includedin_update : ∀ (A : Type) (m m' : partial_map A)
(x : string) (vx : A),
includedin m m' →
includedin (x |-> vx ; m) (x |-> vx ; m').
Proof.
unfold includedin.
intros A m m' x vx H.
intros y vy.
destruct (eqb_spec x y) as [Hxy | Hxy].
- rewrite Hxy.
rewrite update_eq. rewrite update_eq. intro H1. apply H1.
- rewrite update_neq.
+ rewrite update_neq.
× apply H.
× apply Hxy.
+ apply Hxy.
Qed.
This property is quite useful for reasoning about languages with
variable binding -- e.g., the Simply Typed Lambda Calculus, which
we will see in Programming Language Foundations, where maps are
used to keep track of which program variables are defined in a
given scope.