Chapter 4

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:

8 9 10 i 11 i 12 i 13 14 15 for i in 10..13 13 itself is left out
A range a..b starts at a and stops just before 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

  1. 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 single match. What should happen for scores above 100?

  2. Use loop with break value to find the smallest n for which n * n is larger than 2026.

  3. Print a 4 by 4 checkerboard of # and . characters, starting with # in the top-left corner. Each call to print writes one row, so give it four arguments, each an if expression that picks the character for one square.