Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A first program: counting words

Before studying the language piece by piece, it helps to see a whole program that does something useful. This one reads text from standard input and prints the words that occur most often. It is sixty lines long and uses a struct, a hash map, a vector, text handling, error handling, references and a sorting function. You are not expected to understand every line yet. Read it for its shape, then read the explanation, and come back to it from later chapters when the details have names.

The program

import std;

// One row of the final table: a word and how often it appeared.
struct Count
{
    String word;
    u32 n;
}

// The order of the table: most frequent first, then alphabetical.
fn more_frequent(ref<Count, shared> a, ref<Count, shared> b) : bool
{
    a.n > b.n || (a.n == b.n && a.word < b.word)
}

// Reads every line of standard input and counts its words.
fn count_words(ref<HashMap<String, u32>, exclusive> counts) : Result<usize, FileError>
{
    usize lines = 0;
    while (Some(line) = read_line()?)
    {
        lines += 1;
        foreach (word in String::split_whitespace(&line))
        {
            String key = String::to_ascii_lower(&String::from_view(word));
            *HashMap::entry(counts, key, 0) += 1;
        }
    }
    Ok(lines)
}

fn main() : u8
{
    HashMap<String, u32> counts = HashMap::new();
    usize lines = match (count_words(&mut counts))
    {
        Ok(n)  : n,
        Err(e) :
        {
            eprintf("wordcount: %s\n", FileError::text(&e));
            return 1;
        },
    };

    Vec<Count> table = Vec::new();
    foreach (word, n in &counts)
    {
        Vec::push(&mut table, Count { .word = String::clone(word), .n = *n });
    }
    Vec::sort_by(&mut table, more_frequent);

    printf("%v lines, %v distinct words\n", lines, Vec::len(&table));
    foreach (i, c in &table)
    {
        if (i == 5)
        {
            break;
        }
        printf("%5d %s\n", c.n, &c.word);
    }
    0
}

Given this input:

The quick brown fox jumps over the lazy dog.
The dog sleeps; the fox does not.
A fox is quick and a dog is lazy, or so the story goes.

it prints:

3 lines, 20 distinct words
    5 the
    3 fox
    2 a
    2 dog
    2 is

Run it yourself with coby wordcount.cb < some-text-file, or compile it and pipe anything you like into the executable.

A struct and a function of two references

The program begins by declaring a type for one row of its output:

struct Count
{
    String word;
    u32 n;
}

A struct is a group of named fields, each with a type. String is owned text, the kind you build at run time; u32 is an unsigned 32-bit integer. Nothing about this struct is special, except that because it contains a String, it owns something, and the language will treat it accordingly: a Count is moved from place to place, never silently copied. Chapter 6 is about that.

Then a function that says which of two rows comes first:

fn more_frequent(ref<Count, shared> a, ref<Count, shared> b) : bool
{
    a.n > b.n || (a.n == b.n && a.word < b.word)
}

The return type comes after a colon. The function’s body is a single expression with no return and no semicolon: in CobaltC, the last expression of a block is the block’s value, and a function’s body is a block. You will write return only to leave early.

The parameters are references. ref<Count, shared> means “a way to reach a Count that someone else owns, for reading only”. The function could have taken two Count values instead, but then each call would have had to hand over its rows, and the caller wants to keep them. Taking a shared reference says, in the signature, that the function looks and does not touch. Field access works through a reference without any extra syntax: a.n is the n of the row a refers to.

Comparing two Strings with < compares their text, byte by byte.

Reading input, and the first error

The next function does the counting:

fn count_words(ref<HashMap<String, u32>, exclusive> counts) : Result<usize, FileError>
{
    usize lines = 0;
    while (Some(line) = read_line()?)
    {
        lines += 1;
        foreach (word in String::split_whitespace(&line))
        {
            String key = String::to_ascii_lower(&String::from_view(word));
            *HashMap::entry(counts, key, 0) += 1;
        }
    }
    Ok(lines)
}

This time the parameter is ref<HashMap<String, u32>, exclusive>: a reference through which the map may be changed. The two kinds of reference are the whole story of chapter 7, and the short version is that any number of shared references may exist at once, but an exclusive one must be alone.

The return type, Result<usize, FileError>, says the function either succeeds with a count of lines or fails with a file error. Reading input can fail, and CobaltC has no exceptions, so a function that can fail says so in its type, and its caller has to deal with it. The last line, Ok(lines), is the success case.

The loop’s condition deserves a slow look. read_line() returns the next line as Ok(Some(line)), the end of input as Ok(None), or a failure as Err(e). The ? after the call takes the Ok apart: if the result is an error, ? returns that error from count_words at once, and the caller gets it; otherwise the expression’s value is the Option inside. Then Some(line) = ... is a pattern: when the option holds a line, the loop body runs with line bound to it, and when it is None the loop ends. One line expresses “for each line until the input ends, and give up on an error”, and nothing is silently ignored.

Inside, String::split_whitespace(&line) gives the words of the line as a vector of views: pieces of the line’s own text, not copies. The loop foreach (word in ...) visits each one. Then two calls turn a view into a lower-case String of its own, and the last line does the counting:

*HashMap::entry(counts, key, 0) += 1;

HashMap::entry finds the value stored under key, inserting 0 first if the key is new, and returns an exclusive reference to it. The * reaches the value the reference points at, and += 1 adds one. Library functions are called by their qualified name, HashMap::entry, String::from_view, always; CobaltC has no method-call syntax, so you can always see which function runs and what it is given.

Handling the result

In main, the call to count_words can fail, so its result is taken apart with match:

usize lines = match (count_words(&mut counts))
{
    Ok(n)  : n,
    Err(e) :
    {
        eprintf("wordcount: %s\n", FileError::text(&e));
        return 1;
    },
};

&mut counts lends the map to the function exclusively for the duration of the call. A match lists one arm per case, each pattern : expression,, and must cover every case; forgetting one is an error before the program runs. Because match is an expression, the whole thing is the initialiser of lines: the Ok arm gives the count, and the Err arm prints a message to standard error and returns from main with exit status 1. main is declared : u8 precisely so that it can return an exit status; the 0 at its end is the success status.

From a map to a sorted table

A hash map keeps its entries in the order they were added, which is not the order we want to print them in, so the program copies the entries into a vector and sorts that:

Vec<Count> table = Vec::new();
foreach (word, n in &counts)
{
    Vec::push(&mut table, Count { .word = String::clone(word), .n = *n });
}
Vec::sort_by(&mut table, more_frequent);

Looping over &counts borrows the map for reading and gives each key and value as references. A struct is built with a literal that names every field, Count { .word = ..., .n = ... }. The key is copied with String::clone because the map still owns the original and a String is never copied without being asked; *n reads the number through its reference. Vec::sort_by takes the function written earlier and sorts the rows by it. A function is a value you can pass like any other.

The printing loop asks for the position as well as the element, foreach (i, c in &table), so that it can stop after five rows. %5d is C’s: the number right-aligned in five columns. A String is printed through a reference, &c.word, which is the language’s way of saying that printf reads it and does not take it.

What you have seen

In sixty lines: a struct, two kinds of reference, a function that returns an error as a value and a ? that passes one up, a pattern in a loop condition, a match that must be complete, a hash map, a vector, owned text and views into it, a function passed as a value, and formatted output. Every one of these has a chapter of its own.

One flaw is worth noticing before moving on. The input’s dog. and dog were counted as different words, because the program splits only at spaces. Chapter 10 has the text functions to fix that, and it is a good first exercise once you get there.