Introductory R7RS Scheme

Lesson 1

Move from reading expressions to writing small procedures whose behavior can be checked deliberately.

Goal

After this lesson you should be able to read a small R7RS file, define constants and procedures, compose procedure calls, use if and cond, recognize a simple recursive procedure, read basic pair/list operations, and write checks that confirm what your code does.

The examples are still small on purpose. The point is to establish habits that scale: name useful ideas, compose procedures, keep inputs and outputs clear, and test what you claim.

R7RS Program Shape

The browser examples on this page are convenient for experiments, but the checked source file is an R7RS program. A small R7RS program begins with imports:

(import (scheme base)
        (scheme write)
        (scheme process-context))

(scheme base) supplies the core language bindings used here: procedure calls, definitions, predicates, arithmetic, conditionals, pairs, lists, recursion, and common list procedures. (scheme write) gives output procedures such as display and write. (scheme process-context) gives exit, which the test program uses to report success or failure to the shell.

Functional Programming, in the Small

Scheme can be used in several styles. This course begins with functional programming: write procedures that compute values from arguments, then combine those procedures into larger expressions.

For the early lessons, a procedure should usually behave like a mathematical function: the same arguments produce the same result. That makes examples easier to test and easier to reason about.

(define (square-number n)
  (* n n))

(square-number 5)

A procedure is not useful only for one particular input. The point of the parameter n is that square-number works for many numbers.


The call (square-number (+ 2 3)) composes two computations. The argument expression (+ 2 3) produces 5; then square-number uses that value.

Formal Parameters and Actual Arguments

A procedure definition introduces formal parameters. These are the names used inside the procedure body.

(define (rectangle-area width height)
  (* width height))

In the call below, 7 and 6 are actual argument expressions. Their values become the values of width and height while the body runs.

(rectangle-area 7 6)

Keep the distinction clear:

This matters when arguments are themselves compound expressions:


Constants and Small Formulas

A definition can name a value as well as a procedure.

(define unit-square-side 1)
(define default-width 7)

Use names when they make the expression easier to read or when the same value has a role in several places.

Small formula procedures are good early practice:

(define (rectangle-perimeter width height)
  (* 2 (+ width height)))

(define (triangle-area base height)
  (/ (* base height) 2))

Notice that the browser reports the value of the last expression in the block. The perimeter expression is evaluated, then its value is not shown because the final expression is the triangle-area call.

if Is a Special Form

if chooses between two expressions:

