CS 331 Spring 2025  >  A Quick Introduction to Scheme


CS 331 Spring 2025
A Quick Introduction to Scheme

By Glenn G. Chappell
Text updated: 2025-04-29

Table of Contents

  1. The Scheme Programming Language
  2. Expressions
  3. Defining
  4. Source Files
  5. Selection & Recursion
  6. Local Variables
  7. Lists & Quoting
  8. First-Class Code
  9. Topics Not Covered
  10. Copyright & License

1. The Scheme Programming Language

History

Scheme is a programming language created at the MIT Artificial Intelligence Lab around 1970 by Guy Steele and Gerald Sussman. It was based on the Lisp programming language, which was initiated in the late 1950s by MIT professor John McCarthy and Dartmouth student Steve Russell.

Scheme has found a niche in teaching computer programming from a theoretical point of view. Books like The Little Schemer by Daniel P. Friedman introduce various CS concepts using Scheme.

Scheme was standardized in 1991 by IEEE. A series of defacto standards have since been issued as reports by a group called the Scheme Language Steering Committee. The most recent came in 2013; it is called “R7RS” (Revised7 Report on the Algorithmic Language Scheme).

In 1994, the Rice University Programming Languages Team released a version of Scheme called PLT Scheme. Subsequently, this evolved away from standard Scheme; it is now known as Racket. Racket is distributed with a simple IDE called DrRacket, which supports Racket, standard Scheme, and other similar programming languages.

Characteristics

Scheme is a programming language in the Lisp family. It was designed according to a minimalist philosophy. As a result, Scheme is a rather smaller and simpler than some other programming languages in the Lisp family.

Like most other Lisp-family programming languages, Scheme code consists of parenthesized lists whose items are separated by space. These lists may contain other lists, or atoms, which are not lists.

[Scheme]

(define (hello-world)
  (begin
    (display "Hello, world!")
    (newline)
    )
  )

Above, define is a symbol. So are hello-world, begin, display, and newline. However, "Hello, world!" is not a symbol, but a string literal. All of these are atoms.

Scheme is generally considered a functional programming language, but, unlike Haskell, Scheme is not pure: it allows for side effects and mutable data. However, quite a lot can be done with Scheme without making use of side effects. In this article I avoid side effects, except when doing I/O.

Scheme typing is dynamic and implicit. Scheme has a fixed set of types—something like 36 of them. New types cannot be created. By default, function parameters are checked via duck typing. So Scheme’s type system is similar to that of Lua, but a bit more complicated.

Scheme is extensible: we can define new flow-of-control contructs, as well as new ways of defining things. And ours have equal status with the standard ones.

Execution & Conventions Used in This Article

As with many modern programming languages, Scheme can be used interactively, and Scheme programs can be stored in source files. The filenames of Scheme source files traditionally end with the suffix “.scm”.

Examples in this article assume the use of DrRacket. However, standard Scheme is covered, not the Racket programming language.

To make a file executable in DrRacket, its programming language must be specified at the beginning of the file, as follows.

[DrRacket Convention]

#lang scheme

Note that the above is not standard Scheme.

Source files can be loaded into DrRacket using a standard menu interface. The file can be editing and saved. Hitting the “Run” button at the top of the DrRacket window loads the file into Scheme and executes it. If execution hangs, hit the “Stop” button. If and when the code successfully completes execution, the bottom half of the DrRacket window becomes an interactive Scheme environment in which all global symbols defined in the source file are available for use.

In the interactive Scheme examples in this document, boldface typewriter-font characters are those typed by the user. Brownish non-bold typewriter-font characters show Scheme output. As you read this introduction, I recommend that you follow along with DrRacket.

2. Expressions

If an expression is typed in the Scheme interactive environment, then the expression is evaluated, and the result is printed. Thus, the environment is known as a Read-Eval-Print Loop (REPL).

[Interactive Scheme]

> 37
37
> -400
-400

However, using infix operators in the usual manner will not produce the expected results

[Interactive Scheme]

> 3 + 4
3
#<procedure:+>
4

A Scheme expression that consists of more than just an atom is always a list. The first item in the list is whatever operator or function is being applied. The rest of the items are its operands/arguments.

[Interactive Scheme]

> (+ 3 4)
7

