Your first script

A script is a program that reads something, works something out and prints it. This one reads a text file and prints its ten most frequent words. It is sixty lines, and it uses most of what a day-to-day program needs: arguments, files, text, a map, a struct, a sort.

topo/code/wordfreq.nx
import std.fs

/// A word and how often it appeared.
struct Entry { count: u32, word: String }

/// The ten most frequent words in a text file.
fn main() -> !void {
    let args = os.args()
    let path = if args.len > 1 { args[1] } else { "topo/code/data/hike.txt" }
    let text = try fs.read(path)

    // count every word, lower-cased, letters only
    var counts = Map(String, u32).new()
    var total: u32 = 0
    for line in text[..].lines() {
        for raw in line.split(" ") {
            let word = clean(raw)
            if word.len == 0 { continue }
            let n = counts.get(word[..]) orelse 0
            counts.put(word, n + 1)
            total += 1
        }
    }

    // sort the words by count, then alphabetically
    var entries = List(Entry).new()
    for w in counts { entries.append(Entry{ .count = counts[w[..]].?, .word = w.clone() }) }
    sort_entries(&mut entries)

    println("{} words, {} distinct", .{total, counts.len})
    for e, i in entries {
        if i == 10 { break }
        println("{>3}  {}", .{e.count, e.word})
    }
}

/// Lower case, letters only.
fn clean(s: []u8) -> String {
    var out = String.new()
    for c in s {
        if c >= 'a' and c <= 'z' { out.push_byte(c) }
        if c >= 'A' and c <= 'Z' { out.push_byte(c + 32) }
    }
    return out
}

/// Insertion sort: highest count first, ties alphabetical.
fn sort_entries(xs: *mut List(Entry)) {
    var i: usize = 1
    while i < xs.len {
        var j = i
        while j > 0 and before(&xs[j], &xs[j - 1]) {
            let count = xs[j].count
            let word = xs[j].word.clone()
            xs[j].count = xs[j - 1].count
            xs[j].word = xs[j - 1].word.clone()
            xs[j - 1].count = count
            xs[j - 1].word = word
            j -= 1
        }
        i += 1
    }
}

fn before(a: *Entry, b: *Entry) -> bool {
    if a.count != b.count { return a.count > b.count }
    return a.word[..] < b.word[..]
}

Run it on the sample text that ships with this book, or on any file:

$ nx run topo/code/wordfreq.nx
$ nx run topo/code/wordfreq.nx -- notes.txt
topo/code/wordfreq.expected
61 words, 38 distinct
 11  the
  4  is
  3  pitch
  3  ridge
  2  a
  2  and
  2  at
  2  climb
  2  dawn
  2  second

Arguments for the program go after --, so that nx does not read them as its own.

Piece by piece#

import std.fs

The standard library is a set of modules written in Nexium and embedded in the compiler; import std.fs makes fs.read, fs.write, fs.list and the rest available under fs.. There is nothing to install. The builtins you used in the last chapter (println, format, the io, os, math namespaces) need no import at all.

fn main() -> !void {

!void says main may fail with an error. Inside such a function try hands any error to the caller, and if main itself fails, the program prints error: <name> and exits with code 1. Chapter 6 is about errors; for now, try fs.read(path) reads "read the file, and if that fails, stop here".

    let args = os.args()
    let path = if args.len > 1 { args[1] } else { "topo/code/data/hike.txt" }

os.args() is the command line; args[0] is the program. if is an expression, so it can pick the value of a let.

    var counts = Map(String, u32).new()

var declares a variable that can be reassigned or mutated; let does not. A Map is a hash map; this one goes from String to u32. List(T), String and Map(K, V) are the three owning containers, and the type parameters go in parentheses, not angle brackets.

    for line in text[..].lines() {
        for raw in line.split(" ") {

text is a String, an owning buffer; text[..] is a slice of it, a view of the bytes that does not own them. Most text operations are defined on the view type []u8, and a String becomes one with [..]. lines() and split() return lists of views into the same bytes: no copying happens until clean builds a fresh String for the lowered word.

            let n = counts.get(word[..]) orelse 0
            counts.put(word, n + 1)

get returns an optional, ?u32: the count, or null when the word is new. orelse gives the default. Then the word goes into the map, and this is the first sight of the rule the whole language turns on: put takes the key by value, so word is moved into the map. The name word is unusable after that line, and the compiler would refuse a later use of it. That is fine here, because the loop is done with it. When you need to keep a value you hand over, .clone() it. Chapter 8 makes this precise.

struct Entry { count: u32, word: String }

A struct is a value with named fields. Entry{ .count = 1, .word = w } makes one. The leading dots on field names are the same convention as .{...}: "a field of the thing being built".

fn sort_entries(xs: *mut List(Entry)) {

The parameter is a mutable pointer to a list. Functions borrow their parameters: a plain List(Entry) parameter lets the function read the list and nothing more, and the caller keeps it; *mut lets sort_entries change it in place, and the caller still keeps it. (A function that wants to keep the value says own; chapter 8.)

Slices of ordered types sort with one call, xs[..].sort(), but a struct is not ordered until you say how, so this program sorts by hand with an insertion sort. Small programs do that; chapter 9 shows the standard library's helpers.

        println("{>3}  {}", .{e.count, e.word})

{>3} right-aligns the value in three columns. The format language has a handful of these: {:.2} for two decimals, {x} for hexadecimal, {<8} for left alignment. Arguments can be named, .{ .word = w, .cols = 3 }, and then the placeholders name them, {word:>cols}, with the width coming from an argument. They are all in the reference.

What you have#

A program that reads its arguments, reads a file, walks its text, counts with a map, sorts a list of structs and prints a table, with no allocation you did not ask for and no library outside the compiler. The next chapter puts the same language at a prompt.

Next: the REPL.

Exercises#

The compiler grades these: nx topo 3 writes them into a directory, nx topo check runs them. Or do them here: the code is editable, Run runs it with the compiler in the page (it loads on the first run and never sends your code anywhere), Check grades it as nx topo check would, and Reset puts the exercise back. Ctrl+Enter runs.

fix_count_wordsfix the error

The compiler rejects this program; make it build and print the recorded output.

fix_count_words.nx
fn main() {
    let count = 0
    for word in "one two three".split(" ") {
        if word.len > 0 { count += 1 }
    }
    println("{} words", .{count})
}
The recorded output
3 words
Hints
  • a binding that changes is declared with a different keyword
Solution
fn main() {
    var count = 0
    for word in "one two three".split(" ") {
        if word.len > 0 { count += 1 }
    }
    println("{} words", .{count})
}

write_initialswrite it

Fill in initials so the tests pass: the first byte of every word, upper-cased.

write_initials.nx
fn initials(name: []u8) -> String {
    ???
}

fn main() { println("{}", .{initials("Chris Bonington")}) }

test "two names" { expect_eq(initials("Chris Bonington")[..], "CB") }
test "one name" { expect_eq(initials("Reinhold")[..], "R") }
test "lower case" { expect_eq(initials("don whillans")[..], "DW") }
test "extra spaces" { expect_eq(initials("Doug  Scott")[..], "DS") }
The recorded output
CB
Hints
  • for word in name.split(" ") walks the words; word[0] is a u8
  • out.push_byte(b) appends one byte; lower-case letters are 32 above upper-case
Solution
fn initials(name: []u8) -> String {
    var out = String.new()
    for word in name.split(" ") {
        if word.len == 0 { continue }
        let c = word[0]
        out.push_byte(if c >= 'a' and c <= 'z' { c - 32 } else { c })
    }
    return out
}

fn main() { println("{}", .{initials("Chris Bonington")}) }

test "two names" { expect_eq(initials("Chris Bonington")[..], "CB") }
test "one name" { expect_eq(initials("Reinhold")[..], "R") }
test "lower case" { expect_eq(initials("don whillans")[..], "DW") }
test "extra spaces" { expect_eq(initials("Doug  Scott")[..], "DS") }