Declarative Programming Answers to workshop exercises set 8. QUESTION 1 Define versions of map for the following data types: >data Ltree a = LTLeaf a | LTBranch (Ltree a) (Ltree a) >data Cord a = Nil | Leaf a | Branch (Cord a) (Cord a) >data Mtree a = Mnode a [Mtree a] ANSWER >ltree_map :: (a -> b) -> Ltree a -> Ltree b >ltree_map f (LTLeaf a) = LTLeaf (f a) >ltree_map f (LTBranch l r) = LTBranch (ltree_map f l) (ltree_map f r) >cord_map :: (a -> b) -> Cord a -> Cord b >cord_map f Nil = Nil >cord_map f (Leaf a) = Leaf (f a) >cord_map f (Branch l r) = Branch (cord_map f l) (cord_map f r) For Mtrees we can use map (for lists) to process the list of subtrees. >mtree_map :: (a -> b) -> Mtree a -> Mtree b >mtree_map f (Mnode a mts) = Mnode (f a) (map (mtree_map f) mts) QUESTION 2 In "foldr f b" we essentially replace [] by b and : by f. For example, foldr f (+) 0 [1, 2] = foldr f (+) 0 (1:2:[]) = foldr f (+) 0 ((:) 1 ((:) 2 [])) = ((+) 1 ((+) 2 0)) = 1 + (2 + 0) = 3. Similarly, foldr_tree f b (from the previous workshop) replaces Empty by b and Node by f (which takes three arguments). Define versions of foldr for the types in the previous question. ANSWER In all these foldr versions, we have to make some decision on the order of the arguments for the different data constructors. Here we use the reverse of the order they are given above, so base cases are last, like foldr for lists and foldr_tree. Note the code is similar to the versions of map, but we use these extra arguments in place of data constructors on the right sides of equations. >ltree_foldr :: (b -> b -> b) -> (a -> b) -> Ltree a -> b >ltree_foldr _fb fl (LTLeaf a) = fl a >ltree_foldr fb fl (LTBranch l r) = > fb (ltree_foldr fb fl l) (ltree_foldr fb fl r) >cord_foldr :: (b -> b -> b) -> (a -> b) -> b -> Cord a -> b >cord_foldr _fb _fl n Nil = n >cord_foldr _fb fl _n (Leaf a) = fl a >cord_foldr fb fl n (Branch l r) = > fb (cord_foldr fb fl n l) (cord_foldr fb fl n r) For Mtrees we need arguments for Mnode, : and []. We can use foldr and map (for lists) to process the list of subtrees: foldr fc n (map (mtree_foldr fn fc n) mts) but the map and fold can be combined: >mtree_foldr :: (a -> c -> b) -> (b -> c -> c) -> c -> Mtree a -> b >mtree_foldr fn fc n (Mnode a mts) = > fn a (foldr (fc.(mtree_foldr fn fc n)) n mts) Note the most general type has three type variables: (1) the type of the original tree elements, (2) the final result type, and (3) the type that a list of subtrees is converted into. Some examples for testing: >lt1 = LTBranch (LTLeaf 2) (LTLeaf 3) >c1 = Branch (Leaf 2) (Branch (Leaf 3) Nil) >mt1 = Mnode 2 [Mnode 3 []] >ltree_sum = ltree_foldr (+) id >cord_sum = cord_foldr (+) id 0 >mtree_sum = mtree_foldr (+) (+) 0 QUESTION 3 Recall the following functions from lectures, for concatenating a list of lists and converting a cord into a list: >concatr = foldr (++) [] -- Efficient >concatl = foldl (++) [] -- Inefficient >cord_to_list :: Cord a -> [a] -- Inefficient >cord_to_list Nil = [] >cord_to_list (Leaf x) = [x] >cord_to_list (Branch a b) = > (cord_to_list a) ++ (cord_to_list b) >cord_to_list2 :: Cord a -> [a] -- Efficient >cord_to_list2 c = cord_to_list' c [] >cord_to_list' :: Cord a -> [a] -> [a] -- Arg 2 is an accumulator >cord_to_list' Nil rest = rest >cord_to_list' (Leaf x) rest = x:rest >cord_to_list' (Branch a b) rest = > cord_to_list' a (cord_to_list' b rest) Recall the reason for the relative (in)efficiency is that ((a++b)++c) has to copy the elements and cons cells of the list `a' twice, whereas (a++(b++c)) only needs to do it once. The cost of ++ depends on the length of its first argument but not its second argument. Define four versions of concat_rev, which concatenates the lists in reverse order (the inner lists are not reversed, for example concat_rev [[1,2],[3]] = [3,1,2]) using (1) foldr, (2) foldl, (3) recursion without an accumulator, and (4) recursion with an accumulator. Determine which versions are efficient. How would you code cord_to_list_rev, which converts a cord to a list, but in the reverse order? Code it directly rather than creating an in-order list and then reversing it. What is the relationship between foldr for cords and the code above and in what way is the code similar to both foldl and foldr? ANSWER For the fold versions we use the higher order flip function which swaps the order of the arguments to (in this case) ++. This also swaps the relative efficiency: while the time taken by ++ depends on the length of its first argument, the time taken by flip (++) depends on the length of its second argument (since this becomes ++'s first argument). >concat_rev1 = foldr (flip (++)) [] -- Inefficient >concat_rev2 = foldl (flip (++)) [] -- Efficient Foldl for lists is like using an accumulator; the following is efficient: >concat_rev3 xss = concat_rev3_acc xss [] >concat_rev3_acc [] acc = acc >concat_rev3_acc (xs:xss) acc = concat_rev3_acc xss (xs++acc) Simple recursion without an accumulator is like foldr for lists; the following is inefficient: >concat_rev4 [] = [] >concat_rev4 (xs:xss) = (concat_rev4 xss) ++ xs Because cords are a tree structure, the foldr version will still do lots on inefficient concatenation (at least in the worst case). To get an efficient version we must use an accumulator but traverse the tree in a different order to that above. In fact, we need to traverse the tree from left to right (the above version traverses it from right to left). All we need to do is exchange a and b in the recursive calls in the last line of the definition. >cord_to_list_rev :: Cord a -> [a] -- Efficient >cord_to_list_rev c = cord_to_list_acc c [] >cord_to_list_acc :: Cord a -> [a] -> [a] >cord_to_list_acc Nil rest = rest >cord_to_list_acc (Leaf x) rest = x:rest >cord_to_list_acc (Branch a b) rest = > cord_to_list_acc b (cord_to_list_acc a rest) The simple (inefficient) definition of cord_to_list is essentially the same as cord_to_list = cord_foldr (++) (:[]) [] The efficient cord_to_list2 is like foldl in its use of an accumulator but also like foldr in that it traverses from right to left. QUESTION 4 The definition of the cord data type presented in lectures (see Q1 above) can represent the same nonempty sequence of items in more than one way. For example, the sequence [1, 2, 3] can be represented as Branch (Leaf 1) (Branch (Leaf 2) (Leaf 3)) or as Branch (Branch (Leaf 1) (Leaf 2)) (Leaf 3) Without this flexibility, the operation to concatenate two cords couldn't be implemented in constant time. However, this type also has unnecessary flexibility. For example, it can represent the empty sequence in more than one way, including Nil, Branch Nil Nil and Branch Nil (Branch Nil Nil). Design a version of the cord data type that has only way to represent the empty sequence. ANSWER One possible design is >data Cord' a > = EmptyCord > | NonEmptyCord (CordNode a) >data CordNode a > = Leaf' a > | Branch' (CordNode a) (CordNode a)