Beginner

Control Flow in Dylan

Learn conditionals, case and select statements, loops, and non-local exits in Dylan with practical Docker-ready examples

Control flow is how a program decides what to do and how many times to do it. Dylan gives you a comfortable, ALGOL-flavored toolkit — if/elseif/else, while, until, and for — but it carries a distinctly Lisp sensibility underneath. The most important thing to internalize early is that most of Dylan’s control-flow constructs are expressions: an if doesn’t just branch, it produces a value, so you can bind its result directly to a variable.

As a multi-paradigm language, Dylan blends procedural looping with functional, expression-oriented branching. Where C-family languages have a switch statement, Dylan offers two complementary constructs: case, which evaluates a series of arbitrary boolean tests, and select, which compares a single value against sets of candidates. And one design choice surprises newcomers: Dylan has no break or continue keywords. Instead, it uses non-local exits through the block construct — a more general mechanism that, as you’ll see, can express early exit, iteration skipping, and even resource cleanup.

In this tutorial you’ll branch with if/elseif/else, choose among many possibilities with case and select, iterate with for, while, and until, and learn the idiomatic Dylan way to break out of and skip iterations using block. Remember from earlier tutorials that only #f is false in Dylan — every other value, including 0 and the empty string, is truthy.

Conditionals: if, elseif, and if-as-an-expression

The if construct evaluates a condition and runs one branch. Chain alternatives with elseif (one word in Dylan, not else if), and close the whole form with end if. Because if is an expression, you can also use it inline to produce a value.

Create a file named conditionals.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Module: hello

let temperature = 18;

// if / elseif / else as a statement
if (temperature > 30)
  format-out("It is hot\n");
elseif (temperature >= 15)
  format-out("It is mild\n");
else
  format-out("It is cold\n");
end if;

// if used as an expression: its value is bound to `label`
let label = if (temperature >= 15) "comfortable" else "chilly" end;
format-out("Today is %s\n", label);

// Combining conditions with & (and). Comparison binds tighter than &,
// so this reads as (temperature >= 15) & humid?
let humid? = #t;
if (temperature >= 15 & humid?)
  format-out("Mild and humid\n");
end if;

Two things distinguish Dylan here. First, elseif is a single keyword. Second, if is genuinely an expression — let label = if (...) ... else ... end works because the if evaluates to whichever branch is taken. There is no separate ternary ?: operator in Dylan because if already fills that role.

Choosing among many: case and select

Dylan splits “multi-way branching” into two constructs. Use case when each branch is governed by its own arbitrary boolean test — it evaluates the tests top to bottom and runs the first one that is true. Use select when you are comparing one value against fixed candidates; it’s closer to a classic switch. Both use => to separate a branch’s test from its body, and both accept an otherwise fall-through clause.

Create a file named selection.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Module: hello

// case: first true test wins
let score = 85;
case
  score >= 90 => format-out("Grade: A\n");
  score >= 80 => format-out("Grade: B\n");
  score >= 70 => format-out("Grade: C\n");
  otherwise   => format-out("Grade: F\n");
end case;

// select: compare one value against candidate sets (default test is ==)
let month = 4;
select (month)
  12, 1, 2  => format-out("Season: Winter\n");
  3, 4, 5   => format-out("Season: Spring\n");
  6, 7, 8   => format-out("Season: Summer\n");
  otherwise => format-out("Season: Autumn\n");
end select;

// select on strings needs an explicit equality test: `by \=`
let day = "Sat";
select (day by \=)
  "Sat", "Sun" => format-out("It is the weekend\n");
  otherwise    => format-out("It is a weekday\n");
end select;

Notice the by \= clause in the last select. By default select compares branches with == (identity), which is correct for integers and symbols but not for strings, where two equal strings can be distinct objects. Supplying by \= tells select to use the value-equality function = instead — \= is the prefix, function-style spelling of the = operator.

Loops: for, while, and until

Dylan’s for is remarkably flexible. The simplest form counts over a numeric range with from ... to, optionally stepping by an increment. It can also walk a collection with in. For condition-driven repetition, while loops while a test is true, and until loops until a test becomes true.

Create a file named loops.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Module: hello

// Counting for loop over a numeric range (inclusive)
for (i from 1 to 3)
  format-out("Count: %d\n", i);
end for;