Some operators can take more than two arguments.

[Interactive Scheme]

> (+ 1 2 3 4)
10

Operands can be other expressions.

[Interactive Scheme]

> (+ (* 5 7) 6 (* -1 3))
38

Floating-point numbers, exact rational numbers, and complex numbers are also available.

[Interactive Scheme]

> (+ 3.2 4)
7.2
> (+ 1/3 1/2)
5/6
> (+ 7-2i 3+4i)
10+2i

Numeric comparison operators in Scheme are mostly what we expect, except that numeric equality is “=”, and there does not seem to be an inequality operator. Boolean literals are #t and #f.

[Interactive Scheme]

> (>= 3 4)
#f

Logical operators are and, or, and not.

[Interactive Scheme]

> (and (> 5 2) (not (= 3 4)))
#t

For a list that forms an expression, the normal evaluation rule is as follows.

3. Defining

To bind a symbol to a value, use define. This takes a symbol and an expression. The expression is evaluated, and the symbol is bound to the result. The symbol can then be used in expressions.

[Interactive Scheme]

> (define x 3)
> x
3
> (+ x 10)
13
> (define abc (+ x 100))
> abc
103

We can also use define to create a procedure. Once again, define takes two arguments. The first is a picture of a call to our procedure, which may have arguments. The second is an expression. However, the expression is not evaluated yet. When the procedure we create is later called, the parameters, if any, are given their values, and then the expression is evaluated.

[Interactive Scheme]

> (define (square x) (* x x))

Above, “(square x)” is what I am calling a picture of a procedure call. square is the name of the procedure, and x is its parameter.

We call a defined procedure as usual.

[Interactive Scheme]

> (square 5)
25

4. Source Files

Again, the standard filename suffix for a Scheme source file is “.scm”.

A Scheme source file will consists mostly of definitions of symbols.

[Scheme]

#lang scheme
; squarecube.scm

; square
; Returns the square of its argument.
(define (square x) (* x x))

; cube
; Returns the cube of its argument.
(define (cube x) (* x x x))

A single-line comment in Scheme begins with semicolon (;) and goes to end-of-line. Multiline comments begin with “#|” and end with “|#

Interestingly, Scheme has a third kind of comment. To comment out a single expression, use “#;” followed by the expression. This kind of comment does not treat line breaks in any special way.

[Scheme]

#; (this is
commented out) (this is not commented
out)

As stated above, once code is entered in the editor, it can be executed by clicking the “Run” button at the top of the DrRacket window.

5. Selection & Recursion

if takes three expressions. The first expression is evaluated. If the result is anything other than #f (for example, #t), then the second expression is evaluated, and the result is returned. If the result is #f, then the third expression is evaluated and the result is returned. Typically, the first expression evaluates to a boolean, but in case it does not, note that anything other than #f counts as truthy.

[Interactive Scheme]

> (if (= 3 3) 42 6)
42

To make a recursive call in Scheme, a procedure simply calls itself. Putting all this together, here is a recursive factorial.

[Scheme]

(define (fact n)
  (if (= n 0)
      1
      (* n (fact (- n 1)))
      )
  )

[Interactive Scheme]

> (fact 5)
120

6. Local Variables

Local variables can be defined in Scheme using a let construction, which is similar to the let construction in Haskell.

Here is Haskell code for a recursive factorial function that uses a local varible. First we do it with where.

[Haskell]

fact' n = if n == 0
          then 1
          else n * fn1  where
              fn1 = fact' (n-1)

Next we do it with let.

[Haskell]

fact'' n = if n == 0
           then 1
           else let fn1 = fact'' (n-1)
                in n * fn1

Lastly, here is much the same thing in Scheme.

[Scheme]

(define (fact2 n)
  (if (= n 0)
      1
      (let (
            (fn1 (fact2 (- n 1)))
            )
        (* n fn1)
        )
      )
  )

In Scheme, let takes two arguments. The first is a list of 2-item lists. Each of the 2-item lists holds a symbol and an expression. The expression is evaluated and the symbol is bound to the result. The second argument of let is an expression. This expression is evaluated with each of the symbols bound as indicated. The result is returned.

So, above, if n is not zero, the fn1 is bound to the value of (fact2 (- n 1)), the expression (* n fn1) is evaluated, and the result is returned.

