In the previous lecture, we saw an example of verification. Today we'll look at another example.
Consider the following recursive implementation of a function
lmax
that computes the maximum element of a nonempty list:
(* Returns: lmax xs is the maximum element in the list xs. * Checks: xs != [] *) let rec lmax (xs : int list) : int = match xs with [] -> raise (Failure "requires clause of max violated") | [x] -> x | x :: t -> max x (lmax t)
How do we handle recursive functions? We'll use the approach of assuming that a recursive function satisfies its spec whenever it is called recursively. A proof of correctness will then ensure the partial correctness of the function's implementation, which we can distinguish from total correctness:
Partial correctness: whenever the function is applied with the precondition satisfied, and it terminates, it produces results satisfying the postcondition.
Total correctness: the function has partial correctness, and in addition, it always terminates when the function is applied.
The nice thing about this approach is that we can separate the problem of proving that a function computes the right answer from the problem of proving that the function terminates.
Let's prove partial correctness. First of all, we need to understand our postcondition a little more precisely. What are the elements of a list? We can define a function that gives us the elements by induction on the list:
elements([]
) = ∅
elements(h::
t) = {h} ∪ elements(t)
Like an abstraction function, this function maps from the concrete domain of OCaml lists to a more abstract mathematical domain, that of sets.
Expression | Assumptions | Justification | |||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
lmax xs
| xs ≠ [] | Consider an arbitrary call satisfying the precondition. | |||||||||||||||||||||||||||
match xs with |
The key to proving total correctness is to prove that recursion cannot go on forever. We need to be able to map the function arguments onto a set that has a least element. Typically we do this by giving a decrementing function d(x) that maps the function argument x onto the natural numbers. The decrementing function d has two properties:
PRE ⇒ 0 ≤ d(x)
0 ≤ d(x') < d(x).
This means that when the decrementing function is zero, there is no recursive call.
These conditions ensure that the decrementing function keeps getting smaller on every recursive call, but cannot get smaller forever.
For example, in lmax
an appropriate decrementing
function is:
d(x) = List.length
(x) − 1. It must be nonnegative;
when it is zero, the function terminates; and the recursive call to
lmax
is on a shorter list t
.
Recall from CS 2800 that propositional logic is a logic built up from simple symbols representing propositions about some world. For our example, we will use the letters A, B, C, ... as propositional symbols. For example, these symbols might stand for various propositions:
It is not the job of propositional logic to assign meanings to these symbols. However, we use statements to the meanings of D and E to talk about the correctness of programs.
We define a grammar for propositions built up from these symbols. We use the letters P, Q, R to represent propositions (or formulas):
P,Q,R ::= ⊤ (* true *) | ⊥ (* false *) | A, B, C (* propositional symbols *) | ¬P (* sugar for P⇒⊥ *) | P ∧ Q (* "P and Q" (conjunction) *) | P ∨ Q (* "P or Q" (disjunction) *) | P ⇒ Q (* "P implies Q" (implication) *) | P ⇔ Q (* "P if and only if Q" (double implication) *)
Note: On some browsers, on some operating systems, in some fonts, the symbol for conjunction (and) is rendered incorrectly as a small circle. It should look like an upside-down ∨. In these course notes, it will appear variously as ∧, ∧, or ∧.
The precedence of these forms decreases as we go down the list, so P ∧ Q ⇒ R is the same as (P ∧ Q) ⇒ R. One thing to watch out for is that ⇒ is right-associative (like →), so P ⇒ Q ⇒ R is the same as P ⇒ (Q ⇒ R). We will introduce parentheses as needed for clarity. We will use the notation for logical negation, but it is really just syntactic sugar for the implication P ⇒ ⊥. We also write P ⇔ Q as syntactic sugar for (P ⇒ Q) ∧ (Q ⇒ P), meaning that P and Q are logically equivalent.
This grammar defines the language of propositions. With suitable propositional symbols, we can express various interesting statements, for example:
In fact, all three of these propositions are logically equivalent, which we can determine without knowing about what finals and attendance mean.