Following up on today's class, here are some additional notes related to Scheme. ----------------------------------------------------------------------- On car, cdr, pair?, and null Better names for these operators would be first, rest, cons?, and empty, respectively. DrScheme actually uses these names in the lower language levels (but we need Full Scheme for this class). Many of the notes I posted use these names as well. If you want to use these names, add the following line to the top of your file (the top window): (require-library "function.ss") The following session would then work (> is the DrScheme prompt) > (define L (cons 'kathi (cons 3733 empty))) > (first L) 'kathi > (rest L) (cons 3733 empty) ;; which Drscheme writes as (3733) > (cons? L) #t ; Scheme for true > (empty? L) #f ; Scheme for false > (empty? empty) #t These names should be much clearer, especially if you are new to Scheme. With these names loaded, our sum program would look like ; sum : list-of-nums -> num (define sum (lambda (alon) (cond [(empty? alon) 0] [(cons? alon) (+ (first alon) (sum (rest alon)))]))) ----------------------------------------------------------------------- On deriving programs from data descriptions (aka "design recipes") We only got started on this point today, so let me try to make this clearer. When you program in a functional (value-oriented) language like Scheme, your programs can often follow a simple rule: "the structure of the data determines the structure of the program" Consider a program that consumes a list of numbers. What is the structure of a list of numbers? A list-of-nums is either - empty, or - (cons num L), where L is a list-of-nums Based on this description (which is a comment, not part of the program), we know that any program that processes a list-of-nums must have the following structure: (define list-of-nums-prog (lambda (alon) (cond [(empty? alon) ...] [(cons? alon) ...]))) These lines come from the definition of a list of numbers. There are two cases for a list of numbers: empty, or created with cons. Therefore, our program must first check to see which case we are in. That's what the above code skeleton does. All that the skeleton is missing are the answers to produce in each case. The ... indicate where I still need to fill in code to finish the program. Is this all of the information that we can exploit about a list-of-nums? No. In the cons? case, we know that the list must have two pieces (first and rest). So, we can also write down the expressions that pull out those pieces: (define list-of-nums-prog (lambda (alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (rest alon) ...]))) Do we know anything else? Yes, we know that (rest alon) is a list-of-nums (why? Because our definition of list-of-nums says so). How do we process a list-of-nums? We are writing a program to compute list-of-nums. In most cases, we will need to process the rest of the list in the same way, so we need a recursive call. So, we can extend our skeleton as follows: (define list-of-nums-prog (lambda (alon) (cond [(empty? alon) ...] [(cons? alon) ... (first alon) ... (list-of-nums-prog (rest alon)) ...]))) Note: all of this skeleton comes "for free" from our understanding the input data to the program. I haven't yet told you what program we are writing, just that the program will process a list-of-nums. By designing programs from data descriptions, much of the program writes itself! To finish the program, you need to know what we actually want to compute. For sum, we fill in the ... as follows: (define list-of-nums-prog (lambda (alon) (cond [(empty? alon) 0] [(cons? alon) (+ (first alon) (list-of-nums-prog (rest alon)))]))) (this required only 4 additional non-whitespace characters once we had the skeleton) For count, which tells me how many numbers are in the list, we fill in the ... as follows: (define list-of-nums-prog (lambda (alon) (cond [(empty? alon) 0] [(cons? alon) (+ 1 (list-of-nums-prog (rest alon)))]))) (five additional non-whitespace characters, plus deleting the (first alon), which shows that we don't always use both pieces of information). In short, this approach of laying out the data and deriving skeletons helps you derive programs in a clear and systematic manner. To summarize, here are the steps to deriving programs this way: 1. Write down a description of the data that you want to process 2. Write an initial skeleton containing a name for the function and an argument for the data 3. Expand the body of the function with a cond statement containing one clause (question/answer pair) for each line in the data description. 4. For each cond clause, ask whether the data has any additional structure in that case. If so, write expressions which extract those pieces. 5. For each piece, ask whether it is of the same type as the input you are processing with this skeleton. If so, add a recursive call to the function around that piece. ;; this finishes the skeleton; some of the notes call this a template 6. Figure out how to combine the pieces in each case to get to a final answer. These are the "additional characters" that you add to a skeleton to get a running program. For practice, try writing the data descriptions and skeletons for programs processing lists of symbols and lists of symbols and strings (both kinds of data in one list). Also try it for lists of phone book structures (see below). Then try it for trees. ----------------------------------------------------------------------- On records/structures in Scheme (this is new material which you may find useful) Scheme has a mechanism for creating structures called define-struct. Assume I want to create a phonebook structure containing first and last name and phone number. In MzScheme, I could write (define-struct pbentry (firstname lastname phone)) When you do this, MzScheme automatically defines several functions for you: - make-pbentry, which consumes a firstname, lastname, and phone number and returns a pbentry structure containing that data - pbentry-firstname, which consumes a pbentry structure and returns whatever is stored in the firstname field - pbentry-lastname, which consumes a pbentry structure and returns whatever is stored in the lastname field - pbentry-phone, which consumes a pbentry structure and returns whatever is stored in the phone field - pbentry?, which consumes any value and returns a boolean saying whether that value was created using make-pbentry. Examples: > (define-struct pbentry (firstname lastname phone)) > (define Kentry (make-pbentry 'Kathi 'Fisler 5118)) > (pbentry-firstname Kentry) kathi > (pbentry? Kentry) #t > (pbentry 5118) Error: reference to undefined identifier: pbentry Structures are far preferable to lists for grouping together fixed numbers of fields for two reasons: - you get the operators defined for you, which are more readable than a lot of car/cdr operations to get at different positions in the list - you get the pbentry? operator, which tells you what kind of data you have. With a list, you'd need to manually tag the data to know what kind of data it is (how could you tell a phone book entry from a record indicating a person's name and ATM pin number, for example?) Much of what you do with structures we can do with classes. Structures are helpful though because they are much more lightweight than classes, and often more appropriate.