I occasionally ask you to implement a function that already exists in Racket. Homework 4 contains such a problem. Other times, a student does not read documentation or instructions. (Present company excluded, of course.) As a result, they write functions that already exist, sometimes under a different name. That's not even always a bad thing... Sometimes it's better to use your time practicing than poking around documentation.
In order to install this Windows Update, |
Write a structurally recursive Racket function (get los n), where los is a list of symbols and n is a non-negative integer. get returns the symbol in position n of los. For example:
> (get '(a b c) 2) 'c > (get '(d c b a z) 1) 'c > (get '(d c b a z) 10) get: no such position
Make the position 0-based. If n is too big for the list, return an error message as a string.
Start with the inductive definition for a list of symbols:
<list-of-symbols> ::= () | (<symbol> . <list-of-symbols>)
Check out a candidate solution in this source file. This is a fine example of using structural recursion to process an inductively-defined datatype.
We can even use get on other kinds of lists, because it never applies a function to the values in the list. As we have seen, a Racket list is a pair whose cdr is a list. We can define generic lists inductively as:
<list> ::= () | (<any> . <list>)Racket even has a built-in predicate for the "any" datatype, named any/c. We don't need it here, because we don't operate on the items in the list; we only return one of them.
Notice a couple things. First, even though there is a nested if expression in the code, we consider only two kinds of list argument: it is either the empty list or a pair. The second if helps us solve the pair case: Is the item we are looking for in the first part of the pair? Try to think clearly about the different parts of the problem. That will help you make sense of when you need another choice, and why.
Second, if the second argument were a symbol and we wanted to return the position of its first occurrence in the list, like Problem 5 on Homework 4, then we would need something new. Learning that new technique is our first job today.
get is essentially an implementation of Racket's primitive list-ref function, which many of you discovered when not using second or sixth on a previous assignment. You can find many other functions for working with lists in the Racket docs. Most of them are also great exercises for practicing your recursive programming skills!
Finally, my solution uses Racket's primitive error function. Feel free to use it whenever a function has an error case. Note also that we can test our error cases, using another test feature of Rackunit.
about a web page about a web page... |
Writing recursive functions well and confidently requires you to know several techniques, just as writing loops well and confidently does. Last session, we explored the basic technique for writing recursive programs, the technique on which we base all of our recursive functions: structural recursion. With this technique, we mimic the structure of an inductive data specification in the code that processes the data.
This idea is simple but powerful. What if we are asked to write a function to process...
This session continues our discussion of recursive programming by introducing two new techniques:
Structural recursion is the basis for nearly every function we write. Occasionally, we will encounter bumps along the way to a solution. Rather than pitching structural recursion and flailing at our code without guidance, we will use specific techniques to get over, or around, the bump. Over the next few sessions, we will learn several techniques that we can use when we encounter difficulties using structural recursion. The first of these is the interface procedure.
Unless you are omniscient, writing a recursive function will occasionally require "fixing" the function along the way instead of writing it straight through from beginning to end. Consider the function annotate, which takes as its only argument a <list-of-symbols>. For example, if we pass to annotate
(jerry george elaine kramer)
it returns a list with each symbol annotated by its position in the list:
((jerry 1) (george 2) (elaine 3) (kramer 4))
We can use structural recursion to build the framework of our answer:
(define annotate (lambda (los) (if (null? los) ; then: handle an empty list ; else: handle a pair )))
The base case of the data spec is the empty list. In this case, an empty list can be returned, since there are no items to annotate:
(define annotate (lambda (los) (if (null? los) '() ; else handle a pair )))
The inductive case is a symbol followed by a list of symbols. We can combine the annotated symbol with the rest of the list annotated using cons. The result is:
(define annotate (lambda (los) (if (null? los) '() (cons <something computed from (first los)> (annotate (rest los))))))
When we write a function that computes a list of answers, one for each item in the original list, we will often use a piece of code that looks just like this. It will constitute a common mechanism for "putting our answer back together".
How can we annotate a symbol? By creating a list consisting of the symbol and its position of the symbol in the list:
(define annotate (lambda (los) (if (null? los) '() (cons (list (first los) position) (annotate (rest los))))))
Oops! We've run into a slight problem. We need the position of the symbol in the list, but we haven't supplied it anywhere. We could pass the current position down to each recursive call:
(define annotate (lambda (los position) (if (null? los) '() (cons (list (first los) position) (annotate (rest los) (add1 position))))))
This does the work we need, but we have two related problems:
These reasons should persuade us to look for a different solution. Programmers face this problem all of the time and have developed a common "patch". First, rename this version of the solution as a helper function:
(define annotate-with-position (lambda (los position) (if (null? los) '() (cons (list (first los) position) (annotate-with-position (rest los) (add1 position))))))
Second, implement annotate as a function that calls the renamed function:
(define annotate ;; now write annotate ... (lambda (los) ;; ... to jump-start the helper (annotate-with-position los 1)))
We call the new annotate an interface procedure. It serves as an interface to the function that does the real work.
Creating an interface procedure is a common practice in many kinds of programming, including functional programming. It allows us to write our code naturally -- in the way that follows our understanding of the problem -- even when the task becomes complicated, without disturbing the tranquility of the world in which the function resides.
The interface function pattern illustrates a valuable wisdom: When you encounter a difficulty implementing structural recursion, don't give up on the technique. We are following the structure of our data for many good reasons. Instead of giving up, solve the new difficulty. The problem we encountered while implementing annotate is so common that other programmers have developed a standard solution. This wisdom generalizes beyond structural recursion to any well-justified technique, including most every design pattern we use.
Last session, we defined two functions, remove-upto and remove-first, over lists of symbols. List of symbols is the data type on which they operate, and we had an inductive definition for the type that guided our work. Today, let's consider a more complex data structure, one that will be of great use to us when we write programs to process languages: the s-list. The difference between a list of symbols and an s-list is that the elements of the list can themselves be s-lists.
Here is the BNF notation for an s-list:
<s-list> ::= () | (<symbol-expression> . <s-list>) <symbol-expression> ::= <symbol> | <s-list>
And here are some examples:
() (()) (a) ((a) b) (a b c) (a (b) c) (a b c d) (if (zero? n) zero (/ total n)) (a b c d e f g h) (cons (foo (car x)) (foo-cdr (cdr x)))
The items on the left are lists of symbols, but they are also s-lists. A symbol-expression can be a symbol, or an s-list.
Let's define a function, subst, that substitutes one symbol for another anywhere in an s-list. You can think of this as like a global "search and replace" operation. For example, when applied to a program, subst can serve as the foundation of an operation for renaming variables -- a common refactoring that all programmers do.
This function takes three arguments: the new symbol, the old symbol, and an s-list to operate on:
> (subst 'd 'b '(a b c a b c d)) (a d c a d c d) > (subst 'a 'b '((a b) (((b g r) (f r)) c (d e)) b)) ((a a) (((a g r) (f r)) c (d e)) a)
Following the principle of structural recursion, the structure of subst should follow the structure of the BNF specification for an s-list:
(define subst (lambda (new old slist) (if (null? slist) ;; handle the empty list ;; handle a pair that contains a symbol-expression )))
If slist is empty, there are no occurrences of old to replace. The answer is the empty list.
(define subst (lambda (new old slist) (if (null? slist) '() ;; handle a pair containing a symbol-expression )))
The second arm of our BNF definition defines a case where slist is a pair with the form (<symbol-expression> . <s-list>). The result of subst will be the result of substituting new for old in both parts, the <symbol-expression> and the <s-list>.
The first element of the pair is a symbol-expression. Note, however, that symbol-expression is also defined in terms of a choice. Our natural inclination might be to implement this choice with a conditional expression. There are two alternatives: the first element is a symbol, or it is an s-list.
(define subst (lambda (new old slist) (if (null? slist) '() (if (symbol? (first slist)) ;; handle a symbol in the first, handle slist in the rest ;; handle an slist in the first, handle slist in the rest ))))
We have to return a list with the same structure as our input, so in both cases we cons the result from the first into the result from the rest.
(define subst (lambda (new old slist) (if (null? slist) '() (if (symbol? (first slist)) (cons ;; handle symbol in first ;; handle slist in rest) (cons ;; handle slist in first ;; handle slist in rest) ))))
If it is a symbol, we must determine whether or not to replace it with new. We replace the symbol if it is equal to old, and otherwise we leave it alone:
(define subst (lambda (new old slist) (if (null? slist) '() (if (symbol? (first slist)) (if (eq? (first slist) old) (cons new ;; handle the slist in the rest ) (cons (first slist) ;; handle the slist in the rest ) (cons ;; handle slist in first ;; handle slist in rest) ))))
In both of these cases, we need to substitute new for old in the rest of the list. The rest is an s-list, so we can use subst to compute that part of our result.
(define subst (lambda (new old slist) (if (null? slist) '() (if (symbol? (first slist)) (if (eq? (first slist) old) (cons new (subst new old (rest slist))) (cons (first slist) (subst new old (rest slist))) ) (cons ;; handle slist in first ;; handle slist in rest) ))))
The only thing left to do is to decide what to do when the first member of slist is not a symbol. In that case, it is an s-list. We are in luck. We already have a function for substituting symbols in s-lists. It is the function that we are writing, subst! So, we can:
(define subst (lambda (new old slist) (if (null? slist) '() (if (symbol? (first slist)) (if (eq? (first slist) old) (cons new (subst new old (rest slist))) (cons (first slist) (subst new old (rest slist))) ) (cons (subst new old (first slist)) (subst new old (rest slist)))))))
And we are done, or at least we have a working solution. Our basic structural recursion technique guided us along the way. But is this a good solution?
Programming Aside: Notice how the indentation of this code makes the control structures we are using as clear as possible. Whenever you write a program -- especially in a language like Racket (with a uniform syntax (and so (many) function calls!)) -- you should strive to write code that tells us how to read itself.
Our function works but, if we are honest with ourselves, we must admit that it has a couple of weaknesses.
First, we have repeated the expression (subst new old (rest slist)) three times, including twice in the if expression that handles symbols. We know that repeated code can cause all sorts of problems in maintenance. But having to write these expressions separately also makes it hard for us to write the function in the first place. A mistake, even a typo, in any of the expressions will break our function. Besides, all the repetition makes the code harder to read.
Second, it is not really faithful to the structure suggested by the BNF. Look at the definition of an s-list again:
<s-list> ::= () | (<symbol-expression> . <s-list>) <symbol-expression> ::= <symbol> | <s-list>
Structural recursion tells us that the structure of our code should reflect the structure of the data. Our code does not. There are two BNF expressions in the data definition, but we have written only one function!
The second weakness causes the first. By not following the data structure, we have created extra cases to solve and made the problem harder to solve. That is what requires us to duplicate code.
If you look back at the step-by-step evolution of our function, you will see a clue hinting at this second weakness. We had to leave ourselves detailed notes using comments so that we did not lose our place as we solved small parts of the problem. Those comments are a sign that we are managing a lot of complexity in our heads. But the data type we are processing is not that complex!
My running commentary does more than give us a clue about when we went off track. It tells us exactly where we went off track:
The first element of the pair is a symbol-expression. Note, however, that symbol-expression is also defined in terms of a choice. Our natural inclination might be to implement this choice with a conditional expression. ...
A better way to reflect the choice between kinds of symbol expression would be to follow the data definition. An s-list is defined in terms of symbol expressions, and a symbol expression is defined in terms of s-lists. We say that such data types are mutually inductive. We'd like for our code to show this relationship, too.
Patterns that show up in data should probably show up in the code that processes the data. (And perhaps in the languages we use to write the code...)
For our program structure to follow the pattern of the BNF, we must define a function for substituting symbols in s-lists, called subst, and a function for substituting symbols in symbol expressions, called, say, subst-symbol-expr. Because each data type is defined in terms of the other, these functions will call one another. This technique is called mutual recursion, because the recursion involves two functions that call one another, working together to create a solution.
Let's begin again and write subst from scratch. For now, let's suppose that subst-symbol-expr exists.
(define subst (lambda (new old slist) (if (null? slist) '() ;; handle a pair that contains a symbol-expression )))
The "else" clause becomes quite easy to write:
The definition of subst becomes:
(define subst (lambda (new old slist) (if (null? slist) '() (cons (subst-symbol-expr new old (first slist)) (subst new old (rest slist))) )))
Isn't that much clearer?
Now we have to write subst-symbol-expr. Using structural recursion, the definition of this function follows the BNF definition of the data type it processes, a symbol expression. The BNF lists two alternatives for a symbol expression: it is either a symbol, or it is an s-list. So:
(define subst-symbol-expr (lambda (new old symexp) (if (symbol? symexp) ;; handle a symbol ;; handle an slist )))
If the symbol expression is a symbol, then we decide whether to replace it with the new symbol:
(define subst-symbol-expr (lambda (new old symexp) (if (symbol? symexp) (if (eq? symexp old) new symexp) ;; handle an slist )))
If not, then it is an s-list. But we have already written a function that can make substitutions in an s-list: subst! Call it:
(define subst-symbol-expr (lambda (new old symexp) (if (symbol? symexp) (if (eq? symexp old) new symexp) (subst new old symexp))))
That's pretty clear, too.
Our solution now consists of two relatively small, relatively simple functions that work together to solve the problem.
What are the advantages of our new program?
Mutual recursion will be our technique of choice whenever we
have a multiple-part data definition.
Use mutual recursion to implement (count-occurrences s slist), which counts how many times the symbol s occurs in slist.
For example:
> (count-occurrences 'a '(a b c)) 1 > (count-occurrences 'a '(((a be) a ((si be a) be (a be))) (be g (a si be)))) 5
The first step is to examine the BNF:
<s-list> ::= () | (<symbol-expression> . <s-list>) <symbol-expression> ::= <symbol> | <s-list>
From the BNF, we expect to write two functions, one that counts the symbol in an s-list and one that counts the symbol in a symbol expression.
We start with the pattern suggested by the BNF for s-list:
(define count-occurrences (lambda (s slist) (if (null? slist) ;; slist is empty ;; slist is a pair )))
We can conclude without much effort that a symbol occurs in an empty list 0 times. In a non-empty list, the number of times it occurs is equal to the number of times it occurs in the first part of the list plus the number of times it occurs in the rest of the list.
Because this is a mutually recursive specification and function, we will assume that a function named count-occurrences-sym-expr exists, so that we can use it to count the number of occurrences in the car of the pair:
(define count-occurrences (lambda (s slist) (if (null? slist) 0 (+ (count-occurrences-sym-expr s (first slist)) (count-occurrences s (rest slist))) )))
Now, we define count-occurrences-sym-expr. The BNF description for symbol expressions suggests the following pattern:
(define count-occurrences-sym-expr (lambda (s sym-expr) (if (symbol? sym-expr) ;; sym-expr is a symbol ;; sym-expr is an slist )))
If the symbol expression is a symbol, then we need to determine whether it is the symbol we're counting or not and return the appropriate value, 0 or 1. If it is an s-list, then we have a function for counting occurrences -- count-occurrences:
(define count-occurrences-sym-expr (lambda (s sym-expr) (if (symbol? sym-expr) (if (eq? s sym-expr) 1 0) (count-occurrences s sym-expr) )))
And we are done!