In general, a substantial part of the code for a Scheme function arises from the data structure of the function's inputs. This note demonstrates this by example. Consider a function that operates over a list of numbers. We can define a list of numbers as ;; A list-of-nums is either ;; - empty or ;; - (cons num list-of-nums) Assume I've asked you to write a function numlist-func that takes a list of numbers as input (I haven't told you what this function does, or what the function outputs). How much can you write just from knowing that the input is a list-of-nums? (Step 1:) For starters, the function must take the list of nums as an argument and must first determine whether it is empty or a cons (ie, which of the two cases from the data structure definition applies). The ... indicate places where we haven't yet finished the function. (define (numlist-func alon) (cond [(empty? alon) ...] [(cons? alon) ...])) (Step 2:) Next, ask yourself which cases have additional information about the data. The empty case does not, but the cons case does (namly, the first and the rest). We can therefore fill in our program skeleton further. (define (numlist-func alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (rest alon) ... ])) (Step 3:) Finally, look at the types of the pieces you pulled out. Are they simple types (number, symbol, boolean, string), or types that also have structure (lists, define-datatypes, trees, etc)? If they have structure, do they refer to the same datatype as the current function? If so, you'll need a recursive call on that piece. If the piece isn't recursive, you may still want a helper function to process the complex input. In the case of numlist-func, the rest of the list is also a list-of-nums, so we should make a recursive call: (define (numlist-func alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (numlist-func (rest alon)) ... ])) Once you've constructed this skeleton, you just need to fill in the ... based on what the function actually does. For example, to take the product of numbers in a list, rename numlist-func to product and fill in the holes as follows: (define (product alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (product (rest alon)) ... ])) (define (product alon) (cond [(empty? alon) 1] [(cons? alon) (* (first alon) (product (rest alon)))])) This process works for almost all of the programs you will write in this course. Let's look some more examples. ------------------------------------------------------------------ Write the program skeleton for a function over vehicles. Here is the define-datatype for vehicles: (define-datatype vehicle vehicle? [taxi] [bus (seats number?) (hand-holds number?) (color string?)] [subway (cars number?) (seats-per-car number?) (line symbol?)]) A function over a vehicle must start with a cases construct; this parallels Steps 1 and 2 from above. (define (vehicle-func a-vehicle) (cases vehicle a-vehicle [taxi () ...] [bus (seats hand-holds color) ...] [subway (cars seats-per-car line) ...])) For Step 3, we consider the types of the fields on the define-datatype. All are atomic types, so there's no need for recursive calls. ------------------------------------------------------------------ Now let's consider a function over a list of vehicles. ;; A list-of-vehicle is ;; - empty or ;; - (cons vehicle list-of-vehicle) Steps 1 and 2 yield the same skeleton as for list of numbers: (define (vehiclelist-func alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (rest alon) ... ])) What happens in step 3? We need a recursive call on the rest (define (vehiclelist-func alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (vehiclelist-func (rest alon)) ... ])) but in this case, (first alon) also has structure (it's a vehicle). So, we can assume we'll call some function on vehicles to process it. Let's annotate the skeleton with this as follows: (define (vehiclelist-func alon) (cond [(empty? alon) ...] [(cons? alon) ... (vehicle-func (first alon)) ... (vehiclelist-func (rest alon)) ... ])) So, in this case our skeleton really contains skeletons for two functions: one for vehicle and one for list-of-vehicle. Assume we want to write a function contains-blue-bus? that takes a list-of-vehicle and returns a boolean indicating whether the list contains any blue buses. Start with the skeletons: (define (vehicle-func a-vehicle) (cases vehicle a-vehicle [taxi () ...] [bus (seats hand-holds color) ...] [subway (cars seats-per-car line) ...])) (define (vehiclelist-func alon) (cond [(empty? alon) ...] [(cons? alon) ... (vehicle-func (first alon)) ... (vehiclelist-func (rest alon)) ... ])) Now, rename the two skeleton functions to fit the problem: (define (blue-bus? a-vehicle) (cases vehicle a-vehicle [taxi () ...] [bus (seats hand-holds color) ...] [subway (cars seats-per-car line) ...])) (define (contains-blue-bus? alon) (cond [(empty? alon) ...] [(cons? alon) ... (blue-bus? (first alon)) ... (contains-blue-bus? (rest alon)) ... ])) Finally, fill in the holes to implement the two functions. Notice how little code is required over the template. (define (blue-bus? a-vehicle) (cases vehicle a-vehicle [taxi () false] [bus (seats hand-holds color) (string=? color "blue")] [subway (cars seats-per-car line) false])) (define (contains-blue-bus? alon) (cond [(empty? alon) false] [(cons? alon) (or (blue-bus? (first alon)) (contains-blue-bus? (rest alon)))])) ------------------------------------------------------------------ One last example: let's define binary trees of numbers: (define-datatype numtree numtree? [bottom] [node (num number?) (left numtree?) (right numtree?)]) Here's the full program skeleton. Do you see where each piece came from? (define (numtree-func atree) (cases numtree atree [bottom () ...] [node (num left right) ... (numtree-func left) ... (numtree-func right) ... ])) Now, we can fill in the skeleton to write programs such as sum-tree: ;; sumtree : numtree -> number ;; sum all numbers in the tree (define (sumtree atree) (cases numtree atree [bottom () 0] [node (num left right) (+ (sumtree left) (sumtree right))])) Or, we can write in-tree? to check whether a number is in the tree ;; in-tree? number numtree -> boolean ;; determine whether given number is in the tree (define (in-tree? anum atree) (cases numtree atree [bottom () false] [node (num left right) (or (equal? anum num) (in-tree? anum left) (in-tree? anum right))])) ------------------------------------------------------------------ SUMMARY Program skeletons are an extremely useful tool, because they capture the code that traverses a data structure. If you start with the full program skeleton, you never need to think about the traversal; you only need to think about what you do at each point in the data structure. They also help you develop a lot of the code upfront, which lets you focus in on the interesting parts of the code. In general, your code should always follow the structure of the skeletons so that you get the traversals right. If you are faced with a new program and don't know where to start, write the skeleton first! If you can't write the skeleton, you almost certainly won't be able to implement the function either. Once you have the skeleton, if you're still struggling with a program, write down examples of the program's inputs and outputs. These examples will help determine whether you are stuck on what the function does, or how it should do it. If you can't write the examples, then you don't know what the function should do. If you can write the examples but not fill in the skeleton, then you are stuck on how to implement it.