Lecture 4: Building Lists, Printing, the Let Construct


Defining the average function to find the average value of a list.
The "map_list" function transforms a list in a general way.
(display obj) outputs an object to the terminal
(write obj) outputs an object read to be read back into Scheme
(begin expr1..exprn) evaluates the expressions in sequence
(let (binding1..bindingn)body) binds local variables, evaluates expr
(let* (binding1..bindingn)body) binds locals, evaluates
(letrec (binding1..bindingn)body) defines local recursive function

average - our first example with a non-trivial data-structure

Suppose we wanted to find the average of a list of numbers. In Scheme it is best to write a program using quite small functions. It is worth remembering that any loop in an imperative language translates into a recursive function in Scheme - so two nested loops translate into two functions.

The average is the sum of the numbers in the list divided by how many numbers there are in the list. We could define it by:


(define (average list)
    (/ (sum list) (length list)))

When we compile this function, Scheme responds:

    DECLARING VAR sum

This indicates to you that Scheme does not know about the function sum that you have called, so you will have to define it.

It also indicates that Scheme does know about the function length. So you had better check if the built-in function 'length' does the right thing. You can redefine built-in functions in Scheme, but it is not a good idea. (Actually length does just what we want). Try it!


    (length '(the fat cat eats the thin canary))

evaluates to 7.

If we try running average, you will get an error-message, identifying 'sum' as the culprit:


    (average '(1 2 3))
Error: Cannot apply object Uninitialised variable 'sum as function
In file: /users/users3/fac/pop/poplocal/local/Scheme/lecture4.scm

Value = 1
This error report was prepared for Robin Popplestone
 by Jeremiah Jolt, your compile-time helper.

We can now mend our program.


(define (sum list)
    (if (null? list)
        0
        (+ (car list) (sum (cdr list)))))

And try sum


    (sum '(1 2 3))

    6

Finally evaluating


    (average '(1 2 3))

yields:


    2

while:


    (average '(1 2 3 4))

evaluates to the rational number:


    5/2

So perhaps it is better to do


    (average '(1.0 2.0 3.0 4.0))

    2.5

Building structures.

In the last example we made a simple number from a list, using car and cdr to explore the list. In this example we build a new list.

Suppose we want to take a list of numbers and replace each number in the list by a number twice as big. We could do it something like this:


(define (double_list l)
   (if (null? l)
      '()
       (cons (* (car l) 2) (double_list (cdr l)))))

Trying this out:


    (double_list '(22 33 44))

    (44 66 88)

How could we make this more general? Suppose we wanted to multiply by any number? We could replace the constant 2 by a parameter. This is called abstraction.


(define (multiple-list n l)
   (if (null? l)
      '()
       (cons (* (car l) n) (multiple-list n (cdr l)))))
(multiple-list 10 '(5 6 7)) (50 60 70)

Suppose we then wanted to


(define (sqrt_list l)
   (if (null? l)
      '()
       (cons (sqrt (car l)) (sqrt_list (cdr l)))))

(sqrt_list '(4 100 16))

(2 10 4)

The "map_list" function generalises our list operations.

Look at what we are doing here - we are applying a function, sqrt , to every member of the list. Can we generalise this idea? This depends on an important property of Scheme - we can pass functions as arguments to other functions. So: we can replace the call of sqrt   in sqrt_list by a call of f , where f is an argument of a new function I shall call map_list.


(define map_list
    (lambda (f l)
        (if (null? l)
            '()
            (cons
                (f (car l))
                (map_list f (cdr l))
                )
            ); end if
        )    ; end lambda
    )        ; end define

Using map_list we can take the square-root of a list of functions quite easily:

(map_list sqrt '(4 100 16)) ===> '(2 10 4)

But, we can also produce a list indicating where a zero occurs in another list:


(map_list zero? '(0 4 7 0)) ===> '(#t #f #f #t)

And we can produce a list of the squares of values of members of a given list:


(map_list (lambda (x) (* x x)) '(2 4 7))   ==> (4 16 49)

Evaluation of map_list in detail

It is worth looking in detail at the evaluation of one of these map_list examples. This is written out below. If you are on-line, it is worth marking each successive form of this expression and evaluating it - they all evaluate to '(4 100).


    (map_list
        (lambda (x) (* x x))
        '(2 10))
==> ; evaluate function and arguments,  using the definition of map_list

(
 (lambda (f l)
     (if (null? l)
         '()
         (cons
             (f (car l))
             (map_list f (cdr l))
             )
         ); end if
     )    ; end lambda
 (lambda (x) (* x x))
 '(2 10))
===> ; substitute arguments for parameters in body of lambda expression.

(if (null? '(2 10))
    '()
    (cons
        ((lambda (x) (* x x)) (car '(2 10)))
        (map_list (lambda (x) (* x x)) (cdr '(2 10)))
        )
    ); end if
 
===> ; evaluate the first argument of the special form if, choose accordingly

    (cons
        ((lambda (x) (* x x)) (car '(2 10)))
        (map_list (lambda (x) (* x x)) (cdr '(2 10)))
        )
===> ; evaluate the function and arguments of the cons expression

    (cons
        ((lambda (x) (* x x)) 2)
        (map_list (lambda (x) (* x x)) '( 10))
        )
===> ; which involves recursively evaluating  map_list , etc

    (cons
        (* 2 2)
        (
             (lambda (f l)
                 (if (null? l)
                 '()
                 (cons
                     (f (car l))
                     (map_list f (cdr l))
                     )
                 ); end if
             )    ; end lambda
         (lambda (x) (* x x))
         '(10))
         )
===> again, substituting in the lambda-body

    (cons 4
        (if (null? '(10))
            '()
            (cons
                ((lambda (x) (* x x)) (car '(10)))
                (map_list (lambda (x) (* x x)) (cdr '(10)))
                )
            ); end if
        )
===> and evaluating the if

    (cons 4
        (cons
            ((lambda (x) (* x x)) (car '(10)))
            (map_list (lambda (x) (* x x)) (cdr '(10)))
            )
        )
===> evaluating the arguments of cons

    (cons 4
        (cons
            ((lambda (x) (* x x)) 10)
            (map_list (lambda (x) (* x x)) '() ))
            )
===> substituting in one lambda and using the definition of map_list (for the last time, happily!)

    (cons 4
        (cons
            (* 10 10)
            (
             (lambda (f l)
                 (if (null? l)
                     '()
                     (cons
                         (f (car l))
                         (map_list f (cdr l))
                         )
                     ); end if
                 )    ; end lambda
             (lambda (x) (* x x))
             '()))
        )
===> substituting in body of lambda

    (cons 4
        (cons 100
            (if (null? '())
                '()
                (cons
                    ((lambda (x) (* x x)) (car '(10)))
                    (map_list (lambda (x) (* x x)) (cdr '(10)))
                    )
                ); end if
            )
        )
===> now we stop recursing because the condition is true

    (cons 4
        (cons 100
                '()
            )
        )
===> and we are home and dry!

    (cons 4 '(100))

===>

    '(4 100)

Strings

Scheme provides strings as a datatype. For our purposes, a string can be regarded as a sequence of characters enclosed in double quotes, for example

"such bad news for the little tweetleoos - Big Bad Cat is on the loose"

is a string. Do NOT confuse strings and atoms. Strings are used mostly for generating output text whose structure will be unanalysed. There is no case conversion with strings.


write and display - our first imperative constructs

When a program changes the outside world, for example writing a message on the screen or a printer, it is hard to preserve a pure functional model. Scheme provides the following procedures:

       (write   obj)
       (display   obj)

which will write an object on the current-output-port. The difference between the two is that write is intended to produce output that can be read back into Scheme, whereas display is intended to produce output that can easily be read back. In particular display does not print strings with quotes round them.


     (display "hello")

outputs

     hello

     (write "hello")

outputs

     "hello"

UMASS Scheme also provides:

       (writeln obj1....objn)

which outputs all its arguments starting on a new line (using display).

        (writeln 2 3 4)

The "begin" special form

Procedures like write, which have an outside effect, are often combined using the begin special form.

   (begin expr1 expr2 ... exprn )

Evaluate the expressions in order. The value of the begin construct is the value of the last expression, exprn.

For example,

(define (test x)
    (begin
       (display "the value of x is ")
       (display x)
       (newline)
    )
)

(test 45)
the value of x is 45

Actually, the begin statement is not necessary in the test function above, since Scheme allows one to have a sequence of expressions as the body of a function, although the begin may make the code clearer. You are encouraged to use begin in such circumstances.

It may sometimes be useful to have a begin expression in circumstances in which you need to attain an outside effect and still have an expression which has a definite value.


(define (monitor_and_add x y)
    (+
       (begin
           (display "the value of x is ")
           (display x)
           (newline)
           x
           )
       y
       )
    )
(monitor_and_add 2 3)

which produces the print out "the value of x is 2" and has the value 5. Here the begin expression acts as one of the arguments of +. Since its value is x, this will be added to y to obtain the result of the function.

Without the begin, we are trying to add up the values of all the arguments of +, which would cause an error.


(define (monitor_and_add x y)
    (+
       (display "the value of x is ")
       (display x)
       (newline)
       x
       y
       )
    )
If we evaluate: (monitor_and_add 2 3) we get:
Error: NUMBER(S) NEEDED
Culprits: "Undefined Value", 5,
In file: /users/users3/fac/pop/poplocal/local/Scheme/lecture4.scm

Equality

There are three functions in Scheme which can be used to test whether objects are equal, namely eq?, equal? and = . Of these equal? is the most generally useful. The = function is intended to used for numbers. The use of eq? will be explained later.


The "let" construct

Scheme has a special form which allows you to introduce local variables into a function.

      (let bindings body)

Here body  is a sequence of expressions and bindings  has the form:

       ( (variable1 init1) ...)

The meaning of the " let " construct can be understood as follows. All of the init expressions are evaluated, and the corresponding variable bound to each of them. Each expression of the body is then evaluated in sequence, using the new variable bindings. The value of the let expression is the value of the last expression in the body

A simple example of the use of let is:


    (let ((a 2) (b 3) ) ( + a b))

which evaluates to 5. While there is no benefit in the use of let in this case (since (+ 3 5) is a simpler way to write this, we can usefully use let when we have a sizable common-subexpression in something we want to evaluate. For example, if we are doing geometry in our computer we will often want to


    (let ((d (sqrt (+ (* x x) (* y y)))))
          (+ (/ x d) (/ y d)))

[We need to revise this explanation when we come to consider the imperative constructs in Scheme]

The variable bindings in the let construct are local, that is to say the variables have the new values only inside the body of the let statement. Thus the sequence:


(define x 55)

(begin
    (writeln "before let x=" x)
    (let ((x 2))
         (writeln "in let, x=" x)
    )
    (writeln "after let, x=" x)
)

produces the output below (the first 55 arises from the (define x 55) expression, and the "Undefined Value" is the result of the begin statement, that is in fact the result of the last writeln expression.

55
before let x=55
in let, x=2
after let, x=55

"Undefined Value"

Note that all the expressions are evaluated before any of the variables are bound. So,


(define x 55)

(let ((x 3) (y x)) (+ x y))

evaluates to 58, because the value of x that y is bound to is obtained before x is rebound to 3.


Let as lambda

The let construct does not add any new semantic facility to Scheme, because every let expression can be rewritten as a lambda expression applied to arguments. The transformation is quite simple. Take the variables of the let expression as the variables of the lambda expression, take the body of the let expression as the body of the lambda expression and take the init expressions in order as the arguments of the whole lambda expression.

(let ((x 3) (y (+ 2 5))) (+ x y))

((lambda (x y) (+ x y)) 2 (+ 2 5))

let* allows you to use let-bound variables in later bindings

There is also a special form let which is the same as let except that each init expression is evaluated and the value bound to the corresponding variable before the next init expression is evaluated. So


(let* ((x 3) (y x)) (+ x y))

evaluates to 6.


letrec allows you to define recursive functions.

It is sometimes convenient to define recursive functions locally, especially if they are acting as "helper" functions, so that there is no point in cluttering up global name-space with unnecessary names. However we can't do this using let. Consider:


    (let  ((sum1 (lambda (l)
                (if (null? l) 0  (+ (car l) (sum1 (cdr l)))))))
          (sum1 '(2 3 4)))

The problem is that the Scheme compiler will treat sum occurring in the lambda  expression as a different variable from the one introduced in the let  binding. The occurence of sum   in the lambda expression is global , whereas that introduced by the let binding is local.

This problem can be solved by using the letrec special form, which allows you to define recursive functions locally. Syntactically this is the same as let except that letrec replaces let. For example:


(letrec  ((sum (lambda (l)
            (if (null? l) 0  (+ (car l) (sum (cdr l)))))))
      (sum '(2 3 4)))
In the above example we called the sum function that we defined using letrec to add up the members of a list. This is one possible use of letrec: in effect we may keep our program tidy and avoid cluttering up name-space with function names that have only local significance by using letrec. However we can use letrec as a way of creating a function that we use outside the letrec construct. For example the expression

(letrec
    ((sum (lambda (l)
            (if (null? l) 0
              (+ (car l) (sum (cdr l)))))))
      sum)
is the sum function, and we could write:

(define sum
    (letrec
        ((sum (lambda (l)
                (if (null? l) 0
                  (+ (car l) (sum (cdr l)))))))
        sum))
Theorists would argue that the above is the correct way to define sum; we'll discuss their argument later in the course.