Imperative Programming in Racket

We do not cover this material in class for several reasons. You have a lot of experience doing procedural imperative programming in other languages, so most of the ideas here are not new. If you are having some trouble with Racket, or especially its imperative features, you may want to study this section in some detail. And, as always, feel free to ask any questions you may have.

Implementing Imperative Abstractions

The special form begin enables us to create a sequence of expressions to be evaluated in order. This can be useful when we are doing input and output.

For example, we can use begin to create a function similar to the for statement available in Python and many other programming languages:

(define for-each
  (lambda (proc lst)
    (if (null? lst)
        'done
        (begin
          (proc (first lst))
          (for-each proc (rest lst))))))

for-each performs the operation proc on each member of the list lst, in order. It returns the symbol done as its value.

Quick Exercise: Compare and contrast for-each with the built-in function map. How are they alike? How are they different?

Now, we can use the for-each function to write another function that displays each member of a list in order on a single line, without displaying the return value of the expression on the same line:

> (define displayln
    (lambda lst
      (begin
        (for-each display lst)
        (newline))))

> (displayln "The answer is: " (+ 3 4 5))
The answer is: 12

This uses two of Racket's primitive I/O procedures, display and newline.

It turns out that the begin expression in the last procedure is not really necessary. lambda will take any number of expressions in its body. We could have written the displayln procedure like this:

> (define displayln
    (lambda lst
      (for-each display lst)
      (newline)))

> (displayln "The answer is: " (+ 3 4 5))
The answer is: 12

Up to now, we have only written lambda expressions that have a single expression in their bodies. We did not need more, because we were creating "true" functions, procedures that computed and returned a single value.

When a lambda expression has multiple expressions in its body, the value of the last expression is returned as the result of the lambda. let expressions, letrec expressions, and the consequents of cond expressions also take multiple expressions without being enclosed in a begin expression.

Now that we know about output procedures and sequencing, we can add output procedure calls to our code for debugging purposes:

> (define factorial
    (lambda (n)
      (displayln "Entering factorial with n = " n)
      (let ((ans (if (zero? n)
                      1
                      (* n (factorial (- n 1))))))
        (displayln "Returning from factorial with " ans)
        ans)))

> (factorial 5)
Entering fact with n = 5
Entering fact with n = 4
Entering fact with n = 3
Entering fact with n = 2
Entering fact with n = 1
Entering fact with n = 0
Returning from fact with 1
Returning from fact with 1
Returning from fact with 2
Returning from fact with 6
Returning from fact with 24
Returning from fact with 120
120

Other Input/Output Statements

There are other low-level I/O statements available in Racket that you might want to be aware of:

For example, we can use read and write to create our own read-eval-print loop:

> (define read-eval-print
    (lambda ()
      (display "--> ")
      (write (eval (read)))
      (newline)
      (read-eval-print)))

> (read-eval-print)
--> (+ 2 3)
5
--> (* 4 5)
20
--> (cdr '(3 4 5))
(4 5)
--> (cdr 4)
cdr: expects argument of type <pair>; given 4

We use display to print the prompt since we don't it to have quotes around it. We use read to read entire expressions. We use write to print to results so that they contain relevant information such as quotes when they are printed out:

> (read-eval-print)
--> "3 4 5"
"3 4 5"
--> #\x
#\x
-->
>                   ;; I hit the 'Stop' button.

Try implementing some other functions that take advantage of sequencing and I/O...

Racket is a full-bodied extension of Scheme, and it provides many, many higher-level forms for doing I/O, such as the file->lines function we saw in Session 6's mass-to-fuel problem:

(define total-fuel
  (lambda (filename)
    (apply +
           (map mass->fuel
                (map string->number
                     (file->lines filename))))))

Sequences of Statements

As you've now seen, Racket has a special form named begin:

(begin
  <expr_1>
  ...
  <expr_n>)

that enables us to specify a sequence of expressions to evaluate. All but the last will typically be an action with a side effect, such as doing I/O or changing the value associated with a name.

The begin expression guarantees that Racket will execute the expressions inside it in order. For example, we can write:

> (begin
    (display "The answer is: ")
    (display (+ 2 4 (* 4 5))))
The answer is: 26

Though we have never done so, Racket also allows sequences in the bodies of several of the special forms we have used this semester. These include lambda expressions, let expressions, and the individual clauses of a cond expression. For example:

> ((lambda (x y)
     (display "Simulating a let expression")
     (newline)
     (display (+ 2 x (* x y))))
   4 5)
Simulating a let expression      ; printed by call to display
26                               ; print by the REPL

And:

> (define print-sign
    (lambda (n)
      (cond ((> n 0) (display "positive") (newline)  1)
            ((< n 0) (display "negative") (newline) -1)
            (else    (display "zero")     (newline)  0))))
> (print-sign 4)
positive
1

Wait a second... (print-sign 4) seems to give two outputs. Why? Let's see if we can tease them apart:

> (define sign (print-sign 4))
Positive!
> sign
1
> (display sign)
1
> (+ 1 (print-sign 4))
positive
2
> (+ 1 (display sign))
1
+: contract violation
   expected: number?
   given: #<void>;
   argument position: 2nd
   other arguments...:

The display expression prints one of the outputs. The REPL then prints the value of the entire expression, which is the other. But display itself does not return a value!

Next session, we will see another way to use sequences of statements when we find that we need to "mutate state", that is, change the value of a variable!

Code

All of the code for this reading is available in this Racket file. You can use it to experiment with the ideas you saw above.