solutions.ofe
From iris.algebra Require Export ofe.
(* ################################ *)
(* ################################ *)
Ordered Families of Equivalences (OFE)
Coinductive values are allowed to be infinitely deep, meaning we can
construct an infinite stream of zeroes as follows:
CoFixpoints allows us to build coinductive values in the same way
that Fixpoint allows us to use inductive types recursively.
We can get the n'th element of the stream by iterating through the
stream until we find the index we are looking for.
Fixpoint nth (s : stream) (n : nat) : nat :=
match n with
| 0 => head s
| S n => nth (tail s) n
end.
match n with
| 0 => head s
| S n => nth (tail s) n
end.
We can likewise define a stream from its n'th elements.
CoFixpoint fun2stream (f : nat → nat) : stream :=
SCons (f 0) (fun2stream (f ∘ S)).
Lemma fun2stream_nth (f : nat → nat) (n : nat) : nth (fun2stream f) n = f n.
(* SOLUTION *) Proof.
induction n in f |- *; simpl.
- done.
- apply IHn.
Qed.
Section ofe.
SCons (f 0) (fun2stream (f ∘ S)).
Lemma fun2stream_nth (f : nat → nat) (n : nat) : nth (fun2stream f) n = f n.
(* SOLUTION *) Proof.
induction n in f |- *; simpl.
- done.
- apply IHn.
Qed.
Section ofe.
We will say that two streams are equivalent when the elements are
equal. We can specify this using nth.
Now, for the OFE structure, we introduce the concept of approximate
equivalence using the typeclass Dist.
This typeclass defines a family of equivalence relations written
[x ≡{n}≡ y] where n is the precision. In our case, this will
decide how many of the elements of the stream we look at.
These relations should satisfy the following laws:
- dist_equivalence : ∀ n, Equivalence (dist n) As you would expect: The n-equivalences are equivalence relations.
- equiv_dist : ∀ x y, x ≡ y ↔ (∀ n, x ≡{n}≡ y) This states that, as the `precision' of the approximation approaches infinity, the equivalences approach true equivalence.
- dist_lt : ∀ n m x y, x ≡{n}≡ y → m < n → x ≡{m}≡ y This means that reducing the precision can only make more things approximately equivalent.
Lemma stream_ofe_mixin : OfeMixin stream.
Proof.
split.
(* BEGIN SOLUTION *)
- intros s1 s2.
split.
+ intros H n i Hi.
apply H.
+ intros H i.
by apply (H i).
- intros n.
split.
+ intros s i Hi.
done.
+ intros s1 s2 H i Hi.
symmetry.
by apply H.
+ intros s1 s2 s3 H1 H2 i Hi.
trans (nth s2 i).
* by apply H1.
* by apply H2.
- intros n m s1 s2 H Hm i Hi.
apply H.
lia.
Qed.
(* END SOLUTION BEGIN TEMPLATE
(* exercise *)
Admitted.
END TEMPLATE *)
Proof.
split.
(* BEGIN SOLUTION *)
- intros s1 s2.
split.
+ intros H n i Hi.
apply H.
+ intros H i.
by apply (H i).
- intros n.
split.
+ intros s i Hi.
done.
+ intros s1 s2 H i Hi.
symmetry.
by apply H.
+ intros s1 s2 s3 H1 H2 i Hi.
trans (nth s2 i).
* by apply H1.
* by apply H2.
- intros n m s1 s2 H Hm i Hi.
apply H.
lia.
Qed.
(* END SOLUTION BEGIN TEMPLATE
(* exercise *)
Admitted.
END TEMPLATE *)
We can now package this together into an OFE.
We can now ask the question of whether we can build values from a
sequence of better and better approximations. To this end, we use the
concept of a chain. Here, chains are sequences such that all elements
after the n'th element are n-equivalent. An OFE is complete if all
chains approach a value. This is also called a COFE for complete OFE.
In the case of streams, we can find the completion by considering the
n'th number in the n'th stream, as all streams after this point
agree on this value after this point.
Global Program Instance stream_cofe : Cofe streamO := {|
compl c := fun2stream (λ i, nth (c i) i);
|}.
Next Obligation.
intros n [c Hc] i Hi; simpl.
rewrite fun2stream_nth.
specialize (Hc i n Hi i).
symmetry.
by apply Hc.
Qed.
compl c := fun2stream (λ i, nth (c i) i);
|}.
Next Obligation.
intros n [c Hc] i Hi; simpl.
rewrite fun2stream_nth.
specialize (Hc i n Hi i).
symmetry.
by apply Hc.
Qed.
There are two important classes of functions between OFEs called
NonExpansive and Contractive. NonExpansive functions are
functions that preserve dist. This is encoded using the setoid
library as ∀ n, Proper (dist n ==> dist n) f. Contractive
functions furthermore increase the `precision' one step. Encoded as
∀ n, Proper (dist_later n ==> dist n) f.
dist_later is defined differently from what you might have seen
elsewhere, but it is still equivalent to the usual definition.
dist_later n x y := ∀ m, m < n → x ≡{m}≡ y
Global Instance SCons_contractive x : Contractive (SCons x).
Proof.
intros n s1 s2 [H] i Hi.
destruct i as [|j].
- simpl.
done.
- simpl.
by apply (H j).
Qed.
Proof.
intros n s1 s2 [H] i Hi.
destruct i as [|j].
- simpl.
done.
- simpl.
by apply (H j).
Qed.
All Contractive functions are also NonExpansive, but this fact
is not registered as an instance to improve type-class instance
resolution. It is thus necessary to register this fact for your
functions as well.
Furthermore, all NonExpansive functions are also Proper, meaning
we only need to prove the strongest of these three properties and then
use it for the rest.
We don't have any such nice properties for tail and nth. So we
prove that they preserve the equivalence directly.
Global Instance tail_proper : Proper ((≡) ==> (≡)) tail.
Proof.
intros s1 s2 H n.
exact (H (S n)).
Qed.
Global Instance nth_proper : Proper ((≡) ==> (=) ==> (=)) nth.
Proof.
by intros s1 s2 H n _ <-.
Qed.
Proof.
intros s1 s2 H n.
exact (H (S n)).
Qed.
Global Instance nth_proper : Proper ((≡) ==> (=) ==> (=)) nth.
Proof.
by intros s1 s2 H n _ <-.
Qed.
If we look at tail, we notice that it isn't NonExpansive for the
same reason that SCons is contractive. However, it is
anti-contractive in that it decreases the `precision' by one step.
These functions are rarely used as they aren't NonExpansive and
therefore don't behave as well as we would like. However, they can be
used when constructing new functions by balancing anti-contractive
functions with Contractive functions.
Global Instance tail_anti_contractive : ∀ n, Proper (dist n ==> dist_later n) tail.
Proof.
intros n s1 s2 H.
constructor.
intros m Hm i Hi.
apply (H (S i)).
lia.
Qed.
Proof.
intros n s1 s2 H.
constructor.
intros m Hm i Hi.
apply (H (S i)).
lia.
Qed.
With the setup out of the way, we can now begin defining operations
on streams. The first such operation will be appending a list to the
front of a stream.
Fixpoint sapp (l : list nat) (s : stream) : stream :=
match l with
| [] => s
| x :: l => SCons x (sapp l s)
end.
match l with
| [] => s
| x :: l => SCons x (sapp l s)
end.
This operation is NonExpansive when we fix the list we wish to
prepend. Furthermore, we can prove this without unfolding the
definition of dist by using what we have proved thus far.
Global Instance sapp_ne (l : list nat) : NonExpansive (sapp l).
(* SOLUTION *) Proof.
intros n s1 s2 H.
induction l as [|x l IH]; simpl.
- done.
- by f_equiv.
Qed.
Global Instance sapp_proper (l : list nat) : Proper ((≡) ==> (≡)) (sapp l).
Proof. apply ne_proper, _. Qed.
(* SOLUTION *) Proof.
intros n s1 s2 H.
induction l as [|x l IH]; simpl.
- done.
- by f_equiv.
Qed.
Global Instance sapp_proper (l : list nat) : Proper ((≡) ==> (≡)) (sapp l).
Proof. apply ne_proper, _. Qed.
Now, let's define a stream that simply repeats a list over and over
again. Intuitively, we should be able to define it as follows:
However, this correctly fails. Consider the case in which the list
is empty. No matter how many times you repeat an empty list, the
result remains empty. So we will never end up with an infinite
stream.
To fix this, we can insert a separator in between the repetitions of
the list:
Fail CoFixpoint repeat_with_sep (l : list nat) (x : nat) : stream :=
sapp l (SCons x (repeat_with_sep l x)).
sapp l (SCons x (repeat_with_sep l x)).
But this still fails with the same error. This is because Coq uses a
simple syntactic check to validate co-fixpoint definitions.
To satisfy this check, we are forced to syntactically produce at least
one element of the stream per recursive call, which is violated by the
call to sapp. In practice, this means that we cannot reuse most of
our existing operations when writing new recursively defined streams.
We instead have to implement it from scratch as follows:
CoFixpoint repeat_with_sep_helper (l : list nat) (x : nat) (helper : list nat) : stream :=
match helper with
| [] => SCons x (repeat_with_sep_helper l x l)
| y :: helper => SCons y (repeat_with_sep_helper l x helper)
end.
Definition repeat_with_sep (l : list nat) (x : nat) :=
repeat_with_sep_helper l x l.
match helper with
| [] => SCons x (repeat_with_sep_helper l x l)
| y :: helper => SCons y (repeat_with_sep_helper l x helper)
end.
Definition repeat_with_sep (l : list nat) (x : nat) :=
repeat_with_sep_helper l x l.
To ensure that this definition is correct, we can show that it still
solves the fixpoint equation. To do this, we first need to show that
the helper function does what it is supposed to.
Lemma repeat_with_sep_helper_unfold (l : list nat) (x : nat) (helper : list nat) :
repeat_with_sep_helper l x helper ≡ sapp helper (SCons x (repeat_with_sep_helper l x l)).
Proof.
induction helper as [|y helper IH].
- intros [|n]; simpl.
+ done.
+ done.
- intros [|n]; simpl.
+ done.
+ done.
Qed.
Lemma repeat_with_sep_unfold (l : list nat) (x : nat) :
repeat_with_sep l x ≡ sapp l (SCons x (repeat_with_sep l x)).
Proof. apply repeat_with_sep_helper_unfold. Qed.
repeat_with_sep_helper l x helper ≡ sapp helper (SCons x (repeat_with_sep_helper l x l)).
Proof.
induction helper as [|y helper IH].
- intros [|n]; simpl.
+ done.
+ done.
- intros [|n]; simpl.
+ done.
+ done.
Qed.
Lemma repeat_with_sep_unfold (l : list nat) (x : nat) :
repeat_with_sep l x ≡ sapp l (SCons x (repeat_with_sep l x)).
Proof. apply repeat_with_sep_helper_unfold. Qed.
This can be non-trivial and requires code duplication, as mentioned
previously. COFEs offer a way around these restrictions through
guarded-fixpoints. Let us write the function as before, but with the
result of a recursive call as the last argument:
We can then notice that this function is contractive with respect to
this recursive argument s, as sapp is NonExpansive and SCons
is Contractive.
Global Instance repeat_with_sep_contractive (l : list nat) (x : nat) : Contractive (repeat_with_sep_pre l x).
Proof. solve_contractive. Qed.
Proof. solve_contractive. Qed.
To get a fixpoint for such a function, we need three things:
- The type we wish to produce an element of (stream) must have a COFE structure, which we defined earlier.
- The function we wish to find a fixpoint for must be contractive, which we just proved.
- Finally, the COFE must be inhabited, meaning there is an element of that type. We can use zeroes for this, as the choice of inhabitant doesn't matter.
With all conditions met, we can now define the fixpoint as follows:
And the fixpoint can be unfolded up to equivalence:
Lemma repeat_with_sep_alt_unfold (l : list nat) (x : nat) :
repeat_with_sep_alt l x ≡ sapp l (SCons x (repeat_with_sep_alt l x)).
Proof. exact (fixpoint_unfold _). Qed.
repeat_with_sep_alt l x ≡ sapp l (SCons x (repeat_with_sep_alt l x)).
Proof. exact (fixpoint_unfold _). Qed.
Furthermore, any such fixpoint is unique, so we can prove that our two
definitions are equivalent.
Lemma repeat_with_sep_alt_correct (l : list nat) (x : nat) :
repeat_with_sep l x ≡ repeat_with_sep_alt l x.
Proof.
apply fixpoint_unique.
apply repeat_with_sep_unfold.
Qed.
repeat_with_sep l x ≡ repeat_with_sep_alt l x.
Proof.
apply fixpoint_unique.
apply repeat_with_sep_unfold.
Qed.
A common operation one could consider on streams is mapping. This
luckily conforms to the rules of co-fixpoints, but we will also define
it using guarded recursion.
CoFixpoint stream_map (f : nat → nat) (s : stream) : stream :=
SCons (f (head s)) (stream_map f (tail s)).
SCons (f (head s)) (stream_map f (tail s)).
Notice that our recursive call takes different parameters. So we can't
just build this as a fixpoint on streams. Instead, we need to build a
fixpoint on stream → stream. However, this is not equipped with a
canonical OFE structure. Instead, we need to write
stream -d> streamO. Here, d stands for discrete, meaning we don't
consider any OFE structure on the domain. As such, we are allowed to
change the parameter arbitrarily on recursive calls.
Definition stream_map_pre (f : nat → nat) (map : stream -d> streamO) : stream -d> streamO :=
λ s : stream, SCons (f (head s)) (map (tail s)).
Global Instance stream_map_pre_contractive (f : nat → nat) : Contractive (stream_map_pre f).
Proof. solve_contractive. Qed.
λ s : stream, SCons (f (head s)) (map (tail s)).
Global Instance stream_map_pre_contractive (f : nat → nat) : Contractive (stream_map_pre f).
Proof. solve_contractive. Qed.
In this way, we can use guarded-fixpoints as an alternative to
co-fixpoints. Allowing us to use existing functions in the fixpoint
definitions.
Rather than simply restating the fixpoint unfolding lemma directly, we
will fully apply the function.
Lemma stream_map_alt_unfold (f : nat → nat) (s : stream) :
stream_map_alt f s ≡ SCons (f (head s)) (stream_map_alt f (tail s)).
Proof. exact (fixpoint_unfold (stream_map_pre f) s). Qed.
Lemma stream_map_alt_correct (f : nat → nat) (s : stream) : stream_map f s ≡ stream_map_alt f s.
Proof.
apply (fixpoint_unique (stream_map_pre f)).
clear s; intros s.
intros [|n]; simpl.
+ done.
+ done.
Qed.
Lemma stream_map_nth (f : nat → nat) (s : stream) (n : nat) :
nth (stream_map f s) n = f (nth s n).
(* SOLUTION *) Proof.
induction n in s |- *; simpl.
- done.
- apply IHn.
Qed.
Global Instance stream_map_ne (f : nat → nat) : NonExpansive (stream_map f).
(* SOLUTION *) Proof.
intros n s1 s2 H m Hm.
rewrite !stream_map_nth.
f_equal.
apply H, Hm.
Qed.
stream_map_alt f s ≡ SCons (f (head s)) (stream_map_alt f (tail s)).
Proof. exact (fixpoint_unfold (stream_map_pre f) s). Qed.
Lemma stream_map_alt_correct (f : nat → nat) (s : stream) : stream_map f s ≡ stream_map_alt f s.
Proof.
apply (fixpoint_unique (stream_map_pre f)).
clear s; intros s.
intros [|n]; simpl.
+ done.
+ done.
Qed.
Lemma stream_map_nth (f : nat → nat) (s : stream) (n : nat) :
nth (stream_map f s) n = f (nth s n).
(* SOLUTION *) Proof.
induction n in s |- *; simpl.
- done.
- apply IHn.
Qed.
Global Instance stream_map_ne (f : nat → nat) : NonExpansive (stream_map f).
(* SOLUTION *) Proof.
intros n s1 s2 H m Hm.
rewrite !stream_map_nth.
f_equal.
apply H, Hm.
Qed.
If we now wanted to create a stream of all the powers of 2, we would
intuitively want something like this:
However, this again fails the syntactic check, as we are modifying the
tail of the stream.
To fix this, we could instead define a stream starting with a number
n doubling n at every step.
CoFixpoint power2_helper (n : nat) : stream :=
SCons n (power2_helper (n * 2)).
Definition power2 : stream :=
power2_helper 1.
SCons n (power2_helper (n * 2)).
Definition power2 : stream :=
power2_helper 1.
To know that these definitions are equivalent, we must again prove
that power2_helper and power2 satisfy respective fixpoint
equations.
Lemma power2_helper_unfold (n : nat) :
power2_helper n ≡ SCons n (stream_map (λ n, n * 2) (power2_helper n)).
(* SOLUTION *) Proof.
intros [|i]; simpl.
{ done. }
rewrite stream_map_nth.
induction i in n |- *; simpl.
- done.
- done.
Qed.
Lemma power2_unfold :
power2 ≡ SCons 1 (stream_map (λ n, n * 2) power2).
Proof. apply power2_helper_unfold. Qed.
power2_helper n ≡ SCons n (stream_map (λ n, n * 2) (power2_helper n)).
(* SOLUTION *) Proof.
intros [|i]; simpl.
{ done. }
rewrite stream_map_nth.
induction i in n |- *; simpl.
- done.
- done.
Qed.
Lemma power2_unfold :
power2 ≡ SCons 1 (stream_map (λ n, n * 2) power2).
Proof. apply power2_helper_unfold. Qed.
Now, just like before, we can instead define the function for which we
wish to find a fixpoint:
So we can now get a fixpoint, just as before.
Definition power2_alt := fixpoint power2_pre.
Lemma power2_alt_unfold : power2_alt ≡ power2_pre power2_alt.
Proof. apply fixpoint_unfold. Qed.
Lemma power2_alt_unfold : power2_alt ≡ power2_pre power2_alt.
Proof. apply fixpoint_unfold. Qed.
Like before, we know that this fixpoint is unique, so it must be
equivalent to the other definition.
Lemma power2_alt_correct : power2 ≡ power2_alt.
Proof.
apply fixpoint_unique.
apply power2_unfold.
Qed.
Proof.
apply fixpoint_unique.
apply power2_unfold.
Qed.
iProp also has a COFE structure, but it is not a co-inductive type.
This means that even though we can't use CoFixpoint to build
propositions recursively, we can use fixpoint. Importantly, all the
connectives of the logic are NonExpansive. To then use guarded
fixpoints, we need a Contractive function like SCons. And for
iProp, this is later ▷ P. We have already run into two connectives
defined using the guarded-fixpoint, namely the weakest precondition
WP, and invariants inv. This is why we can remove laters from
premises when taking a step in the program, as this corresponds to
unfolding the fixpoint, thus revealing a later.