Saturday, January 29, 2011

Exercise 2.27 asks us to modify our reverse procedure from exercise 2.18 to create a deep-reverse procedure that takes a list as its argument and returns the list with its elements reversed and with all sub-lists reversed as well. For example,

(define x (list (list 1 2) (list 3 4)))

x((1 2) (3 4))

(reverse x)((3 4) (1 2))

(deep-reverse x)((4 3) (2 1))

Recall that the solution to exercise 2.18 was to reverse a list by appending the car of the list to the reverse of the cdr of the list.

We need to do something similar here, but we also need to check to see if each element in the list is a list itself, and reverse it if it is. Since lists can be nested as many levels deep as we want, we'll need to make recursive calls to deep-reverse to handle any depth.

This solution still follows the same basic idea of reverse. We're still appending the car of the list to the reverse of its cdr, but now we first check to see if the car of the list is a pair. If the car is a pair we need to reverse it as well by passing it through deep-reverse. If not, we only need to deep-reverse the cdr.

Sunday, January 23, 2011

Section 2.2.2 introduces more complex data structures than the simple lists we've been working with so far. We now start to build lists whose elements are themselves lists. By doing so we create hierarchical data structures. The first few exercises in this section illustrate the basics.

Exercise 2.24 asks us to evaluate the expression (list 1 (list 2 (list 3 4))). We're to show the result printed by the interpreter, the corresponding box-and-pointer-structure, and an interpretation of the resulting structure as a tree.

The expression evaluates as follows:

> (list 1 (list 2 (list 3 4)))(1 (2 (3 4)))

In somewhat plain English, this is a list whose first element is the number 1 and whose second element is another list. That list has the number 2 as its first element and another list as its second element. This final list has as its elements the numbers 3 and 4.

Here are the box-and-pointer and tree representations of the same structure.

Exercise 2.25 asks us to use combinations of car and cdr to pick the number 7 from each of the following structures:

(1 3 (5 7) 9)

((7))

(1 (2 (3 (4 (5 (6 7))))))

The first thing we should do is figure out how to write an expression that will produce each of these structures. After a little bit of experimentation, we come up with the following:

If that last step is confusing, just remember that list gives us a sequence of pairs formed by nested calls to cons, so (list 5 7) is equivalent to (cons 5 (cons 7 null)). We need to use car to get the first value from the pair returned in the next-to-last step.

The second structure is rather straightforward and so is its solution.

> (car (car (list (list 7))))7

The last solution is similar to the first, but a little bit more complicated. Here's the solution:

If you were expecting to be able to simply "cdr-down" this structure without all those calls to car, once again, remember that list returns a sequence of pairs. We have to use car to extract the inner list returned by cdr at each step.

The append procedure takes the elements from two lists and produces a new list. When given two lists as parameters, the cons procedure returns a list whose first element is the first parameter list and whose remaining elements are the elements of the second list (we saw this at the beginning of section 2.2.1 Representing Sequences). Finally the list procedure simply wraps its parameters in a new list without doing any merge or append operations on them. The returned list just has two lists as its elements.

Related:

For links to all of the SICP lecture notes and exercises that I've done so far, see The SICP Challenge.

Sunday, January 16, 2011

The last few exercises in section 2.2.1 introduce the concept of mapping. The map procedure takes a procedure and a list as arguments, and returns a list of the results produced by applying the procedure to each element in the list.

The higher-order procedure map abstracts away the details of traversing a list and allows you to think in terms of procedures that you want to apply to an entire list.

Exercise 2.21 asks us to complete two different implementations of square-list by filling in missing expressions. Each procedure takes a list of numbers as its argument and returns a list of the squares of those numbers. The first example handles list traversal on its own.

This first implementation produces a list in the reverse order of the input list.

> (square-list (list 1 2 3 4 5))(25 16 9 4 1)

The result is reversed because as the procedure iterates through the list it takes an item from the front of the list, squares it, then puts the square on the front of the answer list. The square of the last item in the original list will be the last item placed at the front of the answer list.

The implementer tried to fix this bug by interchanging the arguments to cons:

This may seem like a reasonable thing to do, but it produces a very strange looking result.

> (square-list (list 1 2 3 4 5))(((((() . 1) . 4) . 9) . 16) . 25)

By flipping the arguments to cons, we've made the first argument a list and the second argument an integer. At every step we're combining the list from the previous step with a new integer and wrapping them in a new list. That accounts for the weird looking result. The good news is that the results are in the right order.

Here we just use append to add the new squared value to the end of the answer list each time through the loop, just like we saw in the reverse example in exercise 2.18.

> (square-list (list 1 2 3 4 5))(1 4 9 16 25)

Exercise 2.23 asks us to implement a for-each procedure, which is very similar to map. The for-each procedure takes a procedure and a list as its arguments, but instead of returning a list it just applies the procedure to each element of the list.

This is nearly the same as the implementation of map that we started out with. The main difference in implementation is that we need to execute two separate statements in sequence in the else clause instead of just one cons statement.

> (for-each (lambda (x) (newline) (display x)) (list 57 321 88))

5732188#t

Related:

For links to all of the SICP lecture notes and exercises that I've done so far, see The SICP Challenge.

Saturday, January 8, 2011

Exercise 2.20 introduces Scheme's dotted-tail notation, which allows procedures like +, *, and list to take arbitrary numbers of arguments. This is known as a variadic function and is supported in many programming languages with different syntax. In Scheme, a parameter list that has a dot before the last parameter name indicates that when the procedure is called, the initial parameters will be treated as normal, but the final parameter's value will be a list of any remaining arguments.

The following examples are given:

(define (f x y . z) <body>)(define (g . w) <body>)

The procedure f can be called with two or more arguments. The body of the f procedure will refer to its first two parameters as x and y, and any remaining parameters passed to the procedure will be accessible through the list named z. The procedure g can be called with zero or more parameters. Any parameters will be in the list w.

This exercise asks us to use dotted-tail notation to write a procedure same-parity that takes one or more integer parameters and returns a list of all the arguments that have the same even-odd parity as the first argument. For example:

(same-parity 1 2 3 4 5 6 7)(1 3 5 7)

(same-parity 2 3 4 5 6 7)(2 4 6)

We'll design a solution that uses an iterative helper procedure similar to the second length example given in section 2.2.1. The helper function will take as its parameters the list of items to iterate through and a variable to store the answer in as we iterate.

The same-parity procedure takes a first parameter a, and any remaining parameters are accessed through the list z. Processing the list is delegated to the helper procedure iter. The first thing iter does is check the list to see if it's null. If so we just return the answer. If there are items left to process in the list, iter is called again with the cdr of the list as its first parameter and the answer as its second parameter. If the car of the list has the same even-odd parity as the first argument to same-parity, then it's appended to the answer.