CSE341 Notes for Monday, 4/29/24

We are now turning our attention to Scheme. I reminded people that at the beginning of the quarter I had given the analogy that studying OCaml and then studying Scheme is a little like a westerner visiting Tokyo and then visiting Beijing. The residents of Tokyo and Beijing would see their cities and their cultures as being quite different, but from our point of view as westerners, we see a lot in common between the two. Scheme has many of the same constructs that OCaml does and we use similar techniques, but we'll also see that Scheme has its own interesting features that are quite distinct from OCaml.

I talked a little bit about the history of the language. Scheme is a direct descendant of Lisp. Lisp was invented by John McCarthy in 1958. Unlike many of the early computing pioneers, McCarthy was more interested in symbolic computation than in numeric computation. He became a pioneer in the field of artificial intelligence research and Lisp became the dominant programming language among AI researchers.

One thing that people like about Lisp and Scheme is that they are very simple languages. They do not have a lot of syntax or special rules to memorize. This was even more true of Lisp than it is of Scheme, but Lisp was too simple. It led to many different flavors of Lisp and made it difficult to execute the same Lisp program on different systems. Two major variations have emerged as the dominant players: Common Lisp and Scheme.

Scheme got a significant boost in the 1980's when Hal Abelson and Jerry Sussman published the textbook they were using in the introductory computer science class at MIT. The book is titled Structure and Interpretation of Computer Programs, also known as the wizard book or SICP. It is one of the best books ever written about computer science. A lot of schools tried using it as an intro book, but that movement has slowly lost steam over time. Still, the book is a great resource. It is available online and I have included links to the book and to Abelson and Sussman's lectures from the "racket" tab of our class page.

We spent most of the rest of the lecture time looking at how the Scheme interpreter evaluates various expressions. We are using the DrRacket environment that will run on most platforms. You can install it on your own computer. When it first executes, it will ask you which language you want to use. Select the first choice, "Racket."

We saw that numbers evaluate to themselves, as in:

        > 3.8
        3.8
        > 42
        42
There are special symbols that represent true and false that also evaluate to themselves:

        > #t
        #t
        > #f
        #f
Just as in OCaml, there are some predefined functions available at the top level:

        > length
        #<procedure:length>
        > abs
        #<procedure:abs>
Scheme calls these procedures rather than functions (I'm likely to use both words because I'm more used to calling these functions). The two listed above are for the absolute value of a number and the length of a list.

In addition to symbols, Scheme has lists of values enclosed in parentheses. To call a function, you put its name as the first value in the list. For example, we can add two numbers by saying:

        > (+ 3 4)
        7
These expressions can be nested, so we can ask for 2 times the sum of 8 and 12 by saying:

        > (* 2 (+ 8 12))
        40
If you want to define a variable to have a value, you can call the define function:

        > (define x 3.4)
        > x
        3.4
After this definition, you can use x in expressions, as in:

        > (define y (+ x 3))
        > y
        6.4
One of the things that takes a while for beginners to get used to is figuring out when Scheme evaluates something and when it does not. For example, you can define one symbol to be bound to another symbol, but not by saying:

        > (define z x)
        > z
        3.4
Scheme binds z to the value of x rather that binding it to the symbol x (if not, our previous example would not have worked out). So we often need to make a call on quote:

        > (define z (quote x))
        > z
        x
This can be abbreviated with the single quotation mark:

        > (define z 'x)
        > z
        x
Using the quote, we can also bind variables to lists:

        > (define a '(1 2 3 4))
        > a
        (1 2 3 4)
We then saw that the OCaml functions hd and tl have counterparts in the Scheme functions first and rest:

        > (first a)
        1
        > (rest a)
        (2 3 4)
But most people use the functions car and cdr (pronounced "kar" and "cou-der") that have been part of Lisp since the beginning:

        > (car a)
        1
        > (cdr a)
        (2 3 4)
There are abbreviations for things like the car of cdr of the cdr of the cdr:

        > (car (cdr (cdr (cdr a))))
        4
        > (cadddr a)
        4
This is pronounced "ka-dih-dih-der." If you don't want to struggle with pronunciation, you can use the simpler name "fourth":

        > (fourth a)
        4
We saw that the function cons is the equivalent of the OCaml :: operator:

        > (cons 'a ())
        (a)
        > (cons 'a (cons 'b ()))
        (a b)
We had to use quotes here because Scheme evaluates each of the arguments passed to the cons function.

I pointed out that define is considered a special form because it does not evaluate its first argument (the variable being defined does not need to be quoted).

We define functions in Scheme with the following syntax:

(define (<name> <param> <param> ... <parm>) <exp>) For example, we can define a function that doubles its argument as follows:

        (define (double n) (* 2 n))
We tend to read this as, "Define double of n to be the times of 2 and n."

In OCaml we had an if/else construct. OCaml uses keywords to separate the different parts:

if <test> then <exp1> else <exp2> In Scheme we put these different elements in a list without any special keywords:

(if <test> <exp1> <exp2>) For example, we might say:

        > (if (< 3 4) "hello" "there")
        "hello"
We put these pieces together to define a factorial function. I defined it in the edit window rather than in the interpreter:

        (define (factorial n)
          (if (= n 0)
              1
              (* n (factorial (- n 1)))))
We then clicked on the "Run" button to go back to the interpreter with this definition loaded. We could then run it with various values:

        > (factorial 3)
        6
        > (factorial 5)
        120
I also pointed out that Scheme supports arbitrary length integers, as in:

        > (factorial 50)
        30414093201713378043612608166064768844377641568960512000000000000
As one final example, we implemented a function called range that takes the place of the "--" infix operator we defined in OCaml:

        (define (range x y)
          (if (> x y)
              '()
              (cons x (range (+ x 1) y))))
This version of range considers the second parameter to be inclusive, as in:

        > (range 1 20)
        '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20)
I said that Racket has a built-in range function that uses the familiar convention where the second parameter is exclusive, as in:

        > (range 1 20)
        '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19)
That built-in version also has a variation where you can provide just one parameter to indicate how many values you want included:

        > (range 20)
        '(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19)

Stuart Reges
Last modified: Mon Apr 29 13:58:06 PDT 2024