(if (> 7 3)
    'larger
    'smaller)

The result is the symbol larger.

if is not an ordinary procedure call. It evaluates the test first, then evaluates only the selected branch. That matters:

(define (safe-reciprocal n)
  (if (= n 0)
      'undefined
      (/ 1 n)))

If both branches were evaluated every time, (safe-reciprocal 0) would still try to divide by zero. It does not, because if has its own evaluation rule.

cond for Several Cases

For more than two cases, cond is usually clearer than nested if forms.

(define (number-kind n)
  (cond
    ((< n 0) 'negative)
    ((= n 0) 'zero)
    ((even? n) 'positive-even)
    (else 'positive-odd)))

Each cond clause has a test and a result. The outer form starts with cond, so the clause parentheses are not procedure calls. This is one of the early exceptions to the simple rule "parentheses mean a call." More precisely: most parenthesized expressions are calls, but special forms have their own syntax.

A small game-like predicate can use cond too:

(define (buzz-basic n)
  (cond
    ((= (remainder n 7) 0) 'buzz)
    ((= (remainder n 10) 7) 'buzz)
    (else n)))

This version says buzz for multiples of seven and for numbers whose last decimal digit is seven. A later exercise can generalize it to search all digits.


A First Look at Recursion

Recursion means a procedure calls itself. Lesson 0 only named the idea. Here we can read two small recursive procedures, but the full method for designing recursive programs comes later.

The first example is factorial. A naive definition can be dangerous: if the only base case is (= n 0), a negative input keeps moving farther from 0 and recurses forever. Guard invalid inputs before the recursive case.

(define (factorial n)
  (cond
    ((or (not (integer? n)) (< n 0))
     (error "factorial: expected a nonnegative integer" n))
    ((<= n 1) 1)
    (else (* n (factorial (- n 1))))))

There are three parts to notice:

Using (<= n 1) handles both ordinary base cases directly. It also avoids one unnecessary recursive call for (factorial 1), which is small here but is the right habit: do not make examples slower or less safe than they need to be.


Euclid's algorithm for greatest common divisor has the same shape: a base case and a smaller recursive problem.

(define (euclid-gcd a b)
  (if (= b 0)
      a
      (euclid-gcd b (remainder a b))))

Do not try to learn all recursion from these examples at once. For now, read the shape: base case first, recursive call on a simpler input second.

A Note on Evaluation Order

Scheme evaluates procedure arguments before calling the procedure. This is why (square-number (+ 2 3)) computes the argument value 5 and then calls square-number with 5.

There are deeper questions about evaluation order, delayed evaluation, and what happens when an expression is not a pure function of its arguments. Those belong in a later evaluation-model lesson. For now, the practical rule is enough: write small procedures whose results depend on their arguments, and use tests to check that they do.

A First Look at Pairs and Proper Lists

Lesson 0 introduced quoted lists as data:

'(scheme r7rs lists)

A non-empty Scheme list is built from pairs. A pair is a two-part value. The procedure cons constructs a pair, car selects its first part, and cdr selects its second part.


The names car and cdr are historical. For now, read them as the two basic selectors for a pair: first part and second part.

A proper list is a chain of pairs whose final cdr is the empty list, written '().


The same proper list can be written directly with quote:


For a non-empty proper list, car gives the first element and cdr gives the rest of the list. The rest of the list is itself a list. null? recognizes the empty list.

The procedure list is a convenience constructor for proper lists. It evaluates its arguments, then returns a proper list containing their values.


This is different from a quoted list. In '(1 (+ 1 2) scheme), the (+ 1 2) is data and is not evaluated. In (list 1 (+ 1 2) 'scheme), the addition is evaluated before list receives its arguments.


An improper list is a pair chain that does not end in '(). One direct example is:


Many Scheme systems print that value as (left . right). It is a pair, but not a proper list.


Improper lists are not mistakes by definition. They are pairs being used outside the proper-list convention. Pairs can also represent small custom linked structures:


Here the pair means "x coordinate and y coordinate," not "first element and rest of a proper list." Later lessons will elaborate pairs, proper lists, improper lists, association lists, records, and when custom pair-linked structures are a good idea.

A Minimal Test Habit

A lesson example should not only show definitions. It should also check them. Here is the tiny checking pattern used in the current example file:

(define failures '())

(define (record-failure label expected actual)
  (set! failures (cons (list label expected actual) failures)))

(define (check-equal label expected actual)
  (unless (equal? expected actual)
    (record-failure label expected actual)))

This is not a full testing library. It is just enough to make examples executable. When a check fails, the label, expected value, and actual value are recorded.

A check looks like this:

(check-equal 'procedure-call 81 (square-number 9))

The quoted symbol procedure-call names the check. It is data, not a variable lookup.

First Complete Example

The published example source for this lesson checks arithmetic, composition, constants, formulas, conditionals, cond, simple recursion, pairs, list selectors, one higher-order teaser, and map. The beginning looks like this:

(import (scheme base)
        (scheme write)
        (scheme process-context))

(define (square-number n)
  (* n n))

(define (rectangle-area width height)
  (* width height))

(check-equal 'procedure-call 81 (square-number 9))
(check-equal 'composed-argument 25 (square-number (+ 2 3)))
(check-equal 'rectangle-area 42 (rectangle-area 7 6))

The full source file has a highlighted view at /examples/r7rs/intro/lesson-01/ and a raw source link from that page. It is currently tested with CHICKEN 5.3.0 and the CHICKEN r7rs egg.

When working from the source tree used to build this site, run the example suite with:

scripts/test_examples_chicken.sh

The expected output includes:

Intro lesson 00 tests passed
Intro lesson 01 tests passed
Intro lesson 02 tests passed
LCG tests passed

Exercises

These exercises are original and deliberately small. They are meant to check whether the basic expression, definition, and testing model is clear.

What Comes Next

The next lesson is Lesson 2: higher-order procedures. It takes the small procedure-writing habits from this lesson and uses them to describe reusable patterns: procedures passed as arguments, procedures returned as results, anonymous procedures, and local names.

Before moving on, make sure you can explain these ideas without guessing: