8 Sorting
We now turn to a study of functional algorithms. The next example will focus on sorting, beginning with an examination of the formal specification of sorting.
Dafny
datatype List<X> = Nil | Cons(head: X, tail: List<X>) function Length<T>(xs: List<T>): nat { match xs case Nil => 0 case Cons(_, tail) => 1 + Length(tail) } function Append<X>(xs: List<X>, ys: List<X>): List<X> ensures Length(Append(xs, ys)) == Length(xs) + Length(ys) {match xs case Nil => ys case Cons(x, tail) => Cons(x, Append(tail, ys)) } predicate Member<X(==)>(x: X, xs: List<X>) { match xs case Nil => false case Cons(y, xs') => x == y || Member(x, xs') } function At<X>(xs: List<X>, i: nat): X requires i < Length(xs) {if i == 0 then xs.head else At(xs.tail, i - 1) }
type IntList = List<int>
To check whether a list is sorted, we must compare elements in pairs. One way to do this is as follows.
Dafny
predicate Ordered(xs: IntList) { match xs case Nil => true case Cons(_, Nil) => true case Cons(x, Cons(y, _)) => x <= y && Ordered(xs.tail) }
Dafny
predicate Ordered'(xs: IntList) { match xs case Cons(x, Cons(y, _)) => x <= y && Ordered'(xs.tail) case _ => true }
When writing a specification, it’s possible to make mistakes—just as we can when writing a program. One way to check a specification is to prove certain properties about it, ensuring it behaves as expected. Here are two such properties:
Dafny
list is at its head // The smallest element of a lemma HeadIsSmallest(xs: IntList, y: int) requires Ordered(xs) && xs.Cons? requires Member(y, xs) ensures xs.head <= y { }
Dafny
lemma AllOrdered(xs: IntList, i: nat, j: nat) requires Ordered(xs) && i <= j < Length(xs) ensures At(xs, i) <= At(xs, j) {if i != 0 { to the elements i and j // let's move closer 1, j - 1); AllOrdered(xs.tail, i - else if i == j { } // easyelse { } 0, j - 1); AllOrdered(xs.tail, } }
A function isn’t a sorting function simply because it returns an ordered list. A true sorting function must return exactly the same elements it was given, only rearranged into order. There are several ways to formalize this “same elements” property. One common approach is to compare the counts of each possible element, requiring that the input and output lists match for every value.
Dafny
function Count<X(==)>(xs: List<X>, k: X): nat { match xs case Nil => 0 case Cons(x, tail) => (if k == x then 1 else 0) + Count(tail, k) }
8.1 Insertion Sort
Dafny
function InsertionSort(xs: IntList): IntList { match xs case Nil => Nil case Cons(x, tail) => var sortedTail := InsertionSort(tail); Insert(x, sortedTail) }
Dafny
function Insert(x: int, xs: IntList): IntList { match xs case Nil => Cons(x, Nil) case Cons(y, tail) => if x <= y then Cons(x, xs) else Cons(y, Insert(x, tail)) }
Notice that Insert is defined for any IntList, not only for sorted ones. However, we will only apply it to sorted lists. Therefore, when proving properties about Insert, we will restrict our lemmas to that case.
To prove that InsertionSort is correct, we must show two things: it produces an ordered list (using Ordered), and it preserves the number of elements (using Count).
Dafny
lemma InsertionSortOrdered(xs: IntList) ensures Ordered(InsertionSort(xs)) {match xs case Nil => // easycase Cons(x, tail) => // When functions get complicated, it can give us peace of mindto start by writing down what the function we're trying to // // prove something about does.var sortedTail := InsertionSort(tail); var result := Insert(x, sortedTail); assert result == InsertionSort(xs); // this assertion makes sure we copied the previous two lines // correctly in detail. But if we // We can start writing out the proof // think the situation looks pretty simple, we can start byin a way analogous to // calling the induction hypothesis function is used. // how the InsertionSortOrdered(tail); not enough. Not too surprising, actually. We'll need // This was to know something about what Insert does. We'll define a // lemma InsertOrdered (below) and call it from here. // InsertOrdered(x, sortedTail); }
Dafny
lemma InsertOrdered(x: int, xs: IntList) requires Ordered(xs) ensures Ordered(Insert(x, xs)) { // This proof is completed by automatic induction. }
Here is a lemma for the second task, showing that InsertionSort preserves element counts. The easiest way to write such a lemma is to parameterize it by an arbitrary element.
Dafny
lemma InsertionSortPreservesCounts(xs: IntList, k: int) ensures Count(xs, k) == Count(InsertionSort(xs), k) {match xs case Nil => case Cons(x, tail) => var sortedTail := InsertionSort(tail); var result := Insert(x, sortedTail); assert result == InsertionSort(xs); calc { Count(InsertionSort(xs), k); Count(Insert(x, sortedTail), k); { InsertCount(x, sortedTail, k); }if k == x then 1 else 0) + Count(sortedTail, k); ( { InsertionSortPreservesCounts(tail, k); }if k == x then 1 else 0) + Count(tail, k); ( Count(xs, k); } } lemma InsertCount(x: int, xs: IntList, k: int) ensures Count(Insert(x, xs), k) == (if k == x then 1 else 0) + Count(xs, k) { }