Beginner

Control Flow in Smalltalk

Learn control flow in Smalltalk - conditionals, loops, and iteration are all ordinary messages sent to objects and evaluated with blocks

Most languages bake control flow into their grammar: if, while, and for are keywords the compiler treats specially. Smalltalk does something radical and beautiful instead - there are no control-flow keywords at all. Conditionals and loops are ordinary messages sent to ordinary objects, and the “branches” are blocks (closures) passed as arguments.

This is the natural consequence of Smalltalk’s central idea: everything is an object, and everything happens through message passing. When you write age >= 18 ifTrue: [ ... ], you are sending the keyword message ifTrue: to a Boolean object (true or false), handing it a block to evaluate. The Boolean decides whether to run the block. The same pattern - send a message, pass a block - drives every loop and iterator in the language.

In this tutorial you’ll learn how Smalltalk expresses conditionals, multi-way branching, condition-controlled loops, counted loops, and collection iteration. Because these are all just messages, conditionals can return values (Smalltalk’s answer to the ternary operator), and you can define your own control structures the same way the standard library defines whileTrue:.

Conditionals with Blocks

The fundamental conditional is the keyword message ifTrue:ifFalse:, sent to a Boolean. Each branch is a block - code wrapped in square brackets that is only evaluated if its branch is selected.

Create a file named conditionals.st:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"conditionals.st - branching is a message sent to a Boolean"
| age temperature |

age := 20.

"ifTrue:ifFalse: - the two-way conditional.
 The comparison >= returns a Boolean, which decides which block runs."
age >= 18
    ifTrue: [ 'You are an adult' displayNl ]
    ifFalse: [ 'You are a minor' displayNl ].

"ifTrue: alone - the block runs only when the condition is true"
age > 65 ifTrue: [ 'Senior discount applies' displayNl ].

"ifFalse: alone - the block runs only when the condition is false"
age < 13 ifFalse: [ 'Not a young child' displayNl ].

temperature := 30.

"Combine conditions with and:/or:. The argument is a block so it is
 only evaluated when needed (short-circuit evaluation)."
((temperature > 25) and: [ temperature < 35 ])
    ifTrue: [ 'Pleasant weather' displayNl ].

Notice that and: takes a block as its argument, not a plain expression. This gives Smalltalk short-circuit evaluation: the right-hand block is only evaluated when the left side is true. The messages displayNl print an object followed by a newline (unlike printNl, displayNl shows strings without surrounding quotes).

Conditionals Are Expressions

Because ifTrue:ifFalse: is a message, it returns a value - specifically the value of whichever block it evaluated. This is how Smalltalk does what other languages call the ternary operator (cond ? a : b), and it scales naturally to multi-way branching. Smalltalk has no switch/case keyword; you chain conditionals instead.

Create a file named conditional_values.st:

 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
"conditional_values.st - conditionals return the value of the chosen block"
| score grade number label |

score := 73.

"Smalltalk's ternary: assign the result of ifTrue:ifFalse:"
label := score >= 60
    ifTrue: [ 'pass' ]
    ifFalse: [ 'fail' ].
Transcript show: 'Result: '; show: label; cr.

"Multi-way branching: there is no switch statement, so nest
 conditionals to classify a value into ranges."
grade := score >= 90
    ifTrue: [ 'A' ]
    ifFalse: [ score >= 80
        ifTrue: [ 'B' ]
        ifFalse: [ score >= 70
            ifTrue: [ 'C' ]
            ifFalse: [ 'F' ] ] ].
Transcript show: 'Grade: '; show: grade; cr.

"Booleans come from any predicate message, such as odd"
number := 7.
label := number odd ifTrue: [ 'odd' ] ifFalse: [ 'even' ].
Transcript show: number printString; show: ' is '; show: label; cr.

Here Transcript show: expects a string, so numbers are converted explicitly with printString. The nested ifTrue:ifFalse: reads top to bottom as a cascade of range checks - the Smalltalk idiom for the else if ladder you’d write elsewhere.

Condition-Controlled Loops

A while loop in Smalltalk is the message whileTrue: sent to a block. The receiver block computes the condition, and the argument block is the loop body. Smalltalk repeatedly evaluates the receiver; as long as it answers true, the body runs again.

Create a file named while_loops.st:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
"while_loops.st - condition-controlled iteration via whileTrue:/whileFalse:"
| count factorial n |

"whileTrue: - the receiver block is the condition, the argument is the body"
count := 1.
[ count <= 5 ] whileTrue: [
    Transcript show: 'Count: '; show: count printString; cr.
    count := count + 1 ].

"whileFalse: - loops as long as the condition block answers false"
n := 10.
[ n <= 0 ] whileFalse: [
    n := n - 3 ].
Transcript show: 'n ended at: '; show: n printString; cr.

"A classic accumulator loop: compute 5 factorial"
factorial := 1.
count := 1.
[ count <= 5 ] whileTrue: [
    factorial := factorial * count.
    count := count + 1 ].
Transcript show: '5! = '; show: factorial printString; cr.

The square brackets around the condition are essential: [ count <= 5 ] is a block that re-evaluates each pass. If you wrote count <= 5 without brackets, it would be computed only once - a fixed true or false - and the loop would never re-check. The whileFalse: variant subtracts 3 from 10 until the value drops to or below zero, landing on -2.

Counted and Stepping Loops

