YCP Logo Lecture 18: Scheme

Scheme

Scheme is a programming language inspired by the lambda calculus. It was invented by Gerald Sussman and Guy Steele in the early/mid 1970s.

It is a simplification of LISP, one of the first programming languages.

Features

Interpreted

Dynamically typed

Basic data types: numbers, symbols, strings, lists, vectors, functions

Functional: very few constructs have side effects. When programming in the preferred style, there are no destructive assignments to variables.

Scheme is available on cygwin and Linux as "guile".

Syntax

Literals

Kind Examples
number 42, 3.14159
boolean #t, #f
symbol 'a, 'hello, 'foobar, '+
string "hello, world"
list '(item1 item2 item3 "item4" 5)

Note that symbols must be "quoted" when we want a literal symbol value; otherwise, they are treated as variable references. E.g.:

guile> '+
+
guile> +
#<primitive-procedure +>

Note that literals introduced with a single quote are really a short-hand for the quote syntactic form. So, the Scheme expressions

'a

and

(quote a)

mean the same thing.

Similarly, lists must be quoted when we want a literal list of values; otherwise, they are treated as function applications.

Examples:

guile> '(+ 2 3)
(+ 2 3)

Here we have a literal list containing the symbol "+" and the numeric literals 2 and 3. Function application

guile> (+ 2 3)
5

This is a function application: we are applying the function + to the literal values 2 and 3, which are numbers.

An argument to a function can be a function application:

guile> (* (+ 2 3) (+ 4 5))
45

Atoms and Lists

Number, string, character, and symbol values are all "atoms" in Scheme. In other words, their corresponding types are primitive types.

(Actually, strings contain characters, so they're not really primitive. But traditionally, Scheme considers them to be primitive.)

Scheme programs use lists as the primary composite data type. Lists are composed of cons cells. Each cons cell contains a single value, the car, and a reference to another value that continues the list, the cdr (pronounced "could-er"). A special "empty list" value exists, and is written as a literal

'()

Let's return to the literal list

'(+ 2 3)

This list is represented in the following way:

figures/schemeList.png

There are three cons cells forming the "backbone" of the list. The car values of the three cons cells contain the symbol + and the numbers 2 and 3, respectively. The list is terminated when the final cons cell, as its cdr value, contains the empty list.

Lists are constructed using the cons function, which creates a new cons cell with given car and cdr values.

Examples:

guile> (cons 3 '())
(3)
guile> (cons 2 (cons 3 '()))
(2 3)
guile> (cons '+ (cons 2 (cons 3 '())))
(+ 2 3)

In this way, we can build up longer lists by consing new values onto the beginning of existing lists.

Functions

Functions in Scheme are written using the lambda construct.

For example

(lambda (x y) (+ x y))

This function takes two parameters, x and y, and uses the built-in + function to add them.

We can call this function using the normal function application syntax:

guile> ((lambda (x y) (+ x y)) 2 3)
5

It would be inconvenient if we had to specify each function using its complete text whenever we want to call it. So, Scheme gives us a construct called define that allows us to give names to functions. E.g.:

(define add
  (lambda (x y)
    (+ x y)))

Once a function has been defined in this way, we can call it by name (just as we can with the built-in functions.)

guile> (add 2 3)
5

It is important to point out that we don't actually need the define construct; the lambda construct itself gives us a perfectly good way of binding a function (or any other value) to a variable.

guile> ((lambda (add) (add 2 3)) (lambda (x y) (+ x y)))
5

Let's examine what's happening here. The overall form is a function application that applies the function

(lambda (add) (add 2 3))

to the single argument

(lambda (x y) (+ x y))

The argument is substituted for the parameter add within the body of the applied function. When the body is evaluated, it passes the values 2 and 3 for the parameters x and y, resulting in their sum being computed.

Decisions

A predicate is a function that returns a boolean value. Examples of predicates:

Predicate Params Behavior
null? 1 returns #t if param is the empty list, #f otherwise
equal? 2 returns #t if params are the same value, #f otherwise
= 2 returns #t if params are equal numerically, #f otherwise
< 2 less than comparison
> 2 greater than comparison
integer? 1 returns #t if param is integer, #f otherwise
list? 1 returns #t if param is a list, #f otherwise
not 1 logical negation; returns #f if param is #t, #t if param is #f

Demonstration:

guile> (null? '())
#t
guile> (null? '(a b c))
#f
guile> (equal? 'a 'a)
#t
guile> (equal? 'a 'b)
#f
guile> (equal? '(a b c) '(a b c))
#t
guile> (= 2 3)
#f
guile> (= 3 3)
#t

Scheme supports two basic decision-making constructs: if and cond. These constructs differ from decision-making constructs in imperative languages because they are expressions (i.e., they yield a value).

General form of an if expression:

(if condition value-if-true value-if-false)

The condition is an expression which yields a boolean (#t or #f) value. value-if-true is the value of the overall expression if the condition is true (#t). value-if-false is the value of the overall expression if the condition is false (#f).

Demonstration:

guile> (if #t 'a 'b)
a
guile> (if #f 'a 'b)
b
guile> (if (integer? (/ 3 2)) "even" "odd")
"odd"
guile> (if (integer? (/ 4 2)) "even" "odd")
"even"

The cond construct is a multi-way if. It allows the program to test for a number of possibilities.

General form:

(cond
  (condition1 expr1)
  (condition2 expr2)
  ...
  (#t default-value-expr))

If condition1 is true, then the value of the cond expression is expr1. If condition2 is true, then the value of the cond expression is expr2. And so forth. By convention, the "default" value of a cond expression is produced by using #t (true) as the final condition.

Recursion

Scheme uses recursion instead of iteration (loops) to express repetition. Scheme functions often work on lists. The structure of recursive list-processing function is typically

the empty list is the base case

process the first value in the list (the car)

recursively process the rest of the list (the cdr)

Example: counting the number of items in a list

(define count-members
  (lambda (lst)
    (if (null? lst)
        0
        (+ 1 (count-members (cdr lst))))))

Example: counting the number of odd values in a list of values.

(define odd?
  (lambda (val)
    (not (integer? (/ val 2)))))

(define count-odd-values
  (lambda (lst)
    (if (null? lst)
        0
        (let ((item (car lst)))
          (cond
            ((not (number? item)) (count-odd-values (cdr lst)))
            ((odd? item) (+ 1 (count-odd-values (cdr lst))))
            (#t (count-odd-values (cdr lst))))))))

Demonstration

guile> (count-odd-values '(2 4 6 8 10))
0
guile> (count-odd-values '(1 3 5 7 9))
5
guile> (count-odd-values '(98 46 91 96 59 10 1 73 3 51))
6

Tail Recursion

Recall that each invocation of a function results in the creation of an activation record. Scheme has no looping constructs. So, what does that imply about a Scheme function that invokes itself recursively 1,000,000 times?

It should imply that the program will create 1,000,000 simultaneous activation records!

If this were true, Scheme would not be a very useful programming language, because its stack of activation records would require too much space.

Fortunately, Scheme gets around this problem for functions that are tail recursive. In a tail recursive function, any recursive invocations of the function are in tail position, meaning that the result of the recursive call becomes the result of the function.

Neither of the recursive functions above (count-members and count-odd-values) is tail recursive. Generally, making a function tail-recursive requires using a separate helper function.

Here is a tail-recursive version of count-members (using a helper function called count-members-work)

(define count-members
  (lambda (lst)
    (count-members-work lst 0)))

(define count-members-work
  (lambda (lst count)
    (if (null? lst)
        count
        (count-members-work (cdr lst) (+ count 1)))))

Demonstration:

guile> (count-members '(a b c d))
4
guile> (count-members '())
0

The idea is that the "count" parameter of the count-members-work function keeps track of the number of list members seen so far. Note that the single recursive call to count-members-work function is in tail position, because the value it computes is used as the result of the entire function.

So, how does tail recursion help with the problem of too many activation records? The answer is that when a call to a recursive function in tail position is made, nothing else remains to be done in the context of the calling function. So, the Scheme interpreter can simply re-use the existing activation record. For example, the call to the count-members-work function can be handled this way:

overwrite lst with (cdr lst)

overwrite count with (+ count 1)

jump back to the beginning of the count-members-work function

In other words:

a tail call can be optimized into an unconditional jump (goto)

Let Expressions

A let expression is a way of defining local variables in a scheme function. It has the general form

(let
  ((var1 expr1)
   (var2 expr2)
   ...)
  body-expr)

When the let expression is evaluated, expr1 is evaluated and its value assigned to var1, expr2 is evaluated and its value assigned to var2, etc. Then body-expr is evaluated, and its value is the overall result of evaluating the let expression.

Example:

(define make-int-list
  (lambda (first max)
    (if (= first max)
        (cons first '())
        (cons first (make-int-list (+ first 1) max)))))

(define sieve
  (lambda (n)
    (sieve-work (make-int-list 2 n))))

(define sieve-work
  (lambda (lst)
    (if (null? lst)
        '()
        (let ((prime (car lst)))
          (cons prime (sieve-work (remove-multiples-of prime (cdr lst))))))))

(define remove-multiples-of
  (lambda (prime lst)
    (if (null? lst)
        '()
        (let ((first (car lst))
              (rest-finished (remove-multiples-of prime (cdr lst))))
          (if (integer? (/ first prime))
              rest-finished
              (cons first rest-finished))))))

The sieve function uses the Sieve of Eratosthenes algorithm to generate the list of prime numbers in the range 2 to n, for some value of n.