Control Flow in Scheme
Learn control flow in Scheme - conditionals with if, cond, and case, boolean short-circuiting, and how recursion and named let replace traditional loops
Control flow is how a program decides what to do next. In most languages that means statements: if blocks, for loops, switch cases. Scheme takes a different view. Because Scheme is a functional Lisp where everything is an expression that returns a value, its control-flow forms are expressions too - if doesn’t run a block, it evaluates to one of two values.
This expression-oriented design has consequences. There is no separate ternary operator because if already returns a value. The and and or forms aren’t just boolean logic - they’re genuine control-flow tools thanks to short-circuit evaluation. And most importantly, Scheme has no traditional for loop at its core. Iteration is expressed through recursion, made practical by Scheme’s guaranteed proper tail calls, or through the idiomatic named let.
In this tutorial you’ll learn Scheme’s conditional forms (if, cond, case, when, unless), how to use and/or for branching and defaults, and how to loop the Scheme way using recursion, named let, and the do form. Every example below runs unchanged in GNU Guile.
Conditionals: if, cond, case, when, and unless
Scheme’s conditional vocabulary is small but expressive. if is the primitive; cond handles multi-way branching cleanly; case dispatches on a value; and when/unless are one-armed conditionals for side effects.
Create a file named conditionals.scm:
| |
A few things stand out. if always has an else branch here because we want a value in every case. cond reads top to bottom and stops at the first true test. case compares the dispatch value against the keys - note the keys are plain symbols like sat, while the value we pass is the quoted symbol 'sat. Finally, when and unless are perfect when you only care about one outcome and the body is for its effect (printing), not its value.
Boolean Forms as Control Flow
In Scheme, and and or do more than compute true/false. They short-circuit and return actual values, which makes them concise control-flow tools. and returns the last value if every test is truthy (or #f on the first failure); or returns the first truthy value it finds.
Remember from Hello World that in Scheme only #f is false - every other value, including 0 and the empty list, is true.
Create a file named boolean_flow.scm:
| |
safe-divide shows short-circuiting protecting a dangerous operation - (/ a b) simply never runs when b is 0, so and returns #f instead. greet uses or to substitute a default when name is #f. And max-of demonstrates that you never need a ternary operator: if already evaluates to a value you can return directly.
Looping with Recursion and Named let
Scheme has no for loop in its core. Instead, iteration is recursion, and because Scheme guarantees proper tail-call optimization, a tail-recursive procedure runs in constant stack space - exactly like a loop in other languages, with no risk of stack overflow.
The idiomatic shortcut for this pattern is the named let, which gives a loop a name and initial bindings in one form.
Create a file named recursion.scm:
| |
countdown is plain recursion driven by when. sum-to introduces an accumulator - carrying the running total in a parameter is the functional way to avoid mutating a variable. factorial uses a named let: loop is both the loop’s name and the procedure you call to start the next iteration. This is the construct you’ll reach for most often when writing loops in Scheme.
The do Loop and Iterating Collections
When you genuinely want imperative-style iteration, Scheme offers the do form. And to walk through an existing list, for-each applies a procedure to every element.
Create a file named loops.scm:
| |
The do form bundles three things: the loop variables with their step expressions, a stop test paired with a result expression, and an optional body. The first loop just prints. The evens-up-to example shows do doing real work - result accumulates matching numbers each iteration, and when the stop test fires, (reverse result) becomes the loop’s return value. for-each is the cleanest way to traverse a list purely for side effects like printing.
Running with Docker
These examples run on GNU Guile. Use the --no-auto-compile flag so Guile interprets the scripts directly without writing compilation caches.
| |
Expected Output
Running conditionals.scm:
positive
negative
zero
A
B
F
weekend
weekday
4 is even
7 is odd
Running boolean_flow.scm:
5
#f
Hello, Ada
Hello, stranger
9
Running recursion.scm:
5 4 3 2 1
5050
120
Running loops.scm:
1 2 3 4 5
apple
banana
cherry
(0 2 4 6 8 10)
Key Concepts
ifis an expression, not a statement - it returns a value, so Scheme needs no ternary operator; just return theifdirectly.condfor multi-way branching - cleaner than nestedifs, withelseas the catch-all clause.casedispatches on a value usingeqv?, grouping matching keys in each clause - ideal for switch-style logic.whenandunlessare one-armed conditionals for side effects, with an implicitbeginbody.andandorshort-circuit and return values - useandto guard dangerous operations andorto supply defaults.- Only
#fis false -0,"", and'()are all truthy, which affects every conditional you write. - Recursion replaces loops, and guaranteed proper tail calls make tail-recursive procedures as efficient as iteration with no stack growth.
- Named
letis the idiomatic loop - it names the iteration and its state in one form; reach fordoonly when you want explicit imperative stepping.
Running Today
All examples can be run using Docker:
docker pull weinholt/guile:latest
Comments
Loading comments...
Leave a Comment