Chapter 4: Control Flow
What you'll learn: branching with if (as a statement and as an expression) and a first look at match; looping with while, do/while, counted for and for-in over collections and ranges; break, continue and labels; destructuring; and running cleanup code with try/finally, with and del.
if, else if, else
@mode(host.scripted)
let temperature = 72;
if (temperature > 90) {
println("hot");
} else if (temperature > 70) {
println("pleasant");
} else if (temperature > 50) {
println("cool");
} else {
println("cold");
}
Listing 4-1: A chain of conditions
pleasant
Two rules apply to every if: the condition is in parentheses, and it must be a bool (Chapter 2 showed the error for if (count)). The branches are always blocks in braces.
Mistake: leaving out the parentheses
Python, Go, Rust and Swift write conditions without parentheses. Loup does not:
@mode(host.scripted)
let temperature = 72;
if temperature > 90 {
println("hot");
}
main.lpscr:4:4: error[E0001]: E_CONDITION_PARENS: the condition of `if` must be in parentheses; add them: `if (...) {`
The same rule, with the same message, applies to while (…) and to the header of for (…). Add the parentheses: if (temperature > 90) {.
if as an expression
if produces a value when both branches end in an expression. Use it where other languages use the ternary operator ?:, which Loup does not have:
let count = 1;
let noun = if (count == 1) { "file" } else { "files" };
println(i"{$ count $} {$ noun $} changed");
1 file changed
Blocks are scopes
A binding declared inside a block is visible only inside it, and may shadow an outer binding of the same name:
let level = 1;
if (level > 0) {
let level = 2;
let note = "inner";
println(i"{$ note $}: {$ level $}");
}
println(level);
inner: 2
1
Using note after the if would be undefined identifier. Declaring the same name twice in one scope is E_REDECLARED.
Loops
while and do/while
while checks its condition before each pass; do { … } while (…) checks after, so the body runs at least once:
@mode(host.scripted)
var countdown = 3;
while (countdown > 0) {
println(countdown);
countdown = countdown - 1;
}
var attempts = 0;
do {
attempts = attempts + 1;
println(i"attempt {$ attempts $}");
} while (attempts < 2);
3
2
1
attempt 1
attempt 2
for-in over collections and ranges
for (x in items) visits each element of a list, the keys of a dict, or the values of a generator. start..end counts from start up to, but not including, end:
@mode(host.scripted)
let fruits = ["apple", "banana", "cherry"];
for (fruit in fruits) {
println(fruit);
}
for (i in 0..fruits.length()) {
println(i"{$ i + 1 $}. {$ fruits[i] $}");
}
let ages = {"alice": 30, "bob": 25};
for ((name, age) in ages.entries()) {
println(i"{$ name $} is {$ age $}");
}
Listing 4-2: Iterating a list, a range and a dict
apple
banana
cherry
1. apple
2. banana
3. cherry
alice is 30
bob is 25
ages.entries() yields (key, value) tuples, and (name, age) unpacks each one. Iterating the dict itself, for (name in ages), visits the keys in insertion order.
Note. Some older Loup material uses
range(1, 6)andlist.enumerate(). Neither exists in Loup 1.25; use1..6and an index range as above.
Mistake: a range as a value
0..n is part of the loop syntax, not a value:
@mode(host.scripted)
let indexes = 0..5;
for (i in indexes) {
println(i);
}
main.lpscr:3:15: error[E0001]: E_RANGE_FORM_MISPLACED: the range form `start..end` is permitted only as the iteration source of a `for` loop or a comprehension clause (§1-6.4)
Write the range in the loop header, for (i in 0..5), or build a list with a comprehension, let indexes = [i for i in 0..5];. There is no inclusive form (0..=5) and no step; use a while loop when you need either.
Mistake: assigning to the loop variable
The variable of a for-in loop is immutable, like a let:
@mode(host.scripted)
for (price in [10, 20, 30]) {
price = price * 2;
println(price);
}
main.lpscr:4:5: error[E0001]: cannot assign to immutable binding 'price'
If you want a changeable copy, declare it with var. Assigning it changes only that iteration's copy, never the list:
@mode(host.scripted)
for (var price in [10, 20, 30]) {
price = price * 2;
println(price);
}
20
40
60
Counted for
The three-part loop from C is available too. Its counter must be a var, because the update clause assigns it:
for (var i = 0; i < 3; i = i + 1) {
println(i);
}
0
1
2
The Language Reference writes this loop with let i = 0 (§1-6.4); the 1.25 compiler rejects that with cannot assign to immutable binding 'i', so use var. For simple counting, for (i in 0..3) is shorter.
Caution. Do not add elements to a list while a
forloop is iterating it. The Language Reference says a script gets a warning; in Loup 1.25 the loop keeps visiting the new elements, sofor (x in items) { items.push(x); }never ends. Build a new list instead.
break, continue and labels
break leaves the innermost loop and continue skips to its next pass. To leave an outer loop from inside an inner one, label the outer loop:
@mode(host.scripted)
let grid = [[1, 2, 3], [4, -5, 6], [7, 8, 9]];
search: for (r in 0..grid.length()) {
for (c in 0..grid[r].length()) {
if (grid[r][c] < 0) {
println(i"first negative at row {$ r $}, column {$ c $}");
break search;
}
}
}
Listing 4-3: Breaking out of two loops with a label
first negative at row 1, column 1
continue search; would move on to the next row instead. A break or continue naming a label that does not exist is a compile error (break label 'nowhere' not found).
Comprehensions
A list comprehension builds a list from a loop in one expression, optionally with a filter:
let squares = [n * n for n in 0..6];
let evenSquares = [n * n for n in 0..6 if n % 2 == 0];
println(squares);
println(evenSquares);
[0, 1, 4, 9, 16, 25]
[0, 4, 16]
Note that a comprehension's for clause has no parentheses. Chapter 5 shows comprehensions over other collections.
Destructuring
A let or var can unpack a tuple or a list into several bindings, including nested ones:
let (width, height) = (1920, 1080);
println(width * height);
let (name, (lat, lon)) = ("Lisbon", (38.7, -9.1));
println(i"{$ name $}: {$ lat $}, {$ lon $}");
let [first, second] = ["gold", "silver"];
println(first);
2073600
Lisbon: 38.7, -9.1
gold
⚠ Planned. Rest patterns (
let (first, *rest) = …) and struct patterns (let Point { x, y } = p;) are in the Language Reference (§1-6.8) but are not accepted by the 1.25 parser. Insidematch, struct and enum patterns do work (Chapter 7).
A First Look at match
match compares a value against a list of patterns and evaluates the arm of the first one that fits. Chapter 7 covers it in full; here it is on strings:
@mode(host.scripted)
func describe(status: string): string {
return match status {
"active" => "user is active",
"inactive" => "user is inactive",
_ => i"unknown status: {$ status $}",
};
}
println(describe("active"));
println(describe("banned"));
user is active
unknown status: banned
_ matches anything. Leave it out and the program still compiles — a string can hold any value, so the compiler cannot check that you covered them all — but a value no arm matches throws at run time:
@mode(host.scripted)
let status = "banned";
let message = match status {
"active" => "user is active",
"inactive" => "user is inactive",
};
println(message);
Uncaught MatchError: No matching arm for value in match expression
at <main> (main.lpscr:4:15)
Give every match on a string or a number a _ arm. For enums, the compiler does check that every variant is covered (Chapter 7).
Cleanup That Always Runs
Scripts open files, start processes and create temporary state that must be cleaned up even when something fails. Loup has three tools for this.
try/finally
A finally block runs when its try block is left in any way — falling off the end, return, break, continue or an exception:
@mode(host.scripted)
func loadConfig(path: string): string {
println(i"opening {$ path $}");
try {
return readText(path);
} finally {
println(i"done with {$ path $}");
}
}
try {
println(loadConfig("missing.toml"));
} catch (IoError as e) {
println(i"could not load: {$ e.message $}");
}
Listing 4-4: finally runs even when the read fails
opening missing.toml
done with missing.toml
could not load: read_text failed on 'missing.toml': No such file or directory (os error 2)
catch (IoError as e) handles one type of exception and binds it to e. Chapter 10 covers exceptions in full.
with
with (name = expression) { … } acquires a resource, runs the block and releases the resource when the block ends, however it ends. The resource's class must implement Closeable (it then needs a close() method) or the more general Scoped[T] (§1-6.6). Several resources are released in reverse order:
@mode(host.scripted)
class LogFile implements Closeable {
pub let name: string;
new(name: string) {
this.name = name;
println(i"open {$ name $}");
}
pub func close(): void {
println(i"close {$ this.name $}");
}
}
try {
with (input = LogFile("in.log"), output = LogFile("out.log")) {
println(i"copying {$ input.name $} to {$ output.name $}");
throw RuntimeError("disk full");
}
} catch (RuntimeError as e) {
println(i"failed: {$ e.message $}");
}
Listing 4-5: with closes both resources, in reverse order, when the block throws
open in.log
open out.log
copying in.log to out.log
close out.log
close in.log
failed: disk full
A class that implements neither interface is rejected with E_WITH_NOT_SCOPED, and the message reminds you that Closeable and Releasable provide what with needs.
del
del name; ends a binding early and calls the object's finalize() method, if it has one, at that point (§1-6.11). It is rarely needed; use it when a large object should be released before the end of its scope.
Known gap: defer
The Language Reference defines defer (§1-6.5): defer close(handle); registers a statement to run when the enclosing block ends, and several defers run last-registered-first. The 1.25 compiler accepts defer, but the interpreter never runs the deferred statement:
@mode(host.scripted)
func work(): void {
defer println("cleanup");
println("working");
}
work();
println("after");
working
after
cleanup is never printed. Until this is fixed, use try/finally or with for cleanup that must happen.
A Worked Example: Guessing by Halving
This script guesses a secret number between 1 and 100 by halving the range on every try. It uses while (true) with break, an else if chain, an if expression and the main guard from Chapter 1:
@mode(host.scripted)
## Find `secret` between 1 and 100 by halving the range each time.
func findByHalving(secret: int): int {
var low = 1;
var high = 100;
var tries = 0;
while (true) {
let guess = (low + high) // 2;
tries = tries + 1;
if (guess < secret) {
println(i"{$ guess $}: too low");
low = guess + 1;
} else if (guess > secret) {
println(i"{$ guess $}: too high");
high = guess - 1;
} else {
let noun = if (tries == 1) { "guess" } else { "guesses" };
println(i"{$ guess $}: found in {$ tries $} {$ noun $}");
break;
}
}
return tries;
}
if (__name__ == "__main__") {
findByHalving(73);
findByHalving(50);
}
Listing 4-6: Binary search with while, break and an if expression
50: too low
75: too high
62: too low
68: too low
71: too low
73: found in 6 guesses
50: found in 1 guess
Summary
if,whileandforheaders always take parentheses (E_CONDITION_PARENS), and conditions must bebool.ifis also an expression; there is no ternary operator.for (x in items)iterates lists, dict keys,entries()and generators;start..endis an exclusive range that only appears in loop headers and comprehensions.- The loop variable is immutable; write
for (var x in …)to get a changeable copy. A countedforneeds avarcounter. breakandcontinuetake an optional label to leave or continue an outer loop.let (a, b) = …andlet [a, b] = …destructure tuples and lists.matchon strings needs a_arm, or an unmatched value throwsMatchError.- For cleanup, use
try/finally,withon aCloseableresource, ordel.deferis accepted but does not run in Loup 1.25.
Try It Yourself
- Print FizzBuzz for 1 to 30 using
for (n in 1..31)and amatchon(n % 3, n % 5)— tuple patterns such as(0, 0) => "FizzBuzz"work inmatch. - Write a
CloseableclassTimerthat prints how many loop iterations ran when it is closed, and use it in awitharound a loop thatbreaks early. - Rewrite Listing 4-3 without a label, using a
var found = false;flag. Which version is easier to read?