Chapter 18

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:

text: the input, read once h o n k · h i s s · h o n k · f l a p ptr 4 3 key count ptr 4 1 key count ptr 4 1 key count the dictionary's keys are slices into the text: no word is ever copied
A dictionary keyed by slices stores views into text the program already holds.

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

  1. 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.

  2. Write a program that writes three lines of CSV (name,eggs) to a file, reads the file back with read_file, and prints the total number of eggs using csv::each_record.

  3. Use stable_sort with a block to sort geese by weight, keeping geese of equal weight in their original order. How can you tell it kept the order?