A Beginner's Guide to Scheme
Scheme is one of the most elegant programming languages ever designed. Created in the 1970s at MIT by Guy Steele and Gerald Sussman, it strips programming down to its essential elements: expressions, definitions, and procedures. While Scheme isn't widely used in industry today, learning it teaches you to think about computation in a fundamentally different way.
This guide will take you from zero to writing your first Scheme programs.
Why Learn Scheme?
Before diving in, let's address the obvious question: why learn a language that most professional programmers never use?
Simplicity reveals fundamentals. Scheme's syntax is so minimal that you can learn nearly the entire language in an afternoon. This simplicity lets you focus on what you're computing rather than fighting with syntax. As David Evans notes in Introduction to Computing:
The primary advantage of using Scheme to learn about computing is its simplicity and elegance. The language is simple enough that this chapter covers nearly the entire language.
It changes how you think. Scheme forces you to think recursively and functionally. These patterns appear everywhere in modern programming—from JavaScript's array methods to React's component model to data processing pipelines.
Historical significance. Many "modern" features—garbage collection, first-class functions, closures—were invented in Lisp/Scheme decades ago. Understanding Scheme helps you appreciate where programming languages came from.
Getting Started with DrRacket
The easiest way to start programming in Scheme is with DrRacket, a free IDE designed for learning. DrRacket provides:
- Syntax highlighting and error messages designed for beginners
- Multiple "language levels" for gradual learning
- An interactive REPL (Read-Eval-Print Loop) for experimentation
Installation
- Download DrRacket from racket-lang.org
- Install and open the application
- Select a language: Language → Choose Language → Teaching Languages → Beginning Student
For the examples in this guide, you can also select Other Languages → R5RS for standard Scheme, or simply use #lang scheme at the top of your file.
Note: If you are using the Beginning Student language level in DrRacket, you do not need to include
#lang schemeat the top of your file. That directive is for when you are running standard Racket/Scheme scripts.
DrRacket's Two Panels
DrRacket has two main areas:
- Definitions Window (top): Where you write your program
- Interactions Window (bottom): Where you can type expressions and see results immediately
Click the Run button (or press Ctrl+R / Cmd+R) to execute your definitions, then experiment in the interactions window.
Expressions: The Building Blocks
Everything in Scheme is an expression—a piece of code that evaluates to a value.
Primitive Expressions
The simplest expressions are primitives that evaluate to themselves:
| Primitive Expressions | |
|---|---|
Application Expressions
To do anything useful, you apply procedures (functions) to arguments. Scheme uses prefix notation—the procedure comes first, followed by its arguments, all wrapped in parentheses:
| Application Expressions (Prefix Notation) | |
|---|---|
Key insight: Every application follows the same pattern: (procedure arg1 arg2 ...). There are no special cases for operators vs. functions—they all work the same way.
Nested Expressions
Expressions can be nested to build complex computations:
| Nested Expressions | |
|---|---|
The innermost expressions evaluate first, then their results flow outward. This is called post-order evaluation.
graph TB
subgraph "Evaluating (+ (* 10 10) (+ 25 25))"
A["(+ (* 10 10) (+ 25 25))"] --> B["(* 10 10)"]
A --> C["(+ 25 25)"]
B --> D["10"]
B --> E["10"]
C --> F["25"]
C --> G["25"]
D --> H["100"]
E --> H
F --> I["50"]
G --> I
H --> J["150"]
I --> J
end
style A fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style B fill:#4a5568,stroke:#cbd5e0,stroke-width:2px,color:#fff
style C fill:#4a5568,stroke:#cbd5e0,stroke-width:2px,color:#fff
style D fill:#1a202c,stroke:#cbd5e0,stroke-width:2px,color:#fff
style E fill:#1a202c,stroke:#cbd5e0,stroke-width:2px,color:#fff
style F fill:#1a202c,stroke:#cbd5e0,stroke-width:2px,color:#fff
style G fill:#1a202c,stroke:#cbd5e0,stroke-width:2px,color:#fff
style H fill:#48bb78,stroke:#cbd5e0,stroke-width:2px,color:#fff
style I fill:#48bb78,stroke:#cbd5e0,stroke-width:2px,color:#fff
style J fill:#48bb78,stroke:#cbd5e0,stroke-width:2px,color:#fff
The leaves (primitives) evaluate first, then results propagate up to the root.
Prefix vs. Infix Notation
Most languages use infix notation where operators go between operands: 3 + 4 * 5. This requires precedence rules (PEMDAS) to determine order.
Scheme's prefix notation eliminates ambiguity. The expression (+ 3 (* 4 5)) explicitly shows that multiplication happens first. No memorization required.
For a deeper dive into why this matters, see Scheme & Parse Trees.
Built-in Procedures
Scheme provides many primitive procedures. Here are the essentials:
| Procedure | Description | Example | Result |
|---|---|---|---|
+ |
Add numbers | (+ 1 2 3) |
6 |
- |
Subtract | (- 10 3) |
7 |
* |
Multiply | (* 2 3 4) |
24 |
/ |
Divide | (/ 15 3) |
5 |
= |
Equal? | (= 5 5) |
#t |
< |
Less than? | (< 3 5) |
#t |
> |
Greater than? | (> 3 5) |
#f |
<= |
Less or equal? | (<= 5 5) |
#t |
>= |
Greater or equal? | (>= 3 5) |
#f |
zero? |
Is zero? | (zero? 0) |
#t |
Notice that + and * can take any number of arguments—another advantage of prefix notation.
Definitions: Naming Things
Computation becomes useful when you can name values and reuse them. The define form creates a name and binds it to a value:
| Defining Constants | |
|---|---|
Once defined, you can use names in expressions:
| Using Definitions in Expressions | |
|---|---|
Naming Conventions
Scheme names can include letters, digits, and special characters like -, ?, and !:
| Naming Convention Examples | |
|---|---|
Good names are:
- Descriptive: cups-per-day not cpd
- Hyphenated: cost-per-cup not costPerCup
- Question-marked for predicates: zero?, even?, is-valid?
Procedures: Defining Your Own Functions
The real power comes from defining your own procedures. Scheme uses lambda (λ) to create procedures:
| Anonymous Procedure (Lambda) | |
|---|---|
This creates an anonymous procedure—it exists but has no name. To use it, you can apply it directly:
| Applying a Lambda Directly | |
|---|---|
But usually, you'll bind procedures to names with define:
| Binding Procedures to Names | |
|---|---|
Shorthand Definition Syntax
Defining named procedures is so common that Scheme provides a shorthand:
| Shorthand vs. Lambda Syntax | |
|---|---|
The shorthand moves the parameter list next to the name. Most Scheme code uses this form.
Multiple Parameters
Procedures can take multiple inputs:
| Procedure with Multiple Parameters | |
|---|---|
Practical Examples
Here are some useful procedures:
Conditionals: Making Decisions
Real programs need to make decisions. The if expression chooses between two values based on a condition:
| If Expression Syntax | |
|---|---|
For example:
| Conditional Examples | |
|---|---|
The cheaper Procedure
Let's build a procedure that returns the lower of two prices:
| The cheaper Procedure | |
|---|---|
How it works:
(< price1 price2)compares the two prices- If
price1is less, returnprice1 - Otherwise, return
price2
Important: if is a Special Form
Unlike normal procedures, if doesn't evaluate all its arguments. Only the selected branch is evaluated:
This is called short-circuit evaluation—crucial for avoiding unnecessary computation or errors.
This pattern is ubiquitous across computing. In most programming languages, && and || operators short-circuit: false && expensive() never calls expensive(). You'll find the same behavior in the UNIX shell:
| Short-Circuit Evaluation in Bash | |
|---|---|
The shell's && only runs the second command if the first succeeds; || only runs the second if the first fails. Same principle, different syntax.
Nested Conditionals
You can nest if expressions for multiple conditions:
| Nested Conditionals | |
|---|---|
The cond Alternative
For multiple conditions, Scheme also provides cond:
| Using cond for Multiple Conditions | |
|---|---|
This is often clearer than nested if expressions.
Data Structures: Lists
Scheme stands for LISP (LISt Processing), so it's no surprise that lists are its fundamental data structure. A list is an ordered sequence of elements.
Creating Lists
You can create a list using the list procedure:
| Creating Lists | |
|---|---|
You can also construct lists using cons (construct). cons adds an element to the front of a list:
empty (or '()) represents an empty list.
Accessing List Elements
Scheme provides two primary procedures for taking lists apart:
first(orcar): Returns the first element of the list.rest(orcdr): Returns the list containing everything except the first element.
| Accessing Elements | |
|---|---|
Historical Note:
carstands for "Contents of the Address part of Register" andcdr(pronounced "could-er") stands for "Contents of the Decrement part of Register". These names refer to the architecture of the IBM 704 computer where Lisp was first implemented! Most modern Scheme teaching languages preferfirstandrest.
Recursion: Where are the Loops?
You might have noticed that this guide hasn't mentioned for or while loops. That's because Scheme doesn't have them! In Scheme, we use recursion—procedures that call themselves—to handle repetition.
For example, if you wanted to sum a list of numbers, you wouldn't use a loop. You would say: "The sum is the first number plus the sum of the rest of the numbers."
| Recursion Example: Summing a List | |
|---|---|
While it might feel strange at first, recursion is a more powerful and flexible way to express computation.
Higher-Order Procedures
One of Scheme's most powerful features is that procedures are first-class values. They can be:
- Passed as arguments to other procedures
- Returned as results from procedures
- Stored in data structures
Procedures That Return Procedures
Here's a procedure that creates other procedures:
- Outer lambda takes
percentand returns an inner lambda - Inner lambda "closes over"
percent, remembering its value
What's happening:
(make-tipper 20)returns(lambda (bill) (* bill (/ 20 100)))- This new procedure "remembers" that
percentis20 - When called with
50, it computes(* 50 0.20)→10
This "memory" is called a closure—the procedure captures the environment where it was created.
flowchart TB
subgraph CREATE ["1. Create the closure"]
A["(make-tipper 20)"]
B["Returns a lambda"]
C["Captures: percent = 20"]
A --> B --> C
end
subgraph STORE ["2. Bind to a name"]
D["tip-20"]
E["Has code + captured environment"]
D --> E
end
subgraph CALL ["3. Call the closure"]
F["(tip-20 50)"]
G["bill = 50, percent = 20"]
H["Result: 10"]
F --> G --> H
end
C --> D
E --> F
style A fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style B fill:#4a5568,stroke:#cbd5e0,stroke-width:2px,color:#fff
style C fill:#48bb78,stroke:#cbd5e0,stroke-width:2px,color:#fff
style D fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style E fill:#4a5568,stroke:#cbd5e0,stroke-width:2px,color:#fff
style F fill:#2d3748,stroke:#cbd5e0,stroke-width:2px,color:#fff
style G fill:#4a5568,stroke:#cbd5e0,stroke-width:2px,color:#fff
style H fill:#48bb78,stroke:#cbd5e0,stroke-width:2px,color:#fff
Procedures as Arguments
You can pass procedures to other procedures:
| Passing Procedures as Arguments | |
|---|---|
fis a procedure passed as an argument- Apply
ftox, then applyfto that result
This is the foundation of functional programming patterns like map, filter, and reduce.
Putting It All Together
Let's build a complete example that uses everything we've learned:
Practice Exercises
Exercise 1: Cube Procedure
Define a procedure cube that takes one number and returns its cube.
Solution
| Solution | |
|---|---|
Exercise 2: Absolute Value
Define a procedure abs-value that returns the absolute value of a number.
| Expected Behavior | |
|---|---|
Exercise 3: Bigger Magnitude
Define bigger-magnitude that returns whichever input has the greater absolute value.
Hint
You can use the abs-value procedure you defined in Exercise 2 to help with the comparison!
| Expected Behavior | |
|---|---|
Exercise 4: Seconds in a Year
Write a Scheme expression to calculate the number of seconds in a year. Define meaningful names for intermediate values.
Exercise 5: Make Multiplier
Define a higher-order procedure make-multiplier that takes a number and returns a procedure that multiplies its input by that number.
| Expected Behavior | |
|---|---|
Key Takeaways
| Concept | Syntax | Example |
|---|---|---|
| Primitive | value |
42, #t, #f, "hello" |
| Application | (proc args...) |
(+ 1 2 3) |
| Definition | (define name value) |
(define pi 3.14) |
| Procedure | (lambda (params) body) |
(lambda (x) (* x x)) |
| Named Procedure | (define (name params) body) |
(define (square x) (* x x)) |
| Conditional | (if test then else) |
(if (> a b) a b) |
What's Next?
This primer covers the core of Scheme. To go deeper:
- Scheme & Parse Trees — Understand why Scheme's syntax directly mirrors computation structure
- Procedures & Higher-Order Functions — Explore the full power of functions as values
- Recursion — Dive deeper into recursive problem solving.
Further Reading
- Structure and Interpretation of Computer Programs (Abelson & Sussman) — The classic MIT textbook
- Introduction to Computing (David Evans) — A key reference for learning Scheme
- Racket Documentation — Official guide to Racket/Scheme
- Teach Yourself Racket — University of Waterloo tutorial
- CSC 151 Course Materials — Grinnell College's introduction to Scheme
Scheme's power lies in its simplicity. With just expressions, definitions, procedures, and conditionals, you can express any computation. The challenge isn't learning more syntax—it's learning to think in terms of these fundamental building blocks.