Control Flow as Expressions
Goose's control flow will look familiar, and most of it works the way you
expect. The main idea to absorb is that if, match and blocks are
expressions: they produce values. Once you are used to that, a lot of
small temporary variables disappear from your code.
#if4.1
An if used as a statement needs no else, and its condition needs no
parentheses. The braces are required:
fn main() {
let honks = 7;
if honks > 5 { print("noisy goose"); }
}
noisy goose
Used as a value, an if must have an else, and both branches must agree
on a type. This is how Goose does what C does with ?:, so there is no
ternary operator:
fn main() {
let weight = 3.8;
let size = if weight > 4.0 { "large" } else if weight > 3.0 { "medium" } else { "small" };
print(size);
}
medium
Leaving out the else is an error, because the if would have no value
when the condition is false:
fn main() {
let honks = 5;
let loud = if honks > 2 { true };
}
error: an if used as a value requires an else branch
A block's value is its last expression, written without a semicolon. That is the same rule as in Rust. Put a semicolon after it and the block has no value.
#Statements that end with a block4.1.1
An if, a match, a loop or any other construct that ends with a } is a
complete statement by itself, and needs no semicolon after it. The flip side
is that an operator cannot continue such a statement. This is an error, even
though it looks reasonable:
fn main() {
let honks = 5;
let v = if honks > 2 { 1 } else { 2 } + 3;
}
error: expected ';' in variable declaration, found '+'
The parser saw a complete if. To use one as an operand, put it in
parentheses:
fn main() {
let honks = 5;
let v = (if honks > 2 { 1 } else { 2 }) + 3;
print(v);
}
4
#match4.2
match picks one of several arms by comparing a value against patterns.
Chapter 12 covers matching on enums, which is where match really earns its
keep. Here we look at matching on integers.
The patterns of an integer match are literals, named constants, half-open
ranges, and comma-separated lists of those. The first arm that matches is
taken, and a final _ arm is required:
let EGG = 0;
let GOSLING = 1;
let JUVENILE = 3;
let ADULT = 52;
fn stage(weeks: i64) -> u8[:] {
match weeks {
EGG => "egg",
GOSLING..JUVENILE => "gosling",
JUVENILE..ADULT => "juvenile",
_ => "adult",
}
}
fn main() {
for w in [0, 1, 2, 3, 30, 52] { print(w, " weeks: ", stage(w)); }
}
0 weeks: egg 1 weeks: gosling 2 weeks: gosling 3 weeks: juvenile 30 weeks: juvenile 52 weeks: adult
Like every range in Goose, GOSLING..JUVENILE is half-open: it includes
1 and 2 but not 3. There is no inclusive range syntax, so adjacent ranges
share their boundary values, as GOSLING..JUVENILE and JUVENILE..ADULT
do here, without overlapping.
A match is an expression, like if, so stage simply returns the value
of the match. Leaving out the _ arm is an error even if you think you
have covered every case:
fn describe(n: i64) -> u8[:] {
match n { 0 => "none", 1..10 => "a few" }
}
fn main() { print(describe(3)); }
error: integer match requires a _ arm
#Blocks are expressions too4.3
A pair of braces on its own makes a scope, and like any block it has the value of its last expression. That is useful for computing a value with a few temporaries that should not leak into the surrounding code:
fn main() {
let area = {
let width = 12;
let depth = 8;
width * depth
};
print(area);
}
96
#block and break with a value4.3.1
Sometimes you want to bail out of a computation early with a result, without
writing a separate function. block { … } gives break a target that is
not a loop, and break value makes that the block's value:
fn main() {
let weight = 0.0;
let eggs = 3;
let verdict = block {
if weight <= 0.0 { break "no goose"; }
if eggs == 0 { break "not laying"; }
"healthy"
};
print(verdict);
}
no goose
Every break of the same block must give a value of the same type, and
the block's final expression must match it too.
#guard4.4
guard states a condition that the rest of the block depends on. It is an
if turned inside out: rather than nesting the main path inside the
condition, you rule out the bad cases first and continue at the same
indentation.
guard cond else { … } runs the else block when the condition is false.
To leave the function, the loop or the block, put a return, break or
continue inside the else:
fn eggs_per_goose(eggs: i64, geese: i64) -> i64 {
guard geese > 0 else { return 0; }
eggs / geese
}
fn main() {
print(eggs_per_goose(30, 6), " ", eggs_per_goose(30, 0));
}
5 0
guard cond; without an else simply skips the rest of the current block
when the condition is false. In a loop body, skipping the rest of the body
means going on to the next iteration:
fn main() {
for i in 6 {
guard i % 2 == 0;
print("even ", i);
}
}
even 0 even 2 even 4
guard really shines with optionals, because after guard x else { … }
the compiler knows that x is not null for the rest of the block. That has
to wait for Chapter 11.
#Loops4.5
Goose has three loops: for, while and loop.
#for4.5.1
for i in n counts from 0 up to n - 1, and for i in a..b counts from
a up to but not including b:
fn main() {
for i in 3 { print("lap ", i); }
for i in 10..13 { print("pen ", i); }
}
lap 0 lap 1 lap 2 pen 10 pen 11 pen 12
The loop variable has the type of the range's bounds, i64 for plain
literals. You can ask for another type, and because the end of a range is
one past the last value, you can visit every value of a small type,
including its maximum:
fn main() {
var count = 0;
for b: u8 in 0..256 { count += 1; }
print(count, " byte values");
}
256 byte values
for x in array visits the elements of an array. For elements of a fixed
size, x is a copy of each element, and the compiler stops you from
writing to it, since the write would change only the copy:
fn main() {
let weights: i64[3] = [4, 5, 6];
for w in weights { w += 1; }
}
error: cannot write w: a by-value binding is a copy of the element; bind it by reference (&w) to write the element (§6.5)
The message tells you the fix. for &x in array binds each element by
reference, so writes go to the array. for x, i in array also gives you
the index:
fn main() {
var weights: i64[3] = [4, 5, 6];
for &w in weights { w *= 1000; }
for w, i in weights { print("goose ", i, ": ", w, " g"); }
}
goose 0: 4000 g goose 1: 5000 g goose 2: 6000 g
#while and loop4.5.2
while cond { … } is what you expect. loop { … } runs until something
leaves it, and like block it can produce a value with break value:
fn main() {
var n = 0;
let first = loop {
n += 6;
if n % 4 == 0 { break n; }
};
print("first multiple of 6 and 4: ", first);
var feed = 10;
while feed > 0 { feed -= 3; }
print(feed);
}
first multiple of 6 and 4: 12 -2
break leaves the innermost loop or block, and continue goes to the
next iteration of the innermost loop. There are no labels, so you cannot
break out of two loops at once. When you need to, put the loops in a
function and return, or use a block around them.
#defer4.6
defer runs a statement, or a block, when the enclosing block ends, however
it ends: by falling off the end, by break, continue or return. It is
the same idea as Odin's defer, and it is used for the same thing: keeping
cleanup next to the code that needs it.
When several defers are pending, they run latest first. A defer inside a
loop body runs at the end of each iteration, including iterations that end
with continue:
fn main() {
{
defer print("first deferred, runs last");
defer print("second deferred, runs first");
print("in the block");
}
for i in 3 {
defer print(" end of lap ", i);
if i == 1 { continue; }
print(" lap ", i);
}
}
in the block second deferred, runs first first deferred, runs last lap 0 end of lap 0 end of lap 1 lap 2 end of lap 2
A deferred body is ordinary code that runs later. It sees variables as they
are when the block exits, not as they were when the defer was written.
It cannot itself return, break or continue out of the block.
You will use defer much less in Goose than in Odin, because there is no
memory to free. It is for the other kinds of cleanup: closing a window,
restoring a counter, printing a closing bracket.
#Exercises4.7
-
Write a function
grade(score: i64) -> u8[:]that returns"A"for scores of 90 and above,"B"for 80 to 89,"C"for 70 to 79 and"F"otherwise, using a singlematch. What should happen for scores above 100? -
Use
loopwithbreak valueto find the smallestnfor whichn * nis larger than 2026. -
Print a 4 by 4 checkerboard of
#and.characters, starting with#in the top-left corner. Each call toprintwrites one row, so give it four arguments, each anifexpression that picks the character for one square.