Continuing my previous post on proving properties about
CRDTs, specifically G-Counter
s, we’ll now look at the PN-Counter
, a
counter which allows the value to both increment and decrement while
guaranteeing convergence. Since this builds off of the lemmas proven in
the previous post, I highly recommend you read that post
first.
First, let’s define a PN-Counter
as a product of two G-Counter
s.
We’ll use one set for counting the increments, and one for counting the
decrements.
(* Initialize an empty PN_Counter. *)
Definition PN_Counter := (G_Counter * G_Counter)%type.
Definition PN_Counter_init : PN_Counter := (G_Counter_init, G_Counter_init).
Then, let’s define increment and decrement functions for a particular
actor, which call the G-Counter
increment function on each set. In
this example, we use fst
and snd
to destructure the PN-Counter
into two seperate counters, and then after we perform the increment we
use pair
, the product constructor, to rebuild the PN-Counter
.
(* Increment a PN_Counter for a particular actor. *)
Definition PN_Counter_incr actor (clocks : PN_Counter) :=
pair (G_Counter_incr actor (fst clocks)) (snd clocks).
(* Decrement a PN_Counter for a particular actor. *)
Definition PN_Counter_decr actor (clocks : PN_Counter) :=
pair (fst clocks) (G_Counter_incr actor (snd clocks)).
Let’s define a function to reveal the current value of the PN-Counter
,
by subtracting the values of each counter.
(* Reveal the current value of a PN_Counter. *)
Definition PN_Counter_reveal clocks :=
minus (G_Counter_reveal (fst clocks)) (G_Counter_reveal (snd clocks)).
Then, let’s define merge, comparsion, and equality functions, which
leverage the functions we’ve defined over G-Counter
s.
(* Merge two PN_Counters. *)
Definition PN_Counter_merge c1 c2 :=
pair (G_Counter_merge (fst c1) (fst c2))
(G_Counter_merge (snd c1) (snd c2)).
(* Compare two G_Counters. *)
Definition PN_Counter_compare (c1 c2 : PN_Counter) :=
and (G_Counter_compare (fst c1) (fst c2))
(G_Counter_compare (snd c1) (snd c2)).
(* Verify that two PN_Counters are equal. *)
Definition PN_Counter_equal (c1 c2 : PN_Counter) :=
ClockMap.Equal (fst c1) (fst c2) /\ ClockMap.Equal (snd c1) (snd c2).
Cool. Now, let’s start proving things.
First, let’s prove that the merge operation commutes. Similar to our
G-Counter
proof, we can simply perform the necessary destructuring of
the PN-Counter
type, and then apply the lemma we’ve previously defined
which shows that the merge operation on clocks commute.
Theorem PN_Counter_merge_comm : forall c1 c2,
PN_Counter_equal (PN_Counter_merge c1 c2) (PN_Counter_merge c2 c1).
Proof.
intros.
unfold PN_Counter_equal.
split; unfold ClockMap.Equal; intros;
unfold PN_Counter_merge; unfold G_Counter_merge; simpl;
repeat rewrite ClockMapFacts.map2_1bis; auto.
apply Clock_merge_comm.
apply Clock_merge_comm.
Qed.
Same thing, we can use the existing lemmas we’ve proven about
G-Counter
’s and their merge operation, and apply it to prove that, in
addition to being commutative, they are also associative and idempotent.
Theorem PN_Counter_merge_idempotent : forall c1,
PN_Counter_equal (PN_Counter_merge c1 c1) c1.
Proof.
intros.
unfold PN_Counter_equal.
split; unfold ClockMap.Equal; intros;
unfold PN_Counter_merge; unfold G_Counter_merge; simpl;
repeat rewrite ClockMapFacts.map2_1bis; auto.
apply Clock_merge_idempotent.
apply Clock_merge_idempotent.
Qed.
Theorem PN_Counter_merge_assoc : forall c1 c2 c3,
PN_Counter_equal
(PN_Counter_merge c1 (PN_Counter_merge c2 c3))
(PN_Counter_merge (PN_Counter_merge c1 c2) c3).
Proof.
intros.
unfold PN_Counter_equal.
split; unfold ClockMap.Equal; intros;
unfold PN_Counter_merge; unfold G_Counter_merge; simpl;
repeat rewrite ClockMapFacts.map2_1bis; auto;
repeat rewrite <- Clock_merge_assoc; reflexivity.
Qed.
Now, let’s prove that both the increment and decrement operations
monotonically advance the structure. This is extremely trivial, as
we’ve already proven that incrementing an individual G-Counter
monotonically advances the structure, given that the increment and
decrement are both only increment operations operating on either side of
a product.
Theorem PN_Counter_incr_mono : forall clocks actor,
PN_Counter_compare clocks (PN_Counter_incr actor clocks).
Proof.
intros; unfold PN_Counter_compare; unfold PN_Counter_incr; simpl.
split. apply G_Counter_incr_mono.
apply G_Counter_compare_idempotent.
Qed.
Theorem PN_Counter_decr_mono : forall clocks actor,
PN_Counter_compare clocks (PN_Counter_decr actor clocks).
Proof.
intros; unfold PN_Counter_compare; unfold PN_Counter_decr; simpl.
split. apply G_Counter_compare_idempotent.
apply G_Counter_incr_mono.
Qed.
Now, let’s confirm that the merge operation of the PN-Counter
cause
the data structure to monotonically advance. This proof is trivial,
given we’ve already proven that this property holds true for the
G-Counter
. We can simply apply the theorem we’ve already defined.
Theorem PN_Counter_merge_mono : forall c1 c2,
PN_Counter_compare c1 (PN_Counter_merge c1 c2).
Proof.
intros; unfold PN_Counter_compare; unfold PN_Counter_merge; split; simpl;
apply G_Counter_merge_mono.
Qed.
Qed.
View this story on Hacker News.