Topic: Proof by Induction Date: Oct. 16, 2009 Number: 12 Examples: DigraphMap.hs, Induction.hs Reading: Chap. 11 -- Finish tree showTree, etc. from last notes. ---- Look at Digraph.hs and FA.hs. Note uses of HOF. ---- Look at DigraphMap.hs. Alternate version of Digraph that uses a Map instead of a list to locate vertices. Will be much faster, as we will see later. First, note: import qualified Data.Map as Map Typical way to avoid name collisions. When we say "Map.insert" Haskell will know we mean the map insert rather than the Digraph insert (or any other insert). Note change in type of Digraph: type AdjList v e = [(v, e)] type Digraph v e = Map.Map v (AdjList v e) type Edge v e = (v, v, e) What is Map.Map? The Map type imported qualified as Map. The modified empty function is: -- Creates an empty Digraph empty :: Digraph v e empty = Map.empty To insert a vertex we just add it to the map with an empty AdjList: -- Inserts vertex vert into digraph g. insertVertex :: (Ord v) => v -> Digraph v e -> Digraph v e insertVertex vert g = Map.insert vert [] g Note - this replaces what was there, if it was! -- Insert edge into digraph. Source vertex must be present. insertEdge :: (Ord v) => Edge v e -> Digraph v e -> Digraph v e insertEdge e@(source, dest, label) g | Map.member source g && Map.member dest g = Map.insertWith (++) source [(dest, label)] g | otherwise = error "insertEdge: edge endpoint not in the graph" Note - insertWith (++) will append the new pair (as a list) to the head of the old AdjList. Couldn't use (:), because of the way the insertWith is defined. f is (b -> b -> b). -- Create a digraph out of a list of vertices and a list of edges makeDigraph :: (Ord v) => [v] -> [Edge v e] -> Digraph v e makeDigraph vertices edges = foldr insertEdge (foldr insertVertex empty vertices) edges Completely unchanged. But nice use of HOF. -- Get the adjacency list associated with vertex vert in graph getAdj :: (Ord v) => v -> Digraph v e ->AdjList v e getAdj vert graph = case Map.lookup vert graph of Nothing -> error "getAdj: vertex not in graph" (Just adjList) -> adjList Uses Map.lookup, does handles the Maybe return type. Same as before. -- Tests to see if a vertex is in the graph vertexInGraph :: (Ord v) => v -> Digraph v e -> Bool vertexInGraph vert g = Map.member vert g Just use Map.member. All in all, might be EASIER with a map than with a list. -- Computation by Calculation Mentioned first day that we can use substitution rule - anywhere that a name is used, can replace it by it value. Can use it to prove things. For example, area of a square with side s should be s^2. Is it? We have the following definitions in the Shape module: square s = Rectangle s s area (Rectangle s1 s2) = s1*s2 Using this, we can calculate: area (square s) => ( unfold square) area (Rectangle s s) => (unfold area) s * s => (arithmetic) s^2 => means "derives in one step". Unfortunately, fold and unfold have NOTHING to do with foldl and foldr. "unfold" means replace an expression on the left side of the "=" in a function definition by the corresponding expression on the right side. "fold" means go the other way - replace a rhs expression by its corresponding lhs. This is quite powerful. Becomes even more powerful if we use proof by induction. -- Proof by induction A very powerful technique for proving recursive programs correct is proof by induction. Book is correct - recursion and proof by induction are very closely linked. In fact, one way to view proof by induction is as a proof-generating function. This function would test to see if the thing it is asked to prove is a base case, and if so would print out a proof directly. If the thing it is asked to prove is not a base case, it would break the problem into one or more subproblems. It would call itself recursively on each subproblem, generating proofs that each of these lemmas is true. Given proofs of the lemmas, it would then prove the bigger case that it was asked to prove. Would produce a horribly long and detailed proof. Nobody would ever want to read it. But this way of thinking about induction - prove base cases directly, prove more complex cases by recursively breaking down the problem and proving the subcases recursively - is a useful way to approach induction. As an example, consider the sum of the integers from 0 to n. If we call this sum Sum(n), then Sum(n) = n(n+1)/2. To prove this by standard induction on the integers, we do two things: 1) Prove it for the base case. That is when n = 0. (Smaller n are not in the domain of the function.) 2) Assume that the formula is true for n-1. That is, Sum(n-1) = (n-1)n/2. Use this fact to prove that the formula for Sum(n) is correct. Base case: Sum(0) = 0 = 0*1/2, so Sum(n) = n(n+1)/2 for n = 0. Inductive case. Sum(n) = Sum(n-1) + n. Using the inductive hypothesis Sum(n-1) = (n-1)n/2, we can substitute in and get Sum(n) = (n-1)n/2 + n = (n^2 - n)/2 + 2n/2 = n(n+1)/2. Thus we have proved by induction that Sum(n) = n(n+1)/2. Note that we assumed that Sum(n-1) = (n-1)n/2. But we could instead prove this recursively. The following program does this. -- Proves that the sum of integers from 1 to n is n(n+1)/2. proveSum :: Int -> IO() proveSum n = if n == 0 then do putStrLn "We verify that 0*1/2 = 0," putStrLn " so Sum(n) = n(n+1)/2 holds for n = 0." else do putStrLn ("To verify that Sum(n) = n(n+1)/2 holds for n = " ++ show n) putStrLn (" we first show that it holds for n = " ++ (show (n-1)) ++ ".") proveSum (n-1) putStrLn ("Having proved that Sum(" ++ (show (n-1)) ++ ") = " ++ (show (n-1)) ++ "*" ++ (show n) ++ "/2 = " ++ (show ((n-1)*n `div` 2))) putStrLn (" we note that " ++ (show ((n-1)*n `div` 2)) ++ " + " ++ (show n) ++ " = " ++ (show (n*(n+1) `div` 2)) ++ " = " ++ (show n) ++ "*" ++ (show (n+1)) ++ "/2.") putStrLn ("Thus Sum(n) = n(n+1)/2 holds for n = " ++ (show n) ++ ".") The print statements are messy, but the idea is simple. Instead of assuming the inductive hypothesis we make a recursive call to prove it in gory detail. The output from proveSum 4 is: To verify that Sum(n) = n(n+1)/2 holds for n = 4 we first show that it holds for n = 3. To verify that Sum(n) = n(n+1)/2 holds for n = 3 we first show that it holds for n = 2. To verify that Sum(n) = n(n+1)/2 holds for n = 2 we first show that it holds for n = 1. To verify that Sum(n) = n(n+1)/2 holds for n = 1 we first show that it holds for n = 0. We verify that 0*1/2 = 0, so Sum(n) = n(n+1)/2 holds for n = 0. In what follows we use the fact that Sum(n) = Sum(n-1) + n. Having proved that Sum(0) = 0*1/2 = 0 we note that 0 + 1 = 1 = 1*2/2. Thus Sum(n) = n(n+1)/2 holds for n = 1. Having proved that Sum(1) = 1*2/2 = 1 we note that 1 + 2 = 3 = 2*3/2. Thus Sum(n) = n(n+1)/2 holds for n = 2. Having proved that Sum(2) = 2*3/2 = 3 we note that 3 + 3 = 6 = 3*4/2. Thus Sum(n) = n(n+1)/2 holds for n = 3. Having proved that Sum(3) = 3*4/2 = 6 we note that 6 + 4 = 10 = 4*5/2. Thus Sum(n) = n(n+1)/2 holds for n = 4. Because a recursive program exists that will print out a proof of the formula for any n, the formula must hold for all n. I will not ask you to write such a recursive program. But thinking this way makes proof by induction easier. We will be using strong structural induction in this course. What this means is that the general approach to proving something by induction is: 1) The base cases are whatever can't be broken down recursively. Typically that is an empty list or a leaf in a tree, but the basic rule is if the problem is such that the recursive decomposition won't work, then you have a base case that must be proved directly. Note that this can lead to somewhat counter-intuitive definitions of "base cases". It is possible to prove inductively that every integer greater than 1 is a product of primes. The proof begins, "If the number is already prime, then it is the product of a single prime - itself." Thus in this proof the base cases are ALL the prime numbers. You may not be used to thinking about having an infinite number of base cases, but it makes sense to think of things this way. 2) If you can break recursively break the case you are trying to prove into smaller cases, do so. Assume that the thing that you want to prove is true for ALL smaller cases. For lists, would mean all sublists. For trees, would mean all subtrees of the given tree. (This assumption is called the inductive hypothesis.) Then use this assumption to prove it for the big case. Two differences between what you are probably used to and what we are doing. 1) Structural induction means that we are happy to do induction on data structures by taking substructures, which are by definition smaller. We don't need to tie the proof to integers. (We could do induction on the size of the substructure, but this adds an unnecessary complication.) 2) Strong induction means that we assume true for ALL smaller cases, not just for the case that is "one smaller". Using "one smaller" works for lists and integers, but when we want to prove something on trees, or prove something like mergesort where we split the list in half, or quicksort where we break into two smaller lists which may be of very different sizes, we need to deal with structures that are not one smaller than the original. We combine "proof by computation" with proof by induction. Let's start with a function on trees, specifically trees with data at leaves: data Tree a = Leaf a | Branch (Tree a) (Tree a) We look at the treeSize function: treeSize :: Tree a -> Integer treeSize (Leaf x) = 1 treeSize (Branch t1 t2) = treeSize t1 + treeSize t2 We want to show that this is correct. This proof a bit less formal than the ones in the book - the book always shows that two programs are equivalent. Here we compare to a "natural understanding" of what the program means. Here what we prove is: treeSize t calculates the number of leaves in tree t. Base case: treeSize (Leaf x) = 1 There is exactly one leaf in a tree consisting of a leaf, so treeSize is correct. Inductive case: Our inductive hypothesis is, for any proper subtree t' of t, treeSize t' computes the number of leaves in t'. In particular, if t = Branch t1 t2, then treeSize t1 computes the number of leaves in t1 and treeSize t2 computes the number of leaves in t2. treeSize (Branch t1 t2) => (unfold treeSize) treeSize t1 + treeSize t2 => (inductive hypothesis) (#leaves in t1) + (#leaves in t2) We can now argue that this is correct. Because every leaf of t is either a leaf of t1 or a leaf of t2, each is counted exactly once. The more formal way to do this would be to come up with two different functions and prove that they are equivalent. So: treeSize = length . fringe where fringe :: Tree a -> [a] fringe (Leaf x) = [x] fringe (Branch t1 t2) = fringe t1 ++ fringe t2 and length [] = 0 length (x:xs) = 1 + length xs We will do this formally, but first some warm-ups. ----- Basic framework of a formal inductive proof to prove two expressions equivlalent: one expression => (unfold one or more times) intermediate expression => (manipulate) expression where IH can be applied => (apply IH) intermediate expression => (manipulate) expression matching right side(s) of definition(s) => (fold one or more times) intermediate expression => (manipulate) other expression This is only a rough framework. Some of the steps may be skipped. Folding and unfolding may be intermixed, and may require more intermediate manipulation. But it expresses the overall approach. ----- pp. 138 and 139 have lots of useful identities involving lists and map, ++, take, drop, reverse, and foldl and foldr. We will prove a few, and assign some others for a short assignment. First we show that: xs ++ [] = xs Remember that ++ is defined: [] ++ ys = ys (x:xs) ++ ys = x:(xs ++ ys) Our induction is on xs. The base case is xs = []. Then [] ++ [] => (unfold ++) [] Thus the identity is true for the base case xs = []. For the inductive case, let xs = x:xs'. We assume that the identity is true for sublists of xs, so in particular: xs' ++ [] = xs' Then x:xs' ++ [] => (unfold ++) x:(xs' ++ []) => (inductive hypothesis) x:xs' => (definition) xs We have proved that xs ++ [] = xs using proof by induction. Second, we prove associativity of ++: (xs ++ ys) ++ zs = xs ++ (ys ++ zs) So what do we do induction on? xs looks most promising - seem to ":" from left. Base case: xs = []. Then: ([] ++ ys) ++ zs => (unfold ++) ys ++ zs => (fold ++) [] ++ (ys + zs) So base case works. What about inductive case? Let xs = x:xs', and assume that the property is true for all shorter lists. In particular, for xs': (xs' ++ ys) ++ zs = xs' ++ (ys ++ zs). So we start with: (x:xs' ++ ys) ++ zs => (unfold ++) x:(xs' ++ ys) ++ zs => (unfold ++) x:((xs' ++ ys) ++ zs) => (inductive hypothesis) x:(xs' ++ (ys ++ zs)) => (fold ++) x:xs' ++ (ys ++ zs) This is what we set out to prove. So ++ is associative. --- Back to treeSize Same idea as above, but now can do everything formally. Base case: Leaf x (length . fringe) (Leaf x) => (unfold .) length (fringe (Leaf x)) => (unfold fringe) length [x] => (unfold length) 1 + length [] => (unfold length) 1 + 0 => (arithmetic) 1 => (fold treeSize) treeSize (Leaf x) So base case is true. Assume property true for all smaller trees. In particular, if t = Branch t1 t2, then treeSize t1 = (length . fringe) t1 treeSize t2 = (length . fringe) t2 treeSize (Branch t1 t2) => (unfold treeSize) treeSize t1 + treeSize t2 => (inductive hypothesis) (length . fringe) t1 + (length . fringe) t2 => (unfold .) length (fringe t1) + length (fringe t2) => (fold length of ++, p. 133) length (fringe t1 ++ fringe t2) => (fold fringe) length (fringe (Branch t1 t2)) => (fold .) (length . fringe) (Branch t1 t2) So we have proved it. --- Another example. Finish up Proof by Induction by doing one more example: take n xs ++ drop n xs = xs (non-neg. n, finite xs) where (for non-negative n): take 0 _ = [] take _ [] = [] take n (x:xs) = x : take (n-1) xs drop 0 xs = xs drop _ [] = [] drop n (_:xs) = drop (n-1) xs [] ++ ys = ys (x:xs) ++ ys = x:(xs ++ ys) So what do we do induction on? 2 choices: n and xs. Try xs. (We should be suspicious that we also need to prove something about n, because otherwise it wouldn't appear in the recursive definition!) Base case: xs = [] take n [] ++ drop n [] => (unfold take and drop) [] ++ [] => (unfold ++) [] Want to prove it for some xs = x:xs' and some n >= 0. So assume true for xs' and n' >= 0. take n (x:xs') ++ drop n (x:xs') => (unfold take and drop) x:take (n-1) xs' ++ drop (n-1) xs' => (unfold ++) x:(take (n-1) xs' ++ drop (n-1) xs') => (inductive hyp.) x:xs' Problem: we applied the inductive hypothesis to formulas involving (n-1). The inductive hypothesis only applies when n >= 0, so we have really only proved the formula for n-1 >= 0, or n > 0. What about n = 0? Must prove that separately. Second base case: when n = 0. take 0 xs ++ drop 0 xs => (unfold take and drop) [] ++ xs => (unfold ++) xs Note: book talks about strict functions. When computing, if reach an error or compute forever the value of the computation is (draw the symbol instead). A function is strict if when we compute f of a bunch of parameters, f is bottom if any of its parameters are bottom. Non-strict functions: Classical ones are && and ||. That is the whole purpose of expressions like: x /= 0 && 1/x > 5 && and || are strict in their first parameter, but not in the second. The second parameter can be bottom, and as long as the first decides the result, it is never evaluated.