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 tour of Pepper

This chapter walks through the main features of Pepper by writing small programs, and ends with a word counter that reads a file and reports how often each word appears. It moves quickly and shows the common case of each feature; every section links to the chapter of the language guide that covers it fully.

Every program here is complete: save it to a file, compile it as shown in Getting started, and run it. Each one checks its own results with assert(condition, "message"), which stops the program with the message if the condition is false, so a program that runs to the end and returns 0 did what the comments say.

Variables and functions

# A function lists its parameters as `name: type` and gives its return
# type after the closing parenthesis.
square(x: int): int {
    return x * x;
}

# A parameter may have a default value, used when the caller leaves it out.
greet(name: String = "world"): String {
    return "Hello, " + name + "!";
}

# Adds up 1 + 2 + ... + n with a while loop.
sum_to(n: int): int {
    var total = 0;
    var i = 1;
    while i <= n {
        total += i;
        i = i + 1;
    }
    return total;
}

describe(n: int): String {
    if n > 100 {
        return "big";
    } else {
        if n > 10 {
            return "medium";
        }
    }
    return "small";
}

main(): int {
    var answer = 42;          # the type, int, comes from the value
    double ratio = 0.5;       # or write the type yourself
    var whole = (ratio * 10.0) as int;
    assert(whole == 5, "'as' converts between number types");

    assert(square(answer) == 1764, "square(42)");
    assert(greet() == "Hello, world!", "the default name");
    assert(greet("Pepper") == "Hello, Pepper!", "an explicit name");
    assert(sum_to(4) == 10, "1 + 2 + 3 + 4");

    assert(describe(answer) == "medium", "42 is medium");
    assert(describe(7) == "small", "7 is small");

    print(greet("Pepper"));
    print($"{sum_to(100)}");
    return 0;
}

A few things to notice:

  • var declares a variable whose type comes from its initial value: 42 is an int. You can also name the type yourself, as in double ratio = 0.5;. as converts between types, such as from double to int. See Types and variables.
  • A function is declared as name(parameters): return type, and each parameter as name: type. A parameter can have a default value, as name does in greet. A function that returns nothing leaves out the return type. See Functions.
  • if and while do not need parentheses around the condition, but the body always needs braces. Chain conditions with else if. See Control flow.
  • += updates a variable in place, and + joins two strings.
  • print writes one value’s text on its own line. Interpolate to combine values: print($"total: {sum_to(100)}").

Structs

A struct groups related data with the methods that act on it:

struct Point {
    var x: int = 0;
    var y: int = 0;

    # `this.x = 0` as a parameter stores the argument straight into the field.
    construct(this.x = 0, this.y = 0) {}

    length_squared(): int {
        return this.x * this.x + this.y * this.y;
    }

    move_by(dx: int, dy: int) {
        this.x = this.x + dx;
        this.y = this.y + dy;
    }
}

# An extension adds methods to a struct that is declared elsewhere.
extend Point {
    swapped(): Point {
        return Point(this.y, this.x);
    }
}

main(): int {
    var p = Point(3, 4);
    assert(p.length_squared() == 25, "3*3 + 4*4");

    var origin = Point();
    assert(origin.x == 0 && origin.y == 0, "defaults from the constructor");

    # Struct values are references: q and p name the same Point.
    var q = p;
    q.move_by(1, 1);
    assert(p.x == 4 && p.y == 5, "p sees the move made through q");

    var s = p.swapped();
    assert(s.x == 5 && s.y == 4, "swapped is a new Point");
    assert(p.x == 4, "p itself is unchanged");
    return 0;
}
  • Fields are declared like variables, with var name: type, and may have default values.
  • The constructor is named construct. A parameter written as this.x = 0 stores its argument straight into the field x, and = 0 makes it optional, so Point() and Point(3, 4) both work.
  • Methods are functions declared inside the struct. Inside a method, this is the object the method was called on.
  • extend Point { ... } adds methods to a struct from outside its declaration, even to a struct from the standard library.

