// Let's bring back our friend the list from last time. datatype List = Nil | Cons(head: T, tail: List) // Here are a few definitions copied over. function Length(xs: List): nat { match xs case Nil => 0 case Cons(_, tail) => 1 + Length(tail) } function Append(xs: List, ys: List): List ensures Length(Append(xs, ys)) == Length(xs) + Length(ys) { match xs case Nil => ys case Cons(x, tail) => Cons(x, Append(tail, ys)) } function ReverseAux(xs: List, acc: List): List { match xs case Nil => acc case Cons(x, tail) => ReverseAux(tail, Cons(x, acc)) } function Reverse(xs: List): List { ReverseAux(xs, Nil) } // Now something new: higher-order functions. function Map(f: A -> B, ls: List): List { match ls case Nil => Nil case Cons(x, ls') => Cons(f(x), Map(f, ls')) } lemma LengthMap(f: A -> B, ls: List) ensures Length(Map(f, ls)) == Length(ls) { } lemma MapAppend(f: A -> B, ls1: List, ls2: List) ensures Map(f, Append(ls1, ls2)) == Append(Map(f, ls1), Map(f, ls2)) { } // We can work with functions in an even more first-class way. function Identity(): A -> A { x => x } lemma MapIdentity(ls: List) ensures Map(Identity(), ls) == ls { } function Compose(f: A -> B, g: B -> C): A -> C { x => g(f(x)) } lemma MapCompose(f: A -> B, g: B -> C, ls: List) ensures Map(Compose(f, g), ls) == Map(g, Map(f, ls)) { } // This function is a classic for stepping through all elements of a list. function Foldl(f : (A, B) -> B, acc: B, ls: List): B { match ls case Nil => acc case Cons(x, ls') => Foldl(f, f(x, acc), ls') } // For instance, we can use Foldl to add up the elements of a list. lemma Foldl_example() ensures Foldl((n, m) => n + m, 0, Cons(1, Cons(2, Cons(3, Nil)))) == 3+(2+(1+0)) { } // The order in which we wrote that sum at the end is suggestive. // Foldl somewhat naturally applies list elements in reverse order. // Let's formalize that connection to list reversal. lemma FoldlReverseAux(ls: List, acc: List) ensures Foldl((h, t) => Cons(h, t), acc, ls) == ReverseAux(ls, acc) { } lemma FoldlReverse(ls: List) ensures Foldl((h, t) => Cons(h, t), Nil, ls) == Reverse(ls) { FoldlReverseAux(ls, Nil); } // Yup, it worked! // Here is a classic algebraic law relating Map and Fold. lemma FoldlMap(f: A -> B, g : (B, C) -> C, acc: C, ls: List) ensures Foldl(g, acc, Map(f, ls)) == Foldl((x, a) => g(f(x), a), acc, ls) { match ls case Nil => case Cons(x, ls') => calc { Foldl(g, acc, Map(f, ls)); == Foldl(g, acc, Map(f, Cons(x, ls'))); == Foldl(g, acc, Cons(f(x), Map(f, ls'))); == Foldl(g, g(f(x), acc), Map(f, ls')); == { FoldlMap(f, g, g(f(x), acc), ls'); } Foldl((x, a) => g(f(x), a), g(f(x), acc), ls'); == Foldl((x, a) => g(f(x), a), acc, ls); } // Or just the following line suffices: // FoldlMap(f, g, g(f(x), acc), ls'); } // Now back to binary search trees. // This time, using higher-order functions, we venture into making trees generic // in their key types! datatype Option = None | Some(value: T) datatype BST = Leaf | Node(key: K, value: V, left: BST, right: BST) // Note how Lookup (and many more operations to come) takes in a "less-than" // comparison function as an argument. function Lookup(lt: (K, K) -> bool, t: BST, k: K): Option { match t case Leaf => None case Node(k', v, l, r) => if k == k' then Some(v) else if lt(k, k') then Lookup(lt, l, k) else Lookup(lt, r, k) } function Insert(lt: (K, K) -> bool, t: BST, k: K, v: V): BST { match t case Leaf => Node(k, v, Leaf, Leaf) case Node(k', v', l, r) => if k == k' then Node(k, v, l, r) else if lt(k, k') then Node(k', v', Insert(lt, l, k, v), r) else Node(k', v', l, Insert(lt, r, k, v)) } // Nifty: we can prove this old law, without needing to know anything about the // implementation of 'lt'. lemma Lookup_Insert(lt: (K, K) -> bool, t: BST, k: K, v: V, k': K) ensures Lookup(lt, Insert(lt, t, k, v), k') == if k' == k then Some(v) else Lookup(lt, t, k') { } // OK, let's try a more stringent example. This function flips the tree on its // side. What should the effect be on the tree, interpreted as a proper binary // search tree? function Mirror(t: BST): BST { match t case Leaf => Leaf case Node(k, v, l, r) => Node(k, v, Mirror(r), Mirror(l)) } // This function transformer will be helpful. // It flips the senses of the two arguments, which is just what we need to // explain what Mirror accomplishes. function Flip(f: (A, B) -> C): (B, A) -> C { (b, a) => f(a, b) } // So Mirror preserves Lookup, when we flip the less-than test. // Note that, working through the proof, we realize that this property depends // on 'lt' acting like a real total order. lemma LookupMirror(lt: (K, K) -> bool, t: BST, k: K) requires forall a, b : K :: lt(a, b) == (!lt(b, a) && a != b) ensures Lookup(lt, t, k) == Lookup(Flip(lt), Mirror(t), k) { // This proof actually goes through automatically, // but we show a 'calc' that helped us realize what precondition to add. match t case Leaf => case Node(k', v', l, r) => if k == k' { } else if lt(k, k') { calc { Lookup(lt, t, k); == Lookup(lt, Node(k', v', l, r), k); == Lookup(lt, l, k); == { LookupMirror(lt, l, k); } Lookup(Flip(lt), Mirror(l), k); == { assert Flip(lt)(k', k); } Lookup(Flip(lt), Node(k', v', Mirror(r), Mirror(l)), k); == Lookup(Flip(lt), Mirror(Node(k', v', l, r)), k); == Lookup(Flip(lt), Mirror(t), k); } } else { } } // This property may seem strange to need to state explicitly: Lookup acts the // same, when passed either of two functions that act the same. However, that // property *does not* hold for any old Dafny function! Or, at least, Dafny // doesn't automatically conclude it for us; we need to inspect each function. // The intuitive explanation is that, sure, different sorting algorithms may // always produce equal outputs, but do we *really* want to consider those // algorithms equal? lemma LookupExtensional(lt: (K, K) -> bool, lt': (K, K) -> bool, t: BST, k: K) requires forall a, b : K :: lt(a, b) == lt'(a, b) ensures Lookup(lt, t, k) == Lookup(lt', t, k) { } // The precondition two lemmas back was a little delicate, so let's check it // works with a concrete key type. lemma LookupMirrorInt(t: BST, k: int) ensures Lookup((a, b) => a < b, t, k) == Lookup((b, a) => a < b, Mirror(t), k) { var lt := (a: int, b: int) => a < b; var lt' := (b: int, a: int) => a < b; LookupMirror(lt, t, k); LookupExtensional(Flip(lt), lt', Mirror(t), k); } // Let's relate trees and lists. // How many nodes in a tree? function Size(t: BST): nat { match t case Leaf => 0 case Node(_, _, l, r) => 1 + Size(l) + Size(r) } // Sorted list of keys from a tree // (Well, it's sorted if the tree is well-formed.) function Keys(t: BST): List { match t case Leaf => Nil case Node(k, _, l, r) => Append(Keys(l), Cons(k, Keys(r))) } lemma SizeKeys(t: BST) ensures Length(Keys(t)) == Size(t) { } // Checking if an element appears in a list predicate Member(x: A, xs: List) { match xs case Nil => false case Cons(y, xs') => x == y || Member(x, xs') } lemma MemberAppend(x: A, xs1: List, xs2: List) ensures Member(x, Append(xs1, xs2)) == (Member(x, xs1) || Member(x, xs2)) { } // OK, now we're ready to characterize well-formedness of BSTs. // This type captures a range of keys, optionally omitting a bound on either end. type range = (Option, Option) predicate InRange(lt: (K, K) -> bool, n: K, r: range) { && (r.0.None? || lt(r.0.value, n)) && (r.1.None? || lt(n, r.1.value)) } // Now we can redefine our well-formedness predicate from last time, but now // it's type-generic. predicate WellFormedBST(lt: (K, K) -> bool, t: BST, rng: range) { match t case Leaf => true case Node(k, _, l, r) => && InRange(lt, k, rng) && WellFormedBST(lt, l, (rng.0, Some(k))) && WellFormedBST(lt, r, (Some(k), rng.1)) } // Let's check that well-formedness captures the intuitive property. // Note that this property depends on a different algebraic property of 'lt': // *transitivity*. lemma RangeKeys(lt: (K, K) -> bool, t: BST, rng: range, k: K) requires WellFormedBST(lt, t, rng) requires Member(k, Keys(t)) requires forall a, b, c :: lt(a, b) && lt(b, c) ==> lt(a, c) ensures InRange(lt, k, rng) { match t case Leaf => case Node(k', v', l, r) => // This proof goes through with automatic induction and one call to MemberAppend: MemberAppend(k, Keys(l), Cons(k', Keys(r))); // Here's a more detailed proof that does not rely on automatic induction: /* assert Member(k, Keys(l)) || k == k' || Member(k, Keys(r)) by { assert Member(k, Append(Keys(l), Cons(k', Keys(r)))); MemberAppend(k, Keys(l), Cons(k', Keys(r))); } if case Member(k, Keys(l)) => RangeKeys(lt, l, (rng.0, Some(k')), k); case k == k' => case Member(k, Keys(r)) => RangeKeys(lt, r, (Some(k'), rng.1), k); */ } // The following we probably won't have time for in class, especially given how // long some of the proofs are, but they should be educational to demonstrate // more-intricate lemma sequences. // Working up to further BST properties, let's define what it means for two // lists to share no elements. predicate Disjoint(ls1: List, ls2: List) { match ls1 case Nil => true case Cons(x, ls1') => !Member(x, ls2) && Disjoint(ls1', ls2) } lemma DisjointCons(ls1: List, x: A, ls2: List) ensures Disjoint(ls1, Cons(x, ls2)) == (!Member(x, ls1) && Disjoint(ls1, ls2)) { } // Checking if a list is duplicate-free. predicate NoDup(xs: List) { match xs case Nil => true case Cons(x, xs') => !Member(x, xs') && NoDup(xs') } // That predicate is helpful to characterize the interaction of NoDup and // Append. lemma NoDupAppend(ls1: List, ls2: List) ensures NoDup(Append(ls1, ls2)) == (Disjoint(ls1, ls2) && NoDup(ls1) && NoDup(ls2)) { match ls1 case Nil => case Cons(v, ls1') => calc { NoDup(Append(ls1, ls2)); == NoDup(Append(Cons(v, ls1'), ls2)); == NoDup(Cons(v, Append(ls1', ls2))); == !Member(v, Append(ls1', ls2)) && NoDup(Append(ls1', ls2)); == { NoDupAppend(ls1', ls2); } !Member(v, Append(ls1', ls2)) && Disjoint(ls1', ls2) && NoDup(ls1') && NoDup(ls2); == { MemberAppend(v, ls1', ls2); } !Member(v, ls1') && !Member(v, ls2) && Disjoint(ls1', ls2) && NoDup(ls1') && NoDup(ls2); } } // Here's an even-more-direct but useful property. lemma DisjointAppend(ls1: List, ls2: List, ls: List) ensures Disjoint(Append(ls1, ls2), ls) == (Disjoint(ls1, ls) && Disjoint(ls2, ls)) { } // Now let's prove an involved kind of "strengthened induction hypothesis" // allowing us to conclude that two trees have disjoint keys. lemma BSTDisjoint(lt: (K, K) -> bool, t1: BST, t2: BST, lower1: Option, upper1: K, lower2: K, upper2: Option) requires WellFormedBST(lt, t1, (lower1, Some(upper1))) requires WellFormedBST(lt, t2, (Some(lower2), upper2)) requires lt(upper1, lower2) || upper1 == lower2 // That last one is critical: the two trees' ranges are ordered in a // particular way, overlapping in *at most one* element, and given how we // interpret range ends strictly, that implies *no overlap of elements*. requires forall a, b, c :: lt(a, b) && lt(b, c) ==> lt(a, c) requires forall a :: !lt(a, a) // New property: 'lt' is *irreflexive*. ensures Disjoint(Keys(t1), Keys(t2)) { match t1 case Leaf => case Node(k, v, l, r) => // Note a little *proof by contradiction* here, // to show how we know a given fact doesn't hold. if Member(k, Keys(t2)) { assert InRange(lt, k, (lower1, Some(upper1))); assert lt(k, upper1); assert InRange(lt, k, (Some(lower2), upper2)) by { RangeKeys(lt, t2, (Some(lower2), upper2), k); } assert lt(lower2, k); assert lt(k, k); // by transitivity assert false; // by antireflexivity } assert !Member(k, Keys(t2)); calc { Disjoint(Append(Keys(l), Cons(k, Keys(r))), Keys(t2)); == { DisjointAppend(Keys(l), Cons(k, Keys(r)), Keys(t2)); } !Member(k, Keys(t2)) && Disjoint(Keys(l), Keys(t2)) && Disjoint(Keys(r), Keys(t2)); == { BSTDisjoint(lt, l, t2, lower1, k, lower2, upper2); } !Member(k, Keys(t2)) && true && Disjoint(Keys(r), Keys(t2)); == { BSTDisjoint(lt, r, t2, Some(k), upper1, lower2, upper2); } !Member(k, Keys(t2)) && true && true; == true && true && true; == true; } } // Finally, we pull it together into one pleasingly simple property: well-formed // BSTs have no duplicate keys. lemma NoDupKeys(lt: (K, K) -> bool, t: BST, rng: range) requires WellFormedBST(lt, t, rng) requires forall a :: !lt(a, a) requires forall a, b, c :: lt(a, b) && lt(b, c) ==> lt(a, c) ensures NoDup(Keys(t)) { match t case Leaf => case Node(k, v, l, r) => var L, R := Keys(l), Keys(r); var R' := Cons(k, R); calc { NoDup(Keys(t)); NoDup(Append(L, R')); { NoDupAppend(L, R'); } Disjoint(L, R') && NoDup(L) && NoDup(R'); { DisjointCons(L, k, R); } !Member(k, L) && Disjoint(L, R) && NoDup(L) && NoDup(R'); !Member(k, L) && Disjoint(L, R) && NoDup(L) && !Member(k, R) && NoDup(R); { NoDupKeys(lt, l, (rng.0, Some(k))); } !Member(k, L) && Disjoint(L, R) && true && !Member(k, R) && NoDup(R); { NoDupKeys(lt, r, (Some(k), rng.1)); } !Member(k, L) && Disjoint(L, R) && true && !Member(k, R) && true; } assert !Member(k, L) by { if Member(k, Keys(l)) { RangeKeys(lt, l, (rng.0, Some(k)), k); } } assert !Member(k, R) by { if Member(k, Keys(r)) { RangeKeys(lt, r, (Some(k), rng.1), k); } } assert Disjoint(L, R) by { BSTDisjoint(lt, l, r, rng.0, k, k, rng.1); } }