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
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
-
Write a function
min_max(a: i64, b: i64, c: i64) -> i64, i64that returns the smallest and the largest of its three arguments, and call it with7, -2, 4. -
Write
fn clamp_weight(w: f64, lo: f64 = 2.0, hi: f64 = 8.0) -> f64, and use it with UFCS to clamp9.5,1.0and5.0, the last one with a custom range of 6 to 7. -
Write a recursive function
digits(n: i64) -> i64that returns how many decimal digits a non-negative number has, withdigits(0) == 1.