Struct values are references. var q = p; does not copy the point: p and q refer to the same object, so moving q moves p. Pepper frees an object when the last reference to it goes away, so you never free memory yourself. See Structs and Memory and reference counting.

Collections

The standard library provides growable arrays, strings, and hash maps. You do not need to import them:

main(): int {
    # Array<T> is a growable list. int[] is another way to write Array<int>.
    var scores = [90, 72];
    scores.append(85);
    assert(scores.count == 3, "three scores");
    assert(scores[0] == 90, "indexing starts at 0");
    scores[1] = 75;
    assert(scores.contains(85), "contains");

    var total = 0;
    for (var score in scores) {
        total += score;
    }
    assert(total == 250, "90 + 75 + 85");

    # sort orders the elements by a comparison function.
    scores.sort((a, b) => a - b);
    assert(scores[0] == 75 && scores[2] == 90, "sorted ascending");

    int[] none = Array<int>();
    assert(none.is_empty, "a new array is empty");

    # String holds UTF-8 text.
    var name = "Pepper";
    var greeting = "Hello, " + name;
    greeting += "!";
    assert(greeting == "Hello, Pepper!", "+ and += join strings");
    assert(greeting.length == 14, "length counts characters");
    assert(greeting.starts_with("Hello"), "starts_with");
    assert(greeting.substr(7, 6) == "Pepper", "substr(start, count)");
    assert(greeting.find("Pepper") == 7, "find returns an offset, or -1");
    assert(greeting[0] == "H", "s[i] is a one-character String");

    # HashMap<K, V> maps keys to values.
    var ages = ["Ada": 36];
    ages.insert("Alan", 41);
    assert(ages.count == 2, "two entries");
    assert(ages["Ada"] == 36, "look up by key");
    assert(ages.contains("Alan"), "contains");
    assert(ages["Grace"] == 0, "a missing key reads as the default value");
    assert(!ages.contains("Grace"), "reading does not insert");
    ages.remove("Ada");
    assert(ages.count == 1, "removed");

    # HashSet<K> keeps each key once.
    var seen = HashSet<String>();
    seen.insert("apple");
    seen.insert("apple");
    assert(seen.count == 1, "duplicates are ignored");
    return 0;
}
  • [90, 72] and ["Ada": 36] are collection literals: a new array and a new map.
  • Array<T> is a list that grows as you append. int[] is a shorter way to write the type Array<int>. Indexing is checked: reading past the end stops the program with an error rather than reading stray memory. sort takes a function that compares two elements, such as the closure (a, b) => a - b.
  • String holds UTF-8 text. The length property counts characters (code points), and s[i] is the character at index i, as a one-character string. Strings compare by content with ==.
  • HashMap<K, V> maps keys to values. Reading a key that is not there gives the default value of V (0 for numbers) without adding it; use contains to tell the two apart. HashSet<K> keeps each key once.
  • for (var x in collection) visits each element of an array, or each character of a string, in order, and for (var i in 0..n) counts from 0 to n - 1. See Iteration.

Handling errors with Result

Pepper has no exceptions. A function that can fail returns a Result: either a value, or an error that explains what went wrong.

# Converts a string of decimal digits to an int, or explains why it cannot.
parse_number(text: String): Result<int> {
    if text.is_empty {
        return Result<int>.err(Error("expected a number, found nothing"));
    }
    var value = 0;
    for (var c in text) {
        var digit = "0123456789".find(c);
        if digit < 0 {
            return Result<int>.err(Error("'" + c + "' is not a digit"));
        }
        value = value * 10 + (digit as int);
    }
    return Result<int>.ok(value);
}

main(): int {
    var good = parse_number("2026");
    assert(good.is_ok(), "digits parse");
    assert(good.value() == 2026, "value() returns the value");

    var bad = parse_number("12x");
    assert(bad.is_err(), "x is not a digit");
    assert(bad.error().message == "'x' is not a digit", "the error explains why");
    print(bad.error().message);

    assert(parse_number("").ok_or(-1) == -1, "ok_or supplies a fallback");
    return 0;
}
  • Result<int> holds either an int or an Error. Build one with Result<int>.ok(value) or Result<int>.err(Error("message")).
  • is_ok() and is_err() tell you which one you have. value() and error() return what it holds; error().message is the error’s text.
  • value() returns the value, and stops the program if there is none. Use it only when an error would be a bug. ok_or(fallback) returns the value, or fallback if there is none.

