Comp 210 Lab 11: State & a Little I/O

set! examples, set! vs. set-structure-component! I/O


set! examples

To change which thing a variable refers to, we use set!, as introduced in class.

set! exercises

Do the following exercises as a group. For each, first try it in your head or on paper, then check your answer using DrScheme.

These are "toy" examples that are not necessarily typical of real programs. However, they illustrate important issues of using state.

  •      (define x 1)
         (define y 2)
         (set! x (+ x 1))
         (set! y (+ y 1))
         (set! x (+ x y))
         
    What are the values of the variables x and y now? It might help you to ask what values x and y have after each set!.

    The hand-evaluation rule for (set! variable exp) is

    1. Evaluate exp to a value val.
    2. Replace the previous definition of the variable with (define variable val).

     

  • Let's look at changing local variables:

         (define x 1)
         (define y 0)
         (local [(define x 3)]
            (begin
              (set! x (+ x 1))
              (set! y x)
              y))
         
    What are the values of x and y now?

  •      (define x 5)
         (+ (begin (set! x 6) x)
            x)
    
         (define y 5)
         (+ y
            (begin (set! y 6) y))
         
    For each, do you get 10, 11, or 12? What are the values of x and y after the following? What does that tell you about the order of evaluation of function arguments?

    This 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.

In class, we've seen some more realistic uses of set!. Here are a couple more.

Function-call counting exercises
  1. Develop

         ; factorial-count! : natnum -> natnum
         ; Purpose: Returns the factorial of its argument.
         ; Effects: Increments factorial-counter.
         ; Examples:
         ; #|
         ; > factorial-counter
         ; 0
         ; > (factorial-count! 3)
         ; 6
         ; > factorial-counter
         ; 1
         ; > (factorial-count! 5)
         ; 120
         ; > factorial-counter
         ; 2
         

    While this seems kind of silly, this idea comes up all the time, as mentioned in class. More generally, we can keep track of lots of kinds of information about how a function is used. A common reason is to profile the function, to see how often a function is used or what its arguments typically are, to know how to better optimize it.

  2. However, we would like to hide the counter factorial-count locally inside the function factorial-count!, so that nobody else can change it. Simply adding a local definition is easy enough, but there are two big problems if the counter isn't global:

    • we need control over when the counter is initialized, and
    • we need some way to view the counter.

    Develop

         factorial-count! : ('init or 'count or natnum) -> (void or natnum)
         
    The input is simply a "message" indicating one of the possible actions of the function:
    • given 'init, the function (re)initializes the counter to zero and returns nothing (i.e., void);
    • given 'count, the function returns the current count; or
    • given a natural number, the function returns the factorial and increments the count.
    Part of the program just follows the three-case template for the above input type -- that should be easy. The "trick" is to define the counter in the right place in the definition. Every call to factorial-count! should use the same counter.

    We'll see more examples of this code style soon.

Sample solutions: Version 1, version 2.

Note the "effects" comment for the first example. You should always provide such a comment when your function changes state that is visible outside the function. E.g., in the first function, factorial-counter is global, so factorial-count! needs to describe what happens to it. In the second, the counter is local, but it still needs to be described since its presence can be noticed when you use the 'count message.


set-structure-component!

An example

So let's make some sample structures:

     (define-struct person (birthyear haircolor annoying activity))

     (define mary-kate-olsen (make-person 1986 'blonde true 'smiling))
     (define ashley-olsen    (make-person 1986 'blonde true 'smiling))

     (define michelle-tanner1 mary-kate-olsen)
     (define michelle-tanner2 ashley-olsen)
You'll see why we're using the ultra-annoying Olsen twins as an example. (Heh, heh. Evil grin.) In case you don't know, they are the child stars who played Michelle Tanner in an inanely stupid TV show "Full House".

Clearly, your expectations are that

We can use two functions to distinguish these cases:

set-structure-component! questions
  1. What does the following do? First, step through the evaluation in your head, then confirm it with DrScheme.
         (define (kill-person! person)
            (begin
                (set-person-activity! person 'dead)
                (set-person-annoying! person false)))
         (kill-person! ashley-olsen)
    
         (person-activity michelle-tanner2) = ?
         (person-activity mary-kate-olsen)  = ?
         
  2. What are the results of evaluating the following?
         (eq? mary-kate-olsen michelle-tanner1)    = ?
         (eq? mary-kate-olsen ashley-olsen)        = ?
         (equal? mary-kate-olsen michelle-tanner1) = ?
         (equal? mary-kate-olsen ashley-olsen)     = ?
         

Note that, by convention, most Scheme functions and special forms which change the state of something are named with a bang (!). It helps to remind us that these things can be dangerous.

Disclaimer: In case it isn't blindingly obvious, this is a joke. The course staff does not advocate killing the Olsen twins. (Fortunately, the fictional Michelle Tanner lives only in reruns.)

List examples

Cons-cells are just predefined kinds of structures, using a naming convention doesn't quite correspond to user-defined structures. They use the special forms set-car! and set-cdr!. (car and cdr are the traditional names for first and rest, respectively. However, the implementors of DrScheme forgot to define the names set-first! and set-rest!.) Remember that a list is not just a single structure, but a bunch of cons-cells.

set-structure-component! questions
     (define mylist (list 1 3 5 7))
     (define yourlist (cdr mylist))

     (set-car! yourlist 10)         ; The car/first should be a list element.
     ; what is mylist now?
     ; what is yourlist now?

     (set-cdr! mylist (list 2 4))   ; The cdr/rest should be a list.
     ; what is mylist now?
     ; what is yourlist now?

     (set-cdr! (cdr mylist) mylist)
     ; what is mylist now?
     ; what is yourlist now?

The shared output you'll see is basically the same as local.


Pragmatics: Using set! vs. set-structure-component!

We are going to get some practice using set! and set-structure-component!, as introduced in class. As a reminder,

When to use set! (Will be discussed in lecture.)

Remember that set! and set-structure-component! are as dangerous as they are useful. Careless side-effecting can have disasterous effects on your program. Use them wisely, little grasshoppper!


Input and output

Some common I/O functions

Since Scheme 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.

printf (Partly discussed in class.)

printf displays information to the screen. It takes at least one argument -- a string giving the format of what to display. printf may take more arguments, if so specified by the format. E.g.,

     (printf "hello, world~n")
     
displays the string hello, world, followed by a newline. The ~n means a newline.

     (printf "hello, ~s~n" 'world)
     
displays the same thing. The ~s indicates that the next argument's value should be printed in place of the ~s. (The "s" standards for "s-expression", Scheme lingo for "any value".)

     (printf "~s ~s ~s~n" (+ 1 1) (list 'a 'b) true)
     
displays three things separated by spaces and terminated by a newline. The three things displayed are the values of the remaining arguments.

error

error causes 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. In real life and later classes, we'd want to check and report any errors, including bad inputs.

     (error 'myfunction "error message")
     
causes an error. It also displays an error message using the symbol provided (typically the current function name), the string for the error message. The format function can be used to format the error string before it is passed to error.

read

read stops 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.

read exercise
Execute the following code, typing in answers when prompted.
     (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:

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 set! or set-structure-component! assignments. 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]
               [else      (* (sub1 n) (buggy-factorial (sub1 n)))]))

We will 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
           (if debug? (printf "buggy-factorial input: ~s~n" n))
           (local [(define result
                       (cond [(zero? n) 1]
                             [else      (* (sub1 n)
                                           (buggy-factorial (sub1 n)))]))]
               (begin
                  (if debug? (printf "buggy-factorial output: ~s~n" result))
                  result))))

printf-debugging exercise
Call buggy-factorial with some example inputs to see what the printed messages look like.

There are several things to note here: