Side effects and sequencing

Side effects

The evaluation of an expression in Scheme always yields a value. Some expressions, however, have side effects -- incidental events that occur whenever the expression is evaluated, more or less independently of the computation and return of the expression's value. For example, output is a side effect: when a display expression is evaluated, it returns some unspecified value (in Chez Scheme, #<void>), but it also causes the argument to be written out.

When you're developing or debugging a program, you'll occasionally find it useful to add expressions with side effects to the procedures you write, in order to trace the sequence of events during the evaluation of a call to a complicated procedure.

Consider, for instance, the following procedure to add 2 to each element in a list:

(define add2
  (lambda (ls)
    (if (null? ls)
        '()
        (cons (+ (car ls) 2) (add2 (cdr ls))))))

Exercise 1

Check that this procedure works correctly, by running it with the command

(add2 '(2 6 -3 0 4.689))

The add2 procedure is recursive, and we might want to view the parameter ls each time that add2 is called. This can be accomplished by inserting calls to the display and newline procedures, as follows:

(define parameter-labelled-add2
  (lambda (ls)
    (display "ls: ")    ; Print a label for LS.
    (display ls)        ; Print out the current value of LS.
    (newline)           ; Start a new output line.
    (if (null? ls)
        '()
        (cons (+ (car ls) 2) (parameter-labelled-add2 (cdr ls))))))

Exercise 2

Run this revised procedure with the same data you used previously, by typing

(parameter-labelled-add2 '(2 6 -3 0 4.689))

Study and account for the side effects of evaluating this expression, and explain the sequence of events which produces the information that appears on your screen.


Now consider the following code which classifies numbers as being positive, negative, or zero:


(define classify
   (lambda (n)
      (display "n:  ")
      (display n)
      (display " --> ")
      (cond ((not (number? n)) "not a number")
            ((< n 0) "negative")
            ((zero? n) "zero")
            (else "positive")
      )
   )
)

Exercise 3

Test that this procedure appropriate classifies a variety of numbers.


In using the display and newline procedures, it is important to distinguish between information printed as side effects and information returned by procedures. In particular, procedures display and newline print information, but they do not return that data.

To see this, consider the following procedure which uses classify.


(define check-classify
    (lambda (n)
        (if (equal? "positive" (classify n))
            "the number was positive"
            "the number was non-positive"
        )
    )
)

Exercise 4

  1. Run this procedure with the following test cases:

    
    (check-classify 5)
    (check-classify 0)
    (check-classify -5)
    (check-classify 'walker)
    

    In each case, explain how the information on your screen was displayed.

  2. Explain why check-classify uses the test (equal? "positive" (classify n)) rather than including "n: in the test.


Exercise 5

Look again at the classify procedure. What happens if the lines

(display "n: ")
(display n)
(display " --> ")

are moved from immediately before the cond expression to immediately after it? That is, run the revised code with several more test cases, and explain the output.


The body of the parameter-labelled-add2 and classify procedures consist of four separate commands that are executed sequentially: Scheme guarantees that the side effects of the display and newline procedure will occur in the order in which those commands appear in the procedure definition.

This need to execute several steps in sequence arises frequently in Scheme. If the steps make up the body of a procedure, as in the preceding examples, or if they follow the test inside a cond-clause, it is sufficient just to write the commands one after another; this construction is sometimes referred to as an implicit begin. Scheme also provides an explicit begin expression that groups a sequence of commands into one large command and causes them to be executed as a group, but in the specified order. Such an expression might be needed if the sequence of commands forms the consequent or the alternative of an if-expression:

(if (on-fire? 'house)
    (begin
      (go-outside)
      (place-telephone-call 'fire-department))
    (begin
      (get 'potato-chips)
      (turn-on 'tv)))

The syntax of an if-expression requires that the consequent be a single expression, not a sequence of expressions, and that the alternative, if present, also be a single expression. The begin combines the sequence of commands into a single expression, as required.

As a further example, consider the following modification of add2.

(define exits-labelled-add2
  (lambda (ls)
    (display ls)
    (if (null? ls)
        (begin 
          (display "  then clause")
          (newline)
          '())
        (begin 
          (display "  else clause")
          (newline)
          (cons (+ (car ls) 2) (exits-labelled-add2 (cdr ls)))))))

Exercise 6

Run this revised procedure with the same data:

(exits-labelled-add2 '(2 6 -3 0 4.689))

Explain the format of the output.


Tracing

Section 2.5 of the textbook describes how to use output side effects to trace the execution of a procedure. By writing and invoking special-purpose entering and leaving procedures to print the output, one can keep track of the various recursive invocations of the procedure, the successive values of its parameters, and the values it returns. Here's how it would work for add2:

(define entering
  (lambda (procedure-name parameter)
    (display "Entering procedure ")
    (display procedure-name)
    (display " with parameter ")
    (display parameter)
    (newline)))

(define leaving
  (lambda (procedure-name result)
    (display "Leaving procedure ")
    (display procedure-name)
    (display " with result ")
    (display result)
    (newline)
    result))

(define fancy-add2
  (lambda (ls)
    (entering 'fancy-add2 ls)
    (if (null? ls)
        (leaving 'fancy-add2 '())
        (leaving 'fancy-add2
                 (cons (+ (car ls) 2) (fancy-add2 (cdr ls)))))))

> (fancy-add2 '(2 6 -3 0 4.689))
Entering procedure fancy-add2 with parameter (2 6 -3 0 4.689)
Entering procedure fancy-add2 with parameter (6 -3 0 4.689)
Entering procedure fancy-add2 with parameter (-3 0 4.689)
Entering procedure fancy-add2 with parameter (0 4.689)
Entering procedure fancy-add2 with parameter (4.689)
Entering procedure fancy-add2 with parameter ()
Leaving procedure fancy-add2 with result ()
Leaving procedure fancy-add2 with result (6.689)
Leaving procedure fancy-add2 with result (2 6.689)
Leaving procedure fancy-add2 with result (-1 2 6.689)
Leaving procedure fancy-add2 with result (8 -1 2 6.689)
Leaving procedure fancy-add2 with result (4 8 -1 2 6.689)
(4 8 -1 2 6.689)

This works, but it's a nuisance to insert the calls to entering and leaving. Chez Scheme provides a built-in tracing facility that gives you the same information and is much easier to use. Here's the idea: If you replace the keyword define in a procedure definition with the keyword trace-define, Chez Scheme will generate a line of output when it enters or leaves the procedure:

(trace-define traced-add2
  (lambda (ls)
    (if (null? ls)
        '()
        (cons (+ (car ls) 2) (traced-add2 (cdr ls))))))

> (traced-add2 '(2 6 -3 0 4.689))
|(traced-add2 (2 6 -3 0 4.689))
| (traced-add2 (6 -3 0 4.689))
| |(traced-add2 (-3 0 4.689))
| | (traced-add2 (0 4.689))
| | |(traced-add2 (4.689))
| | | (traced-add2 ())
| | | ()
| | |(6.689)
| | (2 6.689)
| |(-1 2 6.689)
| (8 -1 2 6.689)
|(4 8 -1 2 6.689)
(4 8 -1 2 6.689)

Each ``entering'' line reproduces the procedure call, with the value of the parameter written out in the argument position. Each ``leaving'' line shows the value returned by the procedure call. The vertical bars at the left-hand side are designed to help you connect the ``entering'' line for a particular procedure call to the ``leaving'' line for the same call.

There's one exception to this pattern. It often happens that two successive ``leaving'' lines would necessarily be exactly alike, because the procedure being traced simply returns the result of a recursive call without performing any more computation on it. In this situation, Chez Scheme will print only one ``leaving'' line even when there are two or more matching ``entering'' lines.

For example, suppose you trace the textbook's procedure for recovering the last item from a non-empty list (Program 2.2, page 47):

(trace-define last-item
  (lambda (ls)
    (cond ((null? (cdr ls)) (car ls))
          (else (last-item (cdr ls))))))

If ls has more than one element, its last item is same as the last item of its cdr, so after the last item of the cdr has been recovered, there's no more computation to do. Here's what a typical trace of this procedure looks like:

> (last-item '(3 1 4 1 5 9 2 6 5))
|(last-item (3 1 4 1 5 9 2 6 5))
|(last-item (1 4 1 5 9 2 6 5))
|(last-item (4 1 5 9 2 6 5))
|(last-item (1 5 9 2 6 5))
|(last-item (5 9 2 6 5))
|(last-item (9 2 6 5))
|(last-item (2 6 5))
|(last-item (6 5))
|(last-item (5))
|5
5

There is only one ``leaving'' line for all nine ``entering'' lines, because the ``leaving'' lines would all necessarily show the same value.


Exercise 7

Here is a definition of a procedure called iota that takes any non-negative integer upper-bound as argument and returns a list of the non-negative integers strictly less than upper-bound, in ascending order:

(define iota
  (lambda (upper-bound)
    (iota-kernel 0 upper-bound)))

(define iota-kernel
  (lambda (so-far upper-bound)
    (if (= so-far upper-bound)
        '()
        (cons so-far (iota-kernel (+ so-far 1) upper-bound)))))

Use trace-define to turn on tracing for iota and iota-kernel. Trace the call (iota 7).


Exercise 8

The factorial of a given positive integer is the product of all the positive integers less than or equal to it. For instance, the factorial of 5 is 1 * 2 * 3 * 4 * 5, or 120. Write a Scheme procedure that computes the factorial of a given positive integer. Trace the call (factorial 12). Explain the sequence of calls which led to the tracing printed.


Exercise 9

The binomial coefficient C(n, k) of a pair of natural numbers n and k is equal to 0 if n < k, and to 1 if k is 0 or if n = k; in any other case,C(n, k) = C(n - 1, k - 1) + C(n - 1, k). Write a Scheme procedure binomial that computes the binomial coefficient of two given natural numbers. Trace the call (binomial 5 2).Explain the sequence of calls which led to the tracing printed.


Work To Turn In:


This document is available on the World Wide Web as

http://www.math.grin.edu/~walker/courses/151.sp99/lab-side-effects.html

created February 12, 1997
last revised February 20, 1999

Henry Walker (walker@math.grin.edu) and John David Stone (stone@math.grin.edu)