With so many parentheses, it can be difficult to see where they match up. For this reason, Scheme allows brackets to be used as well. But a bracket cannot match with a parenthesis. So “[(abc)]” is good, but “([abc)]” is not.

It is traditional to put each of the 2-item lists in a let in brackets. Here is the above code rewritten accordingly.

[Scheme]

(define (fact2 n)
  (if (= n 0)
      1
      (let (
            [fn1 (fact2 (- n 1))]
            )
        (* n fn1)
        )
      )
  )

7. Lists & Quoting

The syntax we have been using to represent code can also represent data. For example, the procedure list takes an arbitrary number of expressions. The expressions are evaluated, and a list of all the results is returned.

[Interactive Scheme]

> (list (+ 1 2) (* 3 4) 5)
(3 12 5)

Simply entering a list—e.g., (3 12 5)—at the scheme prompt is a problem, since things we enter are evaluated, and 3 is not a procedure. We can deal with this using the procedure list. Another way to deal with it is to precede the list with a single quote ('), which supresses evaluation.

[Interactive Scheme]

> (3 12 5)
application: not a procedure ...
> (list 3 12 5)
(3 12 5)
> '(3 12 5)
(3 12 5)

What follows the single quote is said to be quoted.

Note that list evaluates its arguments, while quoting suppresses all evaluation.

[Interactive Scheme]

> (list (+ 1 2) 5)
(3 5)
> '((+ 1 2) 5)
((+ 1 2) 5)

Lists are first-class. We can set a variable equal to a list.

[Interactive Scheme]

> (define t1 (list (+ 1 2) 5))
> t1
(3 5)
> (define t2 '((+ 1 2) 5))
> t2
((+ 1 2) 5)

Scheme has a number of procedures that allow us to construct lists and taken them apart.

car takes a nonempty list and returns its first item.

[Interactive Scheme]

> (car '(11 22 33 44 55))
11

Similarly, cdr takes a nonempty list and returns a list containing all items except the first.

[Interactive Scheme]

> (cdr '(11 22 33 44 55))
(22 33 44 55)

We can do fancier things by putting car and cdr together. For example, for a list with at least 2 items, the car of the cdr will be the second item.

[Interactive Scheme]

> (car (cdr '(11 22 33 44 55)))
22

By the way, the odd names car and cdr come from an implementation detail of an early version of the Lisp programming language. They originally stood for “Contents of the Address Register” and “Contents of the Data Register”, respectively.

cons takes an item and a list. It returns a list with the item prepended to the given list—like the colon operator in Haskell.

[Interactive Scheme]

> (cons 11 '(22 33 44 55))
(11 22 33 44 55)
> (cons 10 (cons 20 (cons 30 '())))
(10 20 30)

8. First-Class Code

As you might have guessed, Scheme has first-class functions (which it calls procedures). But Scheme goes further than that; it has first-class code. The great strength of Scheme—and other Lisp-family programming languages—is that code can be stored in variables, passed around, modified at runtime, and executed.

To evaluate a list as an expression, pass it to eval.

[Interactive Scheme]

> (define w '(+ 5 (* 2 3)))
> w
(+ 5 (* 2 3))
> (eval w)
11

Here is a procedure that takes a list, adds “+” on the beginning, evaluates, and returns the retult.

[Scheme]

(define (add-plus-and-eval xs)
  (let (
        [pxs (cons '+ xs)]
        )
    (eval pxs)
    )
  )

Now let’s apply the above to the list ((* 2 3) (* 3 4)). This will construct the list (+ (* 2 3) (* 3 4)) which evaluates to \((2 \times 3) + (3 \times 4) = 18\).

[Interactive Scheme]

> (define y '((* 2 3) (* 3 4)))
> y
((* 2 3) (* 3 4))
> (add-plus-and-eval y)
18

Manipulating code at runtime in Scheme is not nearly as cumbersome as the above might suggest. Scheme allows for what are called macros. These form a convenient way to specify code transformations to be performed at runtime.

9. Topics Not Covered

This has only been a brief introduction. There is more to say about Scheme. Below are a few topics that were not covered here.

10. Copyright & License

© 2021–2025 Glenn G. Chappell

Creative Commons License
A Quick Introduction to Scheme is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.