WF_REL_TAC : term quotation -> tactic
?R. WF R /\ ...
val qsort_defn = Hol_defn "qsort" `(qsort ___ [] = []) /\ (qsort ord (x::rst) = APPEND (qsort ord (FILTER ($~ o ord x) rst)) (x :: qsort ord (FILTER (ord x) rst)))`;
- tgoal qsort_defn; > val it = Proof manager status: 1 proof. 1. Incomplete: Initial goal: ?R. WF R /\ (!rst x ord. R (ord,FILTER ($~ o ord x) rst) (ord,x::rst)) /\ !rst x ord. R (ord,FILTER (ord x) rst) (ord,x::rst) - e (WF_REL_TAC `measure (LENGTH o SND)`); OK.. 2 subgoals: > val it = !rst x ord. LENGTH (FILTER (ord x) rst) < LENGTH (x::rst) !rst x ord. LENGTH (FILTER (\x'. ~ord x x') rst) < LENGTH (x::rst)
measure (LENGTH o SND)
There are a number of basic and advanced means of specifying well-founded relations. The most common starting point for dealing with termination problems for recursive functions is to find some function, known as a ’measure’ under which the arguments of a function call are larger than the arguments to any recursive calls that result.
For a very simple starter example, consider the following definition of a function that computes the greatest common divisor of two numbers:
- val gcd_defn = Hol_defn "gcd" `(gcd (0,n) = n) /\ (gcd (m,n) = gcd (n MOD m, m))`; - Defn.tgoal gcd_defn; > val it = Proof manager status: 1 proof. 1. Incomplete: Initial goal: ?R. WF R /\ !v2 n. R (n MOD SUC v2,SUC v2) (SUC v2,n)
measure f x y = (f x < f y).
- e (WF_REL_TAC `measure FST`); OK.. 1 subgoal: > val it = !v2 n. n MOD SUC v2 < SUC v2
- e (PROVE_TAC [arithmeticTheory.DIVISION, prim_recTheory.LESS_0]); OK.. Goal proved. ...
- Hol_datatype `btree = Leaf | Brh of btree => btree`; - val Unbal_defn = Hol_defn "Unbal" `(Unbal Leaf = Leaf) /\ (Unbal (Brh Leaf bt) = Brh Leaf (Unbal bt)) /\ (Unbal (Brh (Brh bt1 bt2) bt) = Unbal (Brh bt1 (Brh bt2 bt)))`; - Defn.tgoal Unbal_defn; > val it = Proof manager status: 1 proof. 1. Incomplete: Initial goal: ?R. WF R /\ (!bt. R bt (Brh Leaf bt)) /\ !bt bt2 bt1. R (Brh bt1 (Brh bt2 bt)) (Brh (Brh bt1 bt2) bt)
Weight (Leaf) = 0 Weight (Brh x y) = (2 * Weight x) + (Weight y) + 1
val Weight = Prim_rec.prove_rec_fn_exists (TypeBase.axiom_of ("", "btree")) (Term`(Weight (Leaf) = 0) /\ (Weight (Brh x y) = (2 * Weight x) + (Weight y) + 1)`); > val Weight = |- ?Weight. (Weight Leaf = 0) /\ !x y. Weight (Brh x y) = 2 * Weight x + Weight y + 1 : thm - e (STRIP_ASSUME_TAC Weight); OK.. 1 subgoal: > val it = ?R. WF R /\ (!bt. R bt (Brh Leaf bt)) /\ !bt bt2 bt1. R (Brh bt1 (Brh bt2 bt)) (Brh (Brh bt1 bt2) bt) ------------------------------------ 0. Weight Leaf = 0 1. !x y. Weight (Brh x y) = 2 * Weight x + Weight y + 1
e (WF_REL_TAC `measure Weight`); OK.. 2 subgoals: > val it = !bt bt2 bt1. Weight (Brh bt1 (Brh bt2 bt)) < Weight (Brh (Brh bt1 bt2) bt) ------------------------------------ 0. Weight Leaf = 0 1. !x y. Weight (Brh x y) = 2 * Weight x + Weight y + 1 !bt. Weight bt < Weight (Brh Leaf bt) ------------------------------------ 0. Weight Leaf = 0 1. !x y. Weight (Brh x y) = 2 * Weight x + Weight y + 1
The technique of ‘weighting‘ nodes in a tree in order to prove termination also goes by the name of ‘polynomial interpretation‘. It must be admitted that finding the correct weighting for a termination proof is more an art than a science. Typically, one makes a guess and then tries the termination proof to see if it works.
Occasionally, there’s a combination of factors that complicate the termination argument. For example, the following specification describes a naive pattern matching algorithm on strings (represented as lists here). The function takes four arguments: the first is the remainder of the pattern being matched. The second is the remainder of the string being searched. The third argument holds the original pattern to be matched. The fourth argument is the string being searched. If the pattern (first argument) becomes exhausted, then a match has been found and the function returns T. Otherwise, if the string being searched becomes exhausted, the function returns F.
val match0_defn = Hol_defn "match0" `(match0 [] __ __ __ = T) /\ (match0 __ [] __ __ = F) /\ (match0 (p::pp) (s::ss) p0 rs = if p=s then match0 pp ss p0 rs else if NULL rs then F else match0 p0 (TL rs) p0 (TL rs))`; - val match = Define `match pat str = match0 pat str pat str`;
So much for the behaviour of the function. Why does it terminate? There are two recursive calls. The first call reduces the size of the first and second arguments, and leaves the other arguments unchanged. The second call can increase the size of the first and second arguments, but reduces the size of the fourth.
This is a classic situation in which to use a lexicographic ordering: some arguments to the function are reduced in some recursive calls, and some others are reduced in other recursive calls. Recall that LEX is an infix operator, defined in pairTheory as follows:
LEX R1 R2 = \(x,y) (p,q). R1 x p \/ ((x=p) /\ R2 y q)
What we need now is to formalize this. We want to map from the four-tuple of arguments into a lexicographic combination of relations. This is enabled by inv_image from relationTheory:
inv_image R f = \x y. R (f x) (f y)
- Defn.tgoal match0_defn; - e (WF_REL_TAC `inv_image ($< LEX $<) (\(w,x,y,z). (LENGTH z, LENGTH x))`); OK.. 2 subgoals: > val it = !rs ss s p. (p=s) ==> LENGTH rs < LENGTH rs \/ LENGTH ss < LENGTH (s::ss) !ss rs s p. ~(p = s) /\ ~NULL rs ==> LENGTH (TL rs) < LENGTH rs \/ (LENGTH (TL rs) = LENGTH rs) /\ LENGTH (TL rs) < LENGTH (s::ss)
As a final example, one occasionally needs to recurse over non-concrete data, such as finite sets or multisets. We can define a ‘fold‘ function (of questionable utility) for finite sets as follows:
load "pred_setTheory"; open pred_setTheory; val FOLD_SET_defn = Defn.Hol_defn "FOLD_SET" `FOLD_SET (s:'a->bool) (b:'b) = if FINITE s then if s={} then b else FOLD_SET (REST s) (f (CHOICE s) b) else ARB`;
val (FOLD_SET_0, FOLD_SET_IND) = Defn.tprove (FOLD_SET_defn, WF_REL_TAC `measure (CARD o FST)` THEN PROVE_TAC [CARD_PSUBSET, REST_PSUBSET]);
|- FINITE s ==> (FOLD_SET f s b = if s = {} then b else FOLD_SET f (REST s) (f (CHOICE s) b))