Generics and Blocks
Goose's generics are simple to write: leave out a parameter's type and the
function is generic. Its function values are simple too, because they exist
only at compile time. Together they let the standard library offer map,
filter and sort that compile to plain loops, and let you write your own
iteration helpers that behave exactly like built-in loops.
#Generic functions16.1
A parameter with no type is generic. The function is compiled separately for each combination of argument types it is called with:
fn twice(x) { x + x }
fn main() {
print(twice(21), " ", twice(1.5));
}
42 3.0
When two parameters must have the same type, or when a type is needed that no parameter carries, name it with angle brackets:
fn larger<T>(a: T, b: T) -> T { if a > b { a } else { b } }
fn zero<T>() -> T { default<T>() }
fn main() {
let w1: u8 = 40;
let w2: u8 = 52;
print(larger(3, 8), " ", larger(2.5, 1.0), " ", larger(w1, w2), " ", zero<f64>() + 0.5);
}
8 2.5 52 0.5
Type arguments are usually inferred from the arguments. When nothing
determines one, as with zero, you give it explicitly: zero<f64>().
There are no trait bounds. Goose never type-checks a generic body in the abstract. It checks each specialization with the real types, so a generic function can do anything its arguments allow, and an error shows up where the types do not fit, together with the chain of calls that led there:
fn twice(x) { x + x }
fn main() { print(twice("goose")); }
error: operator + cannot be applied to const u8[:] and const u8[:] in fn twice(const u8[:]) instantiated from main.goose:3
#Function values16.2
A function can be passed to another function. The parameter that receives it is a type parameter, written in the angle brackets like any other, and it is called like a function:
fn square(x: i64) -> i64 { x * x }
fn apply<F>(x: i64) -> i64 { F(x) }
fn main() {
print(apply(7, square));
}
49
The function argument comes after the ordinary ones. You can document its
shape with F: fn(i64) -> i64, but that is optional.
Function values in Goose are compile-time entities. Passing square to
apply creates a specialization of apply with square built in, and the
call F(x) becomes a direct call that can be inlined. A function value can
be passed down, but never stored, returned or put into data, so there are no
function pointers and no closures that outlive their scope:
fn square(x: i64) -> i64 { x * x }
fn main() {
var f = square;
}
error: function values are compile-time only and cannot be stored (§7.6)
That restriction is the reason a higher-order function in Goose costs exactly what the loop it contains costs.
#Trailing blocks16.3
Passing a named function works, but the common case is a small piece of
code written right at the call. A trailing block is a { … } after a
call's closing parenthesis, and it becomes the call's last argument. Inside
it, it is the block's single parameter. For several parameters, name
them before a =>:
fn repeat<F>(n: i64) { for i in n { F(i); } }
fn each_pair<T, F>(xs: T[:]) {
for i in 0..xs.len - 1 { F(xs[i], xs[i + 1]); }
}
fn main() {
repeat(3) { print("honk ", it); };
let weights: i64[5] = [40, 45, 52, 48, 61];
var gains: i64[>..] = [];
each_pair(weights) { a, b => gains.push(b - a); };
print(gains);
}
honk 0 honk 1 honk 2 [5, 7, -4, 13]
A block can read and write the variables of the function it is written in,
as gains.push(…) does. Because the block is inlined into the
specialization, that costs nothing.
Note the ; after the block. A call with a trailing block is an expression
statement, so it ends with a semicolon like any other call.
#return inside a block16.3.1
A return inside a block returns from the function the block is written
in, not from the function that called the block. So a hand-written
iteration helper is just as good as a built-in loop. Inside a loop body you
can return from the enclosing function, and the same goes for the body of
repeat:
fn repeat<F>(n: i64) { for i in n { F(i); } }
fn first_heavy(weights: i64[:]) -> i64 {
repeat(weights.len) {
if weights[it] > 50 { return weights[it]; } // leaves first_heavy
};
return -1;
}
fn main() {
let weights: i64[5] = [40, 45, 52, 48, 61];
print(first_heavy(weights), " ", first_heavy(weights[..2]));
}
52 -1
Under the hood this uses the same mechanism as return … from in Chapter
15.
#The library's higher-order functions16.4
With these pieces, the standard library provides the usual toolkit. All of these take a slice and a block, and all of them compile to a single loop. Results come back as new grow-only arrays, built directly in the variable that receives them:
import std;
fn main() {
let weights: i64[5] = [40, 45, 52, 48, 61];
let heavy = weights.filter() { it > 46 };
let kilos = weights.map() { it as f64 / 10.0 };
let total = fold(weights, 0) { acc, w => acc + w };
print(heavy, " ", kilos, " ", total);
print(weights.any() { it > 60 }, " ", weights.all() { it > 60 }, " ",
weights.count() { it % 2 == 0 }, " ", sum(weights));
let big = weights.find() { it > 50 };
if big { print("first over 50: ", big); }
}
[52, 48, 61] [4.0, 4.5, 5.2, 4.8, 6.1] 246 true false 3 246 first over 50: 52
find returns an optional reference into the slice, like the functions you
wrote in Chapter 11. map's result type is whatever the block returns.
sort sorts in place. Without a block it uses <; with one, the block is
the comparison, and returns true when its first argument should come
first:
import std;
struct Goose { name: u8[..8], weight: i64 }
fn main() {
var flock: Goose[>..] = [Goose { "Ada", 45 }, Goose { "Bea", 52 }, Goose { "Cleo", 38 }];
sort(flock) { a, b => a.weight < b.weight };
print(flock);
var ids: i64[5] = [5, 3, 9, 1, 7];
sort(ids);
print(ids);
}
[Goose { "Cleo", 38 }, Goose { "Ada", 45 }, Goose { "Bea", 52 }]
[1, 3, 5, 7, 9]
Chapter 18 covers the rest of std.
#Nested functions as values16.5
A nested function can be passed as a function value too, which gives you a way to pass a "block" that is too long to write inline, or that you want to use twice:
import std;
fn main() {
var gaggle = 0;
fn count_heavy(w: i64) -> bool {
gaggle++;
w > 46
}
let weights: i64[5] = [40, 45, 52, 48, 61];
let heavy = weights.filter(count_heavy);
print(heavy, " after ", gaggle, " checks");
}
[52, 48, 61] after 5 checks
#Exercises16.6
-
Write a generic
fn clamp_all<T>(xs: T[:], lo: T, hi: T)that clamps every element of a slice in place, and use it on both ani64array and anf32array. -
Write
fn each_window<T, F>(xs: T[:], n: i64)that calls its block with every run ofnconsecutive elements, as a slice. Use it to print the sum of each run of three in[1, 2, 3, 4, 5]. -
Use
filterandmapto turn[61, 72, 58, 66, 70], a list of egg weights in grams, into the list of eggs above 60 grams, in kilograms.