(* begin hide *) (**********************************************************************) (* Equations *) (* Copyright (c) 2009-2021 Matthieu Sozeau *) (**********************************************************************) (* This file is distributed under the terms of the *) (* GNU Lesser General Public License Version 2.1 *) (**********************************************************************) (* end hide *) (** * Working with accumulators A standard pattern of functional programming involves writting tail-recursive versions of an algorithm using an accumulator (also called wrapper/worker), we show how Equations makes this pattern easy to express and reason about using where clauses, well-founded recursion and function eliminators. *) From Equations.Prop Require Import Equations. From Stdlib Require Import List Syntax Arith Lia. Import ListNotations. (** ** Worker/wrapper The most standard example is an efficient implementation of list reversal. Instead of growing the stack by the size of the list, we accumulate a partially reverted list as a new argument of our function. We implement this using a [go] auxilliary function defined recursively and pattern matching on the list. *) Equations rev_acc {A} (l : list A) : list A := rev_acc l := go [] l where go : list A -> list A -> list A := go acc [] := acc; go acc (hd :: tl) := go (hd :: acc) tl. (** A typical issue with such accumulating functions is that one has to write lemmas in two versions, once about the internal [go] function and then on its wrapper. Using the functional elimination principle associated to [rev_acc], we can show both properties simultaneously. *) Lemma rev_acc_eq : forall {A} (l : list A), rev_acc l = rev l. Proof. (** We apply functional elimination on the [rev_acc l] call. The eliminator expects two predicates: one specifying the wrapper and another for the worker. For the wrapper, we give the expected final goal but for the worker we have to invent a kind of loop invariant: here that the result of the whole [go acc l] call is equal to [rev l ++ acc]. *) apply (rev_acc_elim (fun A l revaccl => revaccl = rev l) (fun A _ acc l go_res => go_res = rev l ++ acc)); intros; simpl. (** Functional elimination provides us with the worker property for the initial [go [] l] call, i.e. that it is equal to [rev l ++ []], hence the proof: *) + now rewrite app_nil_r in H. (** For the worker proofs themselves, we use standard reasoning. *) + reflexivity. + now rewrite H, <- app_assoc. Qed. (** ** The worker/wrapper and well-founded recursion Sometimes the natural expression of an algorithm in the worker/wrapper pattern requires well-founded recursion: here we take an example algorithm from Haskell whose termination is justified by a measure. Note that the [worker] subprogram's termination measure and implementation depends on the enclosing [k] argument which is captured in the where clause. *) Local Obligation Tactic := idtac. Equations? isPrime (n : nat) : bool := isPrime 0 := false; isPrime 1 := false; isPrime 2 := true; isPrime 3 := true; isPrime k := worker 2 where worker (n' : nat) : bool by wf (k - n') lt := worker n' with ge_dec n' k := { | left H := true; | right H := if Nat.eqb (Nat.modulo k n') 0 then false else worker (S n') }. Proof. lia. Defined. (* Require Import ExtrOcamlBasic. *) (* Extraction isPrime. *) (** ** Programm equivalence with worker/wrappers Finally we show how the eliminator can be used to prove program equivalences involving a worker/wrapper definition. Here [indexes l] computes the list [0..|l|-1] of valid indexes in the list [l]. *) Equations indexes : list nat -> list nat := indexes l := go [] (length l) where go : list nat -> nat -> list nat := go acc 0 := acc; go acc (S n) := go (n :: acc) n. (** Clearly, all indexes in the resulting list should be smaller than [length l]: *) Lemma indexes_spec (l : list nat) : Forall (fun x => x < length l) (indexes l). Proof. (** We apply the eliminator, giving a predicate that specifies preservation of the property from the accumulator to the end result for [go]'s specification. The rest of the proof uses simple reasoning. *) apply (indexes_elim (fun l indexesl => Forall (fun x => x < length l) indexesl) (fun l acc n indexesl => n <= length l -> Forall (fun x => x < length l) acc -> Forall (fun x => x < length l) indexesl)); clear l; intros. + apply H; constructor. + apply H0. + apply H. lia. constructor. lia. apply H1. Qed. (** Using well-founded recursion we can also define an [interval x y] function producing the interval [x..y-1] *) Equations? interval x y : list nat by wf (y - x) lt := interval x y with lt_dec x y := { | left ltxy => x :: interval (S x) y; | right nltxy => [] }. Proof. lia. Defined. (** We prove a simple lemmas on [interval]: *) Lemma interval_large x y : ~ x < y -> interval x y = []. Proof. funelim (interval x y); clear Heq; intros; now try lia. Qed. (** One can show that [indexes l] produces the interval [0..|l|-1] using [indexes_elim]. The recursion invariant for [indexes_go] records that [acc] corresponds to a partial interval [n..|l|-1] during the computation, and is finally completed into [0..|l|-1] by the end of the computation. We use the previous lemmas as helpers. *) Lemma indexes_interval l : indexes l = interval 0 (length l). Proof. set (P := fun start (l indexesl : list nat) => indexesl = interval start (length l)). revert l. apply (indexes_elim (P 0) (fun l acc n indexesl => n <= length l -> P n l acc -> P 0 l indexesl)); subst P; simpl. intros l. + intros H. apply H; auto. rewrite interval_large; trivial; lia. + intros; trivial. + intros l ? n H Hn ->. apply H. lia. rewrite (interval_equation_1 n). destruct lt_dec. reflexivity. elim n0. lia. Qed.