// Stepping by 2
for (i from 0 to 6 by 2)
  format-out("Even: %d\n", i);
end for;

// Iterating over a literal list
for (fruit in #("apple", "banana", "cherry"))
  format-out("Fruit: %s\n", fruit);
end for;

// while: repeat as long as the test is true
let countdown = 3;
while (countdown > 0)
  format-out("T-minus %d\n", countdown);
  countdown := countdown - 1;
end while;

// until: repeat until the test becomes true
let n = 1;
until (n > 3)
  format-out("Step %d\n", n);
  n := n + 1;
end until;

Recall from the operators tutorial that assignment uses :=, never = — so countdown := countdown - 1 mutates the local binding while = stays reserved for equality. The for ... in form is the idiomatic way to traverse any Dylan collection (lists, vectors, ranges, even strings), and a single for header can combine several clauses, which makes it the workhorse of Dylan iteration.

Loop control: break and continue with block

Here is where Dylan diverges sharply from C-family languages: there is no break and no continue. Instead, Dylan provides block, which establishes a named non-local exit. The name you give in block (name) is bound to an exit function — calling it immediately unwinds and leaves the block. Wrapping a whole loop gives you “break”; wrapping a single iteration’s body gives you “continue”.

Create a file named loop_control.dylan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Module: hello

// "break": wrap the loop and exit it early by calling the exit function
block (stop)
  for (i from 1 to 10)
    if (i = 5)
      format-out("Stopping at %d\n", i);
      stop();
    end if;
    format-out("Visiting %d\n", i);
  end for;
end block;

// "continue": wrap each iteration; calling next() skips the rest of the body
for (i from 1 to 6)
  block (next)
    if (even?(i))
      next();
    end if;
    format-out("Odd: %d\n", i);
  end block;
end for;

// Often a guard condition is clearer than an explicit skip
for (i from 1 to 6)
  if (odd?(i))
    format-out("Guarded odd: %d\n", i);
  end if;
end for;

The exit names stop and next are not keywords — they’re ordinary variables bound to exit functions, so you can name them whatever reads best. This single mechanism is more general than break/continue: because the exit function is a first-class value, you could even pass it to another function and trigger the exit from deep inside a call chain. The built-in predicates even? and odd? follow Dylan’s convention of suffixing boolean-returning functions with ?.

Running with Docker

1
2
3
4
5
6
7
8
# Pull the Dylan Docker image
docker pull codearchaeology/dylan:latest

# Run each example
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest dylan conditionals.dylan
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest dylan selection.dylan
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest dylan loops.dylan
docker run --rm -v $(pwd):/app codearchaeology/dylan:latest dylan loop_control.dylan

Expected Output

Running conditionals.dylan:

It is mild
Today is comfortable
Mild and humid

Running selection.dylan:

Grade: B
Season: Spring
It is the weekend

Running loops.dylan:

Count: 1
Count: 2
Count: 3
Even: 0
Even: 2
Even: 4
Even: 6
Fruit: apple
Fruit: banana
Fruit: cherry
T-minus 3
T-minus 2
T-minus 1
Step 1
Step 2
Step 3

Running loop_control.dylan:

Visiting 1
Visiting 2
Visiting 3
Visiting 4
Stopping at 5
Odd: 1
Odd: 3
Odd: 5
Guarded odd: 1
Guarded odd: 3
Guarded odd: 5

Key Concepts

  • if is an expression — it evaluates to the value of whichever branch runs, so you can write let x = if (test) a else b end. Dylan needs no separate ternary operator.
  • elseif is one word — chain alternatives with elseif, and close the form with end if. The condition is any expression, and remember only #f is false.
  • case vs selectcase evaluates a series of independent boolean tests and runs the first true one; select compares a single value against candidate sets. Both support an otherwise clause.
  • select defaults to == — that’s correct for integers and symbols, but use select (value by \=) for strings and other values where you need value equality rather than identity.
  • Three looping formsfor handles numeric ranges (from ... to ... by) and collection traversal (in); while repeats while a test is true; until repeats until a test becomes true.
  • No break or continue — Dylan uses block to create named non-local exits. Wrap a loop to break out of it; wrap an iteration body to skip it. The exit name is an ordinary first-class function value.
  • Assignment is := — loop counters and accumulators are mutated with :=, keeping = free as the equality predicate.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/dylan:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining