A Tour of the Standard Library
Goose's standard library is small, written almost entirely in Goose, and
built on the same principles as the language: no hidden allocations,
results built directly where they are wanted, and slices instead of
copies. This chapter is a tour rather than a reference. It shows the parts
you will use most, with enough detail to start using them. The official
docs/stdlib.md has every function.
#Importing modules18.1
A module is imported by name:
import std;
import dictionary;
std, dictionary, vec, math and os put their names in the global
namespace, so after import std; you call trim(s) or s.trim(). The
other modules have namespaces of their own, so their functions are written
with a prefix: csv::each_record, json::parse, regex::compile.
An import only loads the file. It does not bring in anything the file does not declare, and importing the same module twice is harmless.
#std: arrays18.2
Most of std is generic functions over slices, and most of those take a
block. You met several in Chapter 16. Here is a wider selection, grouped by
what they do to their input.
Reading and searching leave the array alone: find (a block, or a
sub-slice), position, contains, any, all, count, min, max,
binary_search, starts_with, ends_with.
Rearranging in place keeps the length: sort, stable_sort, reverse,
shuffle, fill.
Changing the length takes the whole array by reference, and works on
any array kind that has the needed operations: insert_at, remove_at,
swap_remove, retain, dedup, push_n.
import std;
fn main() {
var eggs: i64[>..] = [58, 58, 61, 66, 66, 66, 72];
dedup(eggs); // drops adjacent duplicates
print(eggs);
eggs.retain() { it > 60 }; // keeps what the block accepts
eggs.insert_at(0, 59);
let first = eggs.remove_at(1);
print(eggs, " removed ", first);
let at, found = binary_search(eggs, 66);
reverse(eggs);
print(at, " ", found, " ", eggs, " ", contains(eggs, 99));
}
[58, 61, 66, 72] [59, 66, 72] removed 61 1 true [72, 66, 59] false
std also has a binary heap that works on any array you can push to and
pop from. A grow-shrink array is the natural container for it:
import std;
fn main() {
var queue: i64[>..<] = [];
for urgency in [5, 1, 4, 2, 3] { queue.heap_push(urgency); }
var order: i64[>..] = [];
while queue.len > 0 { order.push(queue.heap_pop()); }
print(order);
}
[1, 2, 3, 4, 5]
heap_push and heap_pop keep a min-heap ordered by <. Like sort, they
take an optional block that decides which of two elements comes first.
#std: numbers, hashing and randomness18.3
min, max, clamp, abs, sign and lerp are generic over the
numeric types. rem and div_euclid you met in Chapter 3. popcount,
clz and ctz count bits.
hash hashes any integer, float or byte string to a u64, and
hash_combine mixes two hashes. You need these when you use a struct of
your own as a dictionary key.
Random numbers come from rng, a small splitmix64 generator whose state is
its seed. The same seed always gives the same sequence, which makes tests
reproducible:
import std;
fn main() {
var r = rng { 42 };
var rolls: i64[>..] = [];
for i in 5 { rolls.push(r.rand_int(6) + 1); }
var deck: i64[6] = [1, 2, 3, 4, 5, 6];
shuffle(deck, r);
print(rolls, " ", deck);
}
[2, 2, 1, 1, 5] [3, 4, 2, 5, 6, 1]
For a different sequence each run, import os; and seed it with random_seed(), which asks the operating system for entropy.
#dictionary18.4
dictionary<K, V> is a hash table with fixed-size keys and values. It
uses open addressing, so it is a single grow-shrink array of slots with no
per-entry allocation:
import dictionary;
fn main() {
var weights = dictionary<const u8[:], f32> {};
weights.insert("Ada", 4.5);
weights.insert("Bea", 5.25);
weights.insert("Ada", 4.75); // overwrites
let w = weights.get("Ada");
if w { print("Ada weighs ", w); }
print(weights.count, " ", weights.contains("Cleo"), " ", weights.get("Cleo") == null);
}
Ada weighs 4.75 2 false true
get returns an optional reference to the value, so you can update it
through the result. update(key, initial) { … } does a lookup and a
change in one call, inserting initial first if the key is missing. It is
the right tool for counting:
import std;
import dictionary;
fn main() {
let text = "honk hiss honk flap honk hiss";
var counts = dictionary<const u8[:], i64> {};
each_word(text) { counts.update(it, 0) { it += 1; }; };
var keys: (const u8[:])[>..] = [];
counts.each() { k, v => keys.push(k); };
sort(keys) { a, b => compare(a, b) < 0 };
for k in keys {
let n = counts.get(k);
if n { print(k, " x", n); }
}
}
flap x1 hiss x2 honk x3
The keys of counts are slices into text. Every word is a view of the
original string, so no word is copied:
That is also why the key type is const u8[:]: the keys may be views of
read-only text, such as a literal. A dictionary that needs to own its keys
uses a fixed-size key such as u8[..16].
The dictionary's slots live in a grow-shrink array, so the rule from
Chapter 7 applies. A reference returned by get must not be used after the
next insert, update or remove, and the compiler tells you if it is.
each visits entries in an unspecified order, which is why the example
sorts the keys before printing.
#math and vec18.5
math is libm at both float widths: sqrt, sin, cos, atan2, exp,
log, pow, floor, ceil, round and the rest, plus PI and TAU.
The function is picked by the argument's type, so an integer argument has
to choose: write sqrt(n as f64).
vec provides small vector structs, vec2<T>, vec3<T> and vec4<T>, with
the aliases float3, double2, int3 and so on. They use the memberwise
arithmetic from Chapter 10, and add dot, cross, length, normalize,
distance and lerp:
import math;
import vec;
fn main() {
let v = float3 { 1.0, 2.0, 2.0 };
print(sqrt(2.0), " ", floor(2.7), " ", pow(2.0, 10.0));
print(length(v), " ", dot(v, v), " ", v * 2.0);
print(cross(float3 { 1.0, 0.0, 0.0 }, float3 { 0.0, 1.0, 0.0 }));
}
1.4142135623730951 2.0 1024.0
3.0 9.0 vec3<f32> { 2.0, 4.0, 4.0 }
vec3<f32> { 0.0, 0.0, 1.0 }
#os: files, arguments and time18.6
os is a thin layer over the operating system. File functions take a path
and return a bool for success. Reading appends to a builder you supply,
which fits the memory model: the caller decides where the bytes live.
import std;
import os;
fn main() {
print(write_file("eggs.txt", "61\n72\n58\n"));
append_file("eggs.txt", "66\n");
var text: u8[>..] = [];
if read_file("eggs.txt", text) {
print(text.len, " bytes: ", words(text));
}
delete_file("eggs.txt");
print(file_exists("eggs.txt"));
}
true 12 bytes: ["61", "72", "58", "66"] false
There are also list_dir, make_dir, rename_file and
write_file_atomic, which writes a new file and renames it into place, so
a crash never leaves a half-written file. For the console there are
read_line, read_stdin, write_stdout and write_stderr. args()
returns the program's arguments, as you saw in Chapter 2. clock()
returns a monotonic time in seconds for measuring durations, and
sleep_ms(ms) waits. Subprocesses and networking are not part of the
library yet.
#csv18.7
csv::each_record reads comma-separated text as RFC 4180 defines it,
including quoted fields, and calls its block once per record. The fields
are slices of the input:
import std;
import csv;
fn main() {
let roster = "name,weight,laying\nAda,4.5,true\n\"Bea, the bold\",5.25,false\n";
var total = 0.0;
var row = 0;
let ok = csv::each_record(roster) { fields =>
row++;
if row > 1 {
let w, wok = parse_flt(fields[1]);
if wok { total += w; }
print(fields[0], " | ", fields[2]);
}
};
print(ok, ", total weight ", total);
}
Ada | true Bea, the bold | false true, total weight 9.75
csv::format_field writes a field back out, quoting it if needed.
#json18.8
json offers three ways to work with JSON. The writer appends to a builder,
and you write the brackets and commas yourself:
import json;
fn main() {
var out: u8[>..] = [];
out.push('{');
json::format_key(out, "name");
json::format_string(out, "Ada \"the first\"");
out.push(',');
json::format_key(out, "weight");
json::format_number(out, 4.5);
out.push('}');
print(out);
}
{"name":"Ada \"the first\"","weight":4.5}
The parser builds a document tree in an array you provide, with every node linked to the next by the self-relative links of Chapter 13, and strings left as slices of the input:
import json;
fn main() {
let text = """{"flock": [{"name": "Ada", "weight": 4.5}, {"name": "Bea", "weight": 5.25}], "pond": "north"}""";
var doc: json::Node[>..] = [];
var decoded: u8[>..] = [];
let root, err = json::parse(text, doc, decoded);
guard root else { abort(err); }
let pond, ok = json::as_string(json::member(root, "pond"));
print("pond: ", pond);
let flock = json::member(root, "flock");
if flock {
json::each(flock) { g =>
let name, n_ok = json::as_string(json::member(g, "name"));
let w, w_ok = json::as_f64(json::member(g, "weight"));
print(name, " ", w);
};
}
}
pond: north Ada 4.5 Bea 5.25
The third way reads the text straight into your own types without building
a tree, in the style of a serde derive. json::reader keeps a cursor, and
each_member and each_element walk objects and arrays:
import json;
struct Goose { name: u8[..12], weight: f64 }
fn main() {
var r = json::reader("""[{"name": "Cleo", "weight": 3.8, "colour": "grey"}]""");
var geese: Goose[>..] = [];
json::each_element(r) {
var g = Goose { name: "", weight: 0.0 };
var scratch: u8[>..] = [];
json::each_member(r) { key =>
if key == "name" { g.name = json::read_string(r, scratch); }
else if key == "weight" { g.weight = json::read_number(r); }
else { json::skip(r); }
};
geese.push(g);
};
print(json::finish(r), " ", geese);
}
true [Goose { "Cleo", 3.8 }]
Errors are sticky: after the first unexpected character every read fails
quietly, and finish reports whether the whole text was read cleanly.
#regex18.9
regex implements regular expressions with the syntax and matching rules
of Rust's regex crate, compiled to DFAs when you call regex::compile.
each_captures calls a block for every match, with access to the groups:
import std;
import regex;
fn main() {
let re = regex::compile("(\\w+) laid (\\d+)");
let log = "Ada laid 3, Bea laid 1, Cleo slept, Dot laid 2";
var total = 0;
regex::each_captures(re, log) { c =>
let n, ok = parse_int(regex::group(c, 2));
if ok { total += n; print(regex::group(c, 1), ": ", n); }
};
print(total, " eggs from ", regex::count(re, log), " geese");
}
Ada: 3 Bea: 1 Dot: 2 6 eggs from 3 geese
Anchors and word boundaries are not supported, and compile reports them
as errors.
#The rest18.10
A few more modules round out the library. base64 encodes and decodes
Base64 text. binary gives checked little-endian reads of binary file
formats. ffi copies strings and buffers that a C library owns into Goose
storage. exception you met in Chapter 15. Graphics, audio, user
interfaces and physics live in separate packages built on SDL3 and Box3D.
They are imported the same way, but fetched as packages, which is the
subject of the next chapter.
#Exercises18.11
-
Use a
dictionary<i64, i64>to count how many eggs of each weight appear in[61, 72, 61, 58, 72, 61], and print the most common weight. -
Write a program that writes three lines of CSV (
name,eggs) to a file, reads the file back withread_file, and prints the total number of eggs usingcsv::each_record. -
Use
stable_sortwith a block to sort geese by weight, keeping geese of equal weight in their original order. How can you tell it kept the order?