Instructions for students & labbies: Students use DrScheme, following the design recipe, on the exercises at their own pace, while labbies wander among the students, answering questions, bringing the more important ones to the lab's attention.
set! review
To change which thing a variable refers to, we use set!, as introduced in class.
While define introduces a new variable and initializes its value, set! changes the value of a pre-defined variable. You can't define a variable multiple times. (Of course, you can define the same name in different scopes, but each is considered a different variable.) You can't set! a variable unless it's been defined previously.
The hand-evaluation rule for (set! variable exp) is as follows:
- Evaluate exp to a value val.
- Replace the previous definition of the variable with
(define variable val).
set! exercises
We'll review set! by some small artificial examples.
Try evaluating the following examples. First try them in your head or on paper. Then try them in DrScheme to see if you were right.
The last example is very bad style. You should not write code that depends on the the order of evaluation because it is very confusing. Moreover, by definition, a different implementation of Scheme isn't guaranteed to use the same order. |
Now, let's try some more useful examples of set!.
|
In Scheme, it is common convention to give imperative functions names ending with an exclamation mark.
Imperative Programming
This course has dealt primarily with a style of programming known as functional programming. Another style is imperative programming, which focuses primarily on side-effects (e.g., set!), rather than passing arguments.
Let's get a hint at the relation between these two styles of programming. In particular, let's see an example of how recursion and looping are related.
- We start with a simple structurally recursive program:
(define (sum-list alon) (cond [(empty? alon) 0] [(cons? alon) (+ (first alon) (sum-list (rest alon)))])) - We can then write an accumulator version:
(define (sum-list alon) (local [(define (sum-list-acc the-lon accum) (cond [(empty? the-lon) accum] [(cons? the-lon) (sum-list-acc (rest the-lon) (+ (first the-lon) accum))]))] (sum-list-acc alon 0)))As you've seen, sometimes this step is difficult. Remember that this version adds up the numbers in the oppositive order. It was easy to write this version because addition is commutative, a+b+c = c+b+a, and thus adding in the opposite order gives the same result.
- We can rewrite this to avoid passing arguments in the helper function and, instead, change only local variables. This is a straightforward syntactic translation of the accumulator version:
(define (sum-list alon) (local [(define accum 0) (define the-lon alon) (define (sum-list!) (cond [(empty? the-lon) accum] [(cons? the-lon) (begin (set! accum (+ (first the-lon) accum)) (set! the-lon (rest the-lon)) (sum-list!))]))] (sum-list!)))Q: Does the order of the two
set!s matter? Why or why not? - Lastly, we can rewrite this using one of Scheme's several looping constructs.
loop-untiltakes four arguments: a starting value to loop on, a test for what value(s) to stop on, a function to get the next iteration's value from the current one, and a function to evaluate on each loop iteration. (loop-untilis defined in the "Pretty Big" language level.)(define (sum-list alon) (local [(define accum 0)] (begin (loop-until alon empty? rest (lambda (the-lon) (set! accum (+ (first the-lon) accum)))) accum)))Note: Most programming languages have a construct similar to this, except with a test for what value(s) to continue looping (i.e., the opposite of what's used here). It is typically called a while loop.
|
This shows that recursion and iteration are closely related. In fact, we think of iteration as one form of recursion. The process above works well for recursion/iteration on lists, but is too simple in general. Challenge: Think about why this doesn't work for recursion on trees, and how you can fix that.
Which version is "best"? The first is stylistically best, because it is the most easily understood. Also, it is much easier to make mistakes in the versions with set!. They are all essentially equally efficient -- they each recur once per list element. However, they may differ by constant factors. What these constant factors are and which version is fastest both depend completely on how DrScheme is implemented.
Imperative exercises
Write imperative versions of the following. You might want to use the above steps, but that's not necessary.
|
Input and output
Since DrScheme automatically displays the result of whatever expression we give it, so far we haven't needed to know how to display something ourselves. Here are a few functions for getting information into and out of our programs, although Scheme has lots more. Try them out.
-
printfdisplays information to the screen. It takes at least one argument -- a string giving the format of what to display.printfmay take more arguments, if so specified by the format. E.g.,(printf "hello, world~n")
This displays the string
hello, world, followed by a newline. The~nmeans a newline.(printf "hello, ~s~n" 'world)
This displays the same thing. The
~sindicates that the next argument's value should be printed in place of the~s. (The "s" is short for "s-expression", Scheme lingo for "any value".)(printf "~s ~s ~s~n" (+ 1 1) (list 'a 'b) #t)
This displays three things, the remaining arguments, separated by spaces and terminated by a newline.
- As we've previously seen,
errorcauses an error to happen and displays an error message. In this class, we haven't been concerned with writing functions that check for bad inputs, but in real life and later classes, we'd want to check and report any errors, including bad inputs.(error 'myfunction "error message about ~s~n" 'this-error)
This causes an error. It also displays an error message using the symbol provided (typically the current function name), the format string, and any other arguments. Like
printf, it expects only as many other arguments as specified by the format string. -
readpauses a program, waits for the user to type something in, and returns the value that the user just typed in. It takes no arguments. Each time it is used, it could return a different value, since the user can type in something different each time.(printf "I will read, but immediately forget the next thing you type.~n") (read) (printf "What is your name? ") (define name (read)) (printf "Howdy there, ~s.~n" name)
printf debugging
So far, you have used two good techniques for debugging code:
-
running the code and checking outputs against expected results, and
- stepping through code with DrScheme's stepper or by hand.
Using printf appropriately can provide one more good technique, by displaying important pieces of information. You briefly saw an example of this in class. The most common places to do this are at the beginning of a function, the end of a function, and after a bunch of side-effects. With practice, you'll see that these and other techniques each have strengths and weaknesses.
The following version of factorial doesn't work:
(define (buggy-factorial n)
(cond [(zero? n) 1]
[(positive? n)
(* (sub1 n) (buggy-factorial (sub1 n)))]))
We can rewrite our code to print out the input(s) and output(s) of the function:
(define debug? true) ; do I want to display debugging info?
(define (buggy-factorial n)
(begin
(when debug? (printf "buggy-factorial input: ~s~n" n))
(local [(define result
(cond [(zero? n) 1]
[(positive? n)
(* (sub1 n)
(buggy-factorial (sub1 n)))]))]
(begin
(when debug? (printf "buggy-factorial output: ~s~n" result))
result))))
We use a boolean "flag" debug? to indicate whether or not to print out debugging info. This makes turning on/off debugging very easy. It can get very annoying to add and remove debugging repeatedly. For larger programs, we can extend this idea in various ways. It would be better to hide debug?. It is often useful to use multiple debugging flags, e.g., to debug separate parts of the code independently. It is often useful to use numbers instead of booleans to display different amounts of debugging info.
It is important to label any debugging values you display so that you know what your output means.
The conditional used here, when, is shorthand for a cond that does nothing when the test is false.
(when test-exp then-exp)
= (cond [test-exp then-exp]
[else void])
It is very easy to add the printf to display the input. However, it is somewhat cumbersome to add the
printf to display the output since we have to name the result to refer to it twice.
|
This work is licensed under a Creative Commons Attribution 2.5 License. Please follow our