For counting loops, Smalltalk sends iteration messages to numbers. timesRepeat: repeats a block a fixed number of times, while to:do: and to:by:do: walk a numeric range, binding the current value to a block parameter (the :i part).

Create a file named counted_loops.st:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
"counted_loops.st - counting loops are messages sent to numbers"
| sum |

"timesRepeat: - repeat a block N times, no loop variable needed"
3 timesRepeat: [ 'Knock' displayNl ].

"to:do: - iterate an inclusive range, binding each value to :i"
1 to: 5 do: [ :i |
    Transcript show: 'Square of '; show: i printString;
        show: ' is '; show: (i * i) printString; cr ].

"to:by:do: - step by a custom increment"
Transcript show: 'Even numbers:'.
0 to: 10 by: 2 do: [ :i |
    Transcript show: ' '; show: i printString ].
Transcript cr.

"A negative step counts downward"
sum := 0.
10 to: 1 by: -1 do: [ :i | sum := sum + i ].
Transcript show: 'Sum 10 down to 1: '; show: sum printString; cr.

The block parameter :i is declared between the brackets and a vertical bar, then used in the body. This is the same block syntax you’ve seen everywhere - to:do: simply evaluates your block once per number in the range, passing the current value each time.

Iterating Collections and Exiting Early

Collections are iterated with messages too, and this is where Smalltalk’s approach shines. Rather than writing index-based loops with break and continue (Smalltalk has neither keyword), you choose the message that expresses your intent: do: to visit every element, select:/reject: to filter, and detect:ifNone: to find the first match and stop - the natural replacement for a “break when found” loop.

Create a file named collection_control.st:

 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
"collection_control.st - iteration messages replace manual loops"
| numbers firstEven names |

numbers := #(3 7 2 8 5 10).

"do: visits every element - the workhorse iterator"
Transcript show: 'All numbers:'.
numbers do: [ :each | Transcript show: ' '; show: each printString ].
Transcript cr.

"detect:ifNone: returns the first matching element and stops searching -
 the idiomatic way to 'break' out as soon as you find what you want"
firstEven := numbers detect: [ :each | each even ] ifNone: [ nil ].
Transcript show: 'First even number: '; show: firstEven printString; cr.

"select: keeps elements that satisfy the block (filter)"
Transcript show: 'Evens: '; show: (numbers select: [ :each | each even ]) printString; cr.

"reject: is the inverse of select:"
Transcript show: 'Odds:  '; show: (numbers reject: [ :each | each even ]) printString; cr.

"do:separatedBy: runs extra code between elements, never before or after"
names := #('Ada' 'Alan' 'Grace').
names do: [ :n | Transcript show: n ] separatedBy: [ Transcript show: ', ' ].
Transcript cr.

There is no continue either - if you want to skip elements, you wrap the body in a conditional or use select: to filter first. This “pick the right message” style means your code states what you want done to the collection rather than spelling out the mechanics of an index counter.

Running with Docker

The examples use GNU Smalltalk via Docker, so you don’t need to install anything locally.

1
2
3
4
5
6
7
8
9
# Pull the GNU Smalltalk image
docker pull sl4m/gnu-smalltalk:latest

# Run each example
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst conditionals.st
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst conditional_values.st
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst while_loops.st
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst counted_loops.st
docker run --rm -v $(pwd):/app -w /app sl4m/gnu-smalltalk gst collection_control.st

Expected Output

Running conditionals.st:

You are an adult
Not a young child
Pleasant weather

Running conditional_values.st:

Result: pass
Grade: C
7 is odd

Running while_loops.st:

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
n ended at: -2
5! = 120

Running counted_loops.st:

Knock
Knock
Knock
Square of 1 is 1
Square of 2 is 4
Square of 3 is 9
Square of 4 is 16
Square of 5 is 25
Even numbers: 0 2 4 6 8 10
Sum 10 down to 1: 55

Running collection_control.st:

All numbers: 3 7 2 8 5 10
First even number: 2
Evens: (2 8 10 )
Odds:  (3 7 5 )
Ada, Alan, Grace

Key Concepts

  • Control flow is message passing. There are no if, while, or for keywords. ifTrue:ifFalse:, whileTrue:, to:do:, and detect:ifNone: are ordinary messages sent to objects (Booleans, blocks, numbers, collections).
  • Blocks are the branches. Code in [ ... ] is a closure that is only evaluated if its branch is selected. A condition that must re-evaluate (like a loop test) must be wrapped in a block, or it computes only once.
  • Conditionals return values. ifTrue:ifFalse: answers the value of the block it ran, giving Smalltalk a ternary operator for free and letting you assign the result directly.
  • No switch statement. Multi-way branching is expressed by nesting ifTrue:ifFalse: into an else if ladder, or - more idiomatically in large programs - by polymorphism (different objects responding to the same message).
  • and:/or: take blocks for short-circuit evaluation, so the second condition is only checked when necessary.
  • No break or continue. To stop early, choose a message that stops on its own, like detect:ifNone:. To skip elements, filter with select:/reject: or guard the body with a conditional.
  • Pick the message that states intent. do: to visit all, select:/reject: to filter, detect: to find one, timesRepeat: to repeat - the message name documents what the loop does.
  • You can build your own control structures. Because loops and conditionals are just messages plus blocks, defining a new one is the same as defining any other method - a level of extensibility most languages can’t match.

Running Today

All examples can be run using Docker:

docker pull sl4m/gnu-smalltalk:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining