Chapter 5

Functions

Goose functions are free functions: there are no methods, no classes and no impl blocks. That does not stop you from writing goose.honk(), as you will see. This chapter covers declaring and calling functions, the several ways of getting values in and out of them, overloading, and functions declared inside other functions.

#Declaring a function5.1

A function is fn, a name, a parameter list and, after ->, the result type. The body's last expression is the result, so short functions need no return:

fn feed(grams: i64) -> i64 { grams * 2 }

fn ration(geese: i64, grams_each: i64) -> i64 {
    if geese <= 0 { return 0; }
    return geese * feed(grams_each);
}

fn main() {
    print(ration(12, 150));
}
3600

Both styles work: an explicit return, or a final expression without a semicolon. Most Goose code uses the final expression for short functions and return for early exits.

The result type can be left out when the compiler can infer it from the body. It is good style to write it on anything other than a trivial helper, because it documents the function and keeps error messages close to the mistake.

Top-level declarations can appear in any order. There are no prototypes or headers, and main can call functions declared below it.

#Parameters are lets5.1.1

A parameter behaves like a let: you cannot reassign it. If you want a local copy you can change, mark the parameter var:

fn countdown(var n: i64) -> i64 {
    var steps = 0;
    while n > 0 { n -= 3; steps += 1; }
    steps
}

fn main() {
    print(countdown(10));
}
4

Without the var, the n -= 3 would be the familiar error, cannot assign to let n.

#Reference parameters5.2

A parameter of type T& is a reference to a T that belongs to the caller. Writing to it writes the caller's variable. At the call site you pass the variable itself, with no &:

fn add_eggs(count: i64&, n: i64) {
    count += n;
}

fn main() {
    var eggs = 0;
    add_eggs(eggs, 4);
    add_eggs(eggs, 2);
    print(eggs);
}
6
main 4 eggs add_eggs(count: i64&, n: i64) • count 2 n (a copy) a reference parameter is the caller's variable
A reference parameter refers to a variable in the caller's frame. An ordinary parameter is a copy.

Inside the function, count behaves exactly like an i64. There is no dereference operator, no *count and no ->. A reference is transparent: wherever it is used as a value, it stands for the thing it refers to. Chapter 8 is about references in general, and explains how you talk about the reference itself rather than its target.

#Default arguments5.3

Trailing parameters can have default values, which a call may leave out:

fn portion(grams: f64, geese: f64 = 1.0, waste: f64 = 0.1) -> f64 {
    grams / geese * (1.0 - waste)
}

fn main() {
    print(portion(1000.0), " ", portion(1000.0, 4.0), " ", portion(1000.0, 4.0, 0.0));
}
900.0 225.0 250.0

A default is evaluated at each call that leaves it out. It can use globals, constants and other functions, but never the caller's local variables, even if one happens to have the same name as something the default refers to.

#Several results5.4

A function can return more than one value. List the types after ->, and return the values separated by commas:

fn split_flock(geese: i64, pens: i64) -> i64, i64 {
    return geese / pens, geese % pens;
}

fn main() {
    let per_pen, left_over = split_flock(23, 5);
    print(per_pen, " per pen, ", left_over, " left over");
}
4 per pen, 3 left over

Several results are a calling convention, not a tuple type. Goose has no tuples. You cannot store "the pair" in a variable or pass it around. You bind the results to names with let a, b = f();, or assign them to variables that already exist with a, b = f();. Used where a single value is expected, a call gives its first result.

The usual way to report success or failure is a trailing bool, which says whether the other results mean anything:

fn eggs_on_day(day: i64) -> i64, bool {
    if day % 7 == 0 { return 0, false; }    // the geese rest on Sundays
    return day % 3 + 1, true;
}

fn main() {
    let n, ok = eggs_on_day(14);
    print(n, " ", ok);
    var count = 0;
    var laid = false;
    count, laid = eggs_on_day(5);
    print(count, " ", laid, " ", eggs_on_day(5));
}
0 false
3 true 3

The last call is in a value context, so only its first result, 3, is printed.

Only return and a function call can carry several values. An if cannot produce two values, so return if c { 1, 2 } else { 3, 4 }; is not allowed; each branch has to return on its own.

#Overloading5.5

Several functions can share a name, as long as their parameter types differ. The call picks the one that fits its arguments, at compile time:

fn describe(x: i64) -> u8[:] { "a count" }
fn describe(x: f64) -> u8[:] { "a weight" }
fn describe(x: bool) -> u8[:] { "a yes or no" }

fn main() {
    print(describe(12), ", ", describe(4.5), ", ", describe(true));
}
a count, a weight, a yes or no

An exact match beats a match that needs a conversion. Converting an integer to a float is the last resort, so an integer argument picks an integer overload over a float one when both are possible:

fn weigh(x: i32) -> u8[:] { "i32" }
fn weigh(x: f64) -> u8[:] { "f64" }

fn main() {
    let b: u8 = 3;
    print(weigh(2), " ", weigh(b), " ", weigh(2.5));
}
i32 i32 f64

When no overload fits, the error lists each candidate and why it was rejected:

fn hatch(day: i64) { }

fn main() {
    hatch(1.5);
}
error: no matching overload for call to hatch
candidate hatch at main.goose:1: argument 1: cannot pass f64 as i64

The result type never selects an overload. You can declare two functions that differ only in what they return, but every call to them is then ambiguous, and the compiler says so at the call.

#Method-call syntax5.6

Any function can be called as if it were a method of its first argument: x.f(a, b) means f(x, a, b). This is called uniform function call syntax, or UFCS, and Goose uses it everywhere. Most of the standard library is written as free functions that read naturally in this form:

fn twice(x: i64) -> i64 { x * 2 }
fn plus(x: i64, y: i64) -> i64 { x + y }

fn main() {
    print(21.twice(), " ", 4.twice().twice(), " ", 5.plus(3).twice());
}
42 16 16

When you write x.f(), fields and built-in members such as .len are looked up first, then functions. You will see xs.push(v), text.trim() and list.sort() throughout this book. They are all ordinary functions called with UFCS.

#Nested functions5.7

A function can be declared inside another function. The nested function can read and write the enclosing function's local variables:

fn main() {
    var total = 0;
    var visits = 0;
    fn feed(grams: i64) {
        total += grams;
        visits++;
    }
    feed(150);
    feed(200);
    feed(125);
    print(total, " g over ", visits, " feedings");
}
475 g over 3 feedings

A nested function is not a closure object. It exists only at compile time, so it cannot be stored in a variable, returned or put in a data structure. It can be passed to other functions as a compile-time argument, which is the subject of Chapter 16. That restriction is what lets every call be a direct call that the compiler can inline.

Nested functions are more useful than they look. A parser, for example, can keep its input, its cursor and its output as locals of one function and declare its recursive-descent functions inside it, all sharing that state without passing a context struct around. Chapter 25 is built that way.

#Recursion is declared5.8

There is one thing about functions that is quite unlike other languages: a function that calls itself must say so. This factorial does not compile:

fn fact(n: i64) -> i64 {
    if n <= 1 { 1 } else { n * fact(n - 1) }
}

fn main() { print(fact(10)); }
error: recursive call cycle through fact, which is not declared `recursive fn` (§7.8)

Mark the function recursive fn and it does:

recursive fn fact(n: i64) -> i64 {
    if n <= 1 { 1 } else { n * fact(n - 1) }
}

fn main() { print(fact(10)); }
3628800

The keyword exists because of the memory model. A recursive function has some restrictions on the data it can hold across a recursive call, and the compiler needs to know where every recursive cycle starts in order to check them. Chapter 17 explains the rules. Until then, a recursive function that only uses scalars, like fact, needs nothing more than the keyword.

#Functions that never return5.9

The built-ins abort(message) and exit(code) never return, and the compiler knows it. A branch that ends in one of them does not need to produce a value:

fn eggs_per_goose(eggs: i64, geese: i64) -> i64 {
    if geese > 0 { eggs / geese } else { abort("no geese") }
}

fn main() {
    print(eggs_per_goose(30, 6));
}
5

This also works through your own functions: a function whose every path ends in abort or exit is known never to return, so it can be called in the same position.

#Exercises5.10

  1. Write a function min_max(a: i64, b: i64, c: i64) -> i64, i64 that returns the smallest and the largest of its three arguments, and call it with 7, -2, 4.

  2. Write fn clamp_weight(w: f64, lo: f64 = 2.0, hi: f64 = 8.0) -> f64, and use it with UFCS to clamp 9.5, 1.0 and 5.0, the last one with a custom range of 6 to 7.

  3. Write a recursive function digits(n: i64) -> i64 that returns how many decimal digits a non-negative number has, with digits(0) == 1.