See Error handling with Result.

Putting it together: a word counter

The last program combines everything above. It writes a short text file, reads it back, and counts how often each word appears. It is split into two files: counter.pep defines a WordCounter struct, and main.pep uses it.

counter.pep:

# Counts how often each word occurs in some text.
export struct WordCounter {
    var counts: HashMap<String, int>;
    var order: String[];    # each distinct word, in the order first seen
    var total: int;

    construct() {
        this.counts = HashMap<String, int>();
        this.order = Array<String>();
        this.total = 0;
    }

    add(word: String) {
        if !word.is_empty {
            if !this.counts.contains(word) {
                this.order.append(word);
            }
            this.counts[word] = this.counts[word] + 1;
            this.total = this.total + 1;
        }
    }

    # Splits `text` into words at spaces and counts each one. `add` skips
    # the empty pieces that runs of spaces leave.
    add_text(text: String) {
        for (var word in text.split(" ")) {
            this.add(word);
        }
    }

    count(word: String): int {
        return this.counts[word];
    }
}

main.pep:

import [File, Directory, Path] from io;
import "counter";

# Reads the file at `path` and counts its words.
count_file(path: Path): Result<WordCounter> {
    var text = File.read_to_string(path);
    if text.is_err() {
        return Result<WordCounter>.err(Error(text.error().message));
    }
    var counter = WordCounter();
    counter.add_text(text.value());
    return Result<WordCounter>.ok(counter);
}

main(): int {
    # Write a small file to read back.
    var dir = Directory.temp().join("pepper-word-count");
    Directory.create_all(dir);
    var path = dir.join("poem.txt");
    File.write_text(path, "the cat sat on the mat and the dog sat too");

    var result = count_file(path);
    assert(result.is_ok(), "the file we just wrote can be read");
    var counter = result.value();
    for (var word in counter.order) {
        print($"{word}: {counter.count(word)}");
    }
    assert(counter.total == 11, "eleven words");
    assert(counter.count("the") == 3, "'the' appears three times");
    assert(counter.count("sat") == 2, "'sat' appears twice");
    assert(counter.count("bird") == 0, "'bird' does not appear");

    var missing = count_file(dir.join("missing.txt"));
    assert(missing.is_err(), "a missing file is an error, not a crash");
    print(missing.error().message);

    Directory.remove_all(dir);
    return 0;
}

Compile it by passing the entry file; the compiler finds counter.pep from the import:

pepper build main.pep -o word-count
./word-count

It prints each word with its count, one word per line, and then the error message for the missing file (its exact text comes from the operating system):

the: 3
cat: 1
sat: 2
...
No such file or directory

What is new here:

  • Imports. import "counter"; makes the declarations that counter.pep marks with export available in main.pep. Anything not exported stays private to its file (or, in a module, to the module’s files). import [File, Directory, Path] from io; brings three names in from the standard library’s io module, which, unlike String or Array, must be imported. See Modules and imports and Visibility.
  • Files. Directory.temp() is the system’s temporary directory, and join adds a name to a Path; the name can be a plain String, which converts to a Path implicitly. File has helpers that read or write a whole file at once: File.write_text and File.read_to_string.
  • I/O errors are Results. File.read_to_string returns a Result whose error is an IoError, so reading a missing file is an ordinary error value, not a crash. count_file turns it into its own Result<WordCounter>.
  • Splitting strings. add_text breaks the text into words with split(" "), which returns an Array<String> of the pieces between the spaces (see String).

Where to go next

You have now seen most of what you need to write everyday Pepper code. The language guide covers each topic in depth, including features this tour skipped: generics, function types and closures, traits and extensions, atomics, threads, and calling C.