Sometimes, we write programs that take two inputs with complicated data definitions, such as two lists. Depending on the program we're trying to write, we may or may not need to use a more complicated template. Let's explore this by considering several examples. 1. append ;; append : list-of-nums list-of-nums -> list-of-nums ;; produces a list with all elements of the first followed by those of ;; the second, in order (define (append lon1 lon2) (cond [(empty? lon1) lon2] [(cons? lon1) (cons (first lon1) (append (rest lon1) lon2))])) In this example, we don't need to look inside lon2, we just need to use it at an appropriate time. Using the template on lon1 only therefore suffices. 2. make-points ;; make-points : list-of-nums list-of-nums -> list-of-posn given two ;; lists of the same length, returns a list of points formed pairwise ;; from the two input lists (define (make-points x-coords y-coords) ...) Here, we will need to traverse the y-coords. However, note that the two input lists are assumed to have the same length. With this knowledge, what might we use for a template? (define (make-points x-coords y-coords) (cond [(empty? x-coords) ...] [else ... (first x-coords) ... (first y-coords) ... (make-points (rest x-coords) (rest y-coords)) ...])) Why does this template make sense? Since the lists must have the same length, either both lists are empty or neither list is empty. So the template needs only two cases. Furthermore, since the rests of the lists must have the same length, it makes sense to call make-points recursively on the rests of the lists. The final function looks like: (define (make-points x-coords y-coords) (cond [(empty? x-coords) empty] [(cons? x-coors) (cons (make-posn (first x-coords) (first y-coords)) (make-points (rest x-coords) (rest y-coords)))])) 3. merge ; merge : list-of-numbers list-of-numbers -> list-of-numbers ; merges two lists (sorted in ascending order) into one list (sorted ; in ascending order) (define (merge alon1 alon2) ...) Here, we cannot assume anything about the lengths of the two lists, and we need to look at all elements of both lists. Therefore, neither of our two previous cases apply. Let's start by making up examples of this function. How many examples do we need at a minimum? Two unrelated inputs, each with two possible cases in their data definitions means at least 4 examples. (merge empty empty) = empty (merge empty (cons 1 (cons 5 empty))) = (cons 1 (cons 5 empty)) (merge (cons 1 (cons 5 empty)) empty) = (cons 1 (cons 5 empty)) (merge (cons 1 (cons 5 empty)) (cons 6 (cons 7 empty))) = (merge (cons 1 (cons 6 empty)) (cons 5 (cons 7 empty))) = (merge (cons 5 (cons 6 empty)) (cons 1 (cons 7 empty))) = (cons 1 (cons 5 (cons 6 (cons 7 empty)))) These examples show that our program must be able to handle all four cases. Thus, our template will need a cond with four cases. What questions distinguish these cases? Let's use a table to work them out. (empty? alon2) (cons? alon2) ----------------------------------------------------------- (empty? alon1) (and (empty? alon1) (and (empty? alon1) (empty? alon2)) (cons? alon2)) (cons? alon1) (and (cons? alon1) (and (cons? alon1) (empty? alon2)) (cons? alon2)) With the table, we can now write a general template for two lists. Ignoring the recursion, we have (define (f alon1 alon2) (cond [(and (empty? alon1) (empty? alon2)) ...] [(and (empty? alon1) (cons? alon2)) ... (first alon2) ... (rest alon2) ...] [(and (cons? alon1) (empty? alon2)) ... (first alon1) ... (rest alon1) ...] [(and (cons? alon1) (cons? alon2)) ... (first alon1) ... (first alon2) ... (rest alon1) ... (rest alon2) ...])) What about the recursions? In the second case, where alon1=empty, we can recur on (rest alon2), but what other list could we use? Well, if we recur on (f alon1 (rest alon2)), which effectively recurs on just the second list, since the first is already empty. The third case is similar. In the last case, we have several options: - (f alon1 (rest alon2)) - (f (rest alon1) alon2) - (f (rest alon1) (rest alon2)) There are problems that would use each of these. Since we don't know which one we need, we show all three in the template: (define (f alon1 alon2) (cond [(and (empty? alon1) (empty? alon2)) ...] [(and (empty? alon1) (cons? alon2)) ... (first alon2) ... (f alon1 (rest alon2)) ...] [(and (cons? alon1) (empty? alon2)) ... (first alon1) ... (f (rest alon1) alon2) ...] [(and (cons? alon1) (cons? alon2)) ... (first alon1) ... (first alon2) ... (f alon1 (rest alon2)) ... (f (rest alon1) alon2) ... (f alon1 alon2) ...])) Now let's finish merge: (define (merge alon1 alon2) (cond [(and (empty? alon1) (empty? alon2)) empty] [(and (empty? alon1) (cons? alon2)) alon2] [(and (cons? alon1) (empty? alon2)) alon1] [(and (cons? alon1) (cons? alon2)) (cond [(< (first alon1) (first alon2)) (cons (first alon1) (merge (rest alon1) alon2))] [else (cons (first alon2) (merge alon1 (rest alon2)))])])) This could be shortened by combining the first and (second or third) cases. Doing so is not necessarily a "simplification", because it obscures the structure. On such examples, you should provide this long version, and optionally a shortened version. These three programs demonstrate three different kinds of programs that process two complex inputs: 1. one complex input need not be traversed entirely 2. both inputs must be traversed, both of the same length 3. (the general case) both inputs must be traversed, different lengths For practice distinguishing between and writing these kinds of programs, try the following exercises: ;; list-pick : list-of-symbols N[>=1] -> symbol or false return the ;; nth symbol in the list (counting from 1) or false if there is no ;; nth item Which kind is this? 1, because there is only one list input? No, because you also need to recur on the structure of the natural number. 2, because you want to recur on the list and number in lock-step? Almost, but we aren't assuming that they are of the same "length". 3? It must be the general case. (list-pick empty 1) = false (list-pick (cons 'hi empty) 1) = 'hi (list-pick empty 5) = false (list-pick (cons 'hi empty) 2) = false (define (list-pick alos n) (cond [(and (= n 1) (empty? alos)) false] [(and (> n 1) (empty? alos)) false] [(and (= n 1) (cons? alos)) (first alos)] [(and (> n 1) (cons? alos)) (list-pick (sub1 n) (rest alos))])) Once again, we could combine the first and second cases. ;; subset? : list-of-symbol list-of-symbol -> boolean ;; returns true if every symbol in first list is in the second list, ;; otherwise returns false. This seems to clearly fall into the general case, since we may need to look at all the elements of both lists, and the lists are not necessarily of the same length. (define (subset? alos1 alos2) (cond [(and (empty? alos1) (empty? alos2)) true] [(and (empty? alos1) (cons? alos2)) true] [(and (cons? alos1) (empty? alos2)) false] [(and (cons? alos1) (cons? alos2)) (and ... (subset? alos1 (rest alos2)))])) where the ... returns whether (first alos1) is in alos2. That sounds complicated, so let's use a helper function. (We don't need the helper, but it would make subset? overly complicated.) ; member? : symbol list-of-symbols -> boolean ; returns whether the symbol is in the list (define (member? s alos) (cond [(empty? alos) false] [(cons? alos) (or (symbol=? s (first alos)) (member? s (rest alos)))])) (define (subset? alos1 alos2) (cond [(and (empty? alos1) (empty? alos2)) true] [(and (empty? alos1) (cons? alos2)) true] [(and (cons? alos1) (empty? alos2)) false] [(and (cons? alos1) (cons? alos2)) (and (member? (first alos1) alos2) (subset? (rest alos1) alos2))])) It seems subset? doesn't really recur on both arguments after all, assuming we use member? as a helper. Understanding that originally would have been difficult, since we didn't immediately realize a need for the helper function. We could either go back and start again from alos1's template, or equivalently, combine the (first and second) and (third and fourth) cases here: (define (subset? alos1 alos2) (cond [(empty? alos1) true] [(cons? alos1) (and (member? (first alos1) alos2) (subset? (rest alos1) alos2))]))