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:
vardeclares a variable whose type comes from its initial value:42is anint. You can also name the type yourself, as indouble ratio = 0.5;.asconverts between types, such as fromdoubletoint. See Types and variables.- A function is declared as
name(parameters): return type, and each parameter asname: type. A parameter can have a default value, asnamedoes ingreet. A function that returns nothing leaves out the return type. See Functions. ifandwhiledo not need parentheses around the condition, but the body always needs braces. Chain conditions withelse if. See Control flow.+=updates a variable in place, and+joins two strings.printwrites 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 asthis.x = 0stores its argument straight into the fieldx, and= 0makes it optional, soPoint()andPoint(3, 4)both work. - Methods are functions declared inside the struct. Inside a method,
thisis 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 youappend.int[]is a shorter way to write the typeArray<int>. Indexing is checked: reading past the end stops the program with an error rather than reading stray memory.sorttakes a function that compares two elements, such as the closure(a, b) => a - b.Stringholds UTF-8 text. Thelengthproperty counts characters (code points), ands[i]is the character at indexi, 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 ofV(0 for numbers) without adding it; usecontainsto 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, andfor (var i in 0..n)counts from 0 ton - 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 anintor anError. Build one withResult<int>.ok(value)orResult<int>.err(Error("message")).is_ok()andis_err()tell you which one you have.value()anderror()return what it holds;error().messageis 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, orfallbackif 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 thatcounter.pepmarks withexportavailable inmain.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’siomodule, which, unlikeStringorArray, must be imported. See Modules and imports and Visibility. - Files.
Directory.temp()is the system’s temporary directory, andjoinadds a name to aPath; the name can be a plainString, which converts to aPathimplicitly.Filehas helpers that read or write a whole file at once:File.write_textandFile.read_to_string. - I/O errors are Results.
File.read_to_stringreturns aResultwhose error is anIoError, so reading a missing file is an ordinary error value, not a crash.count_fileturns it into its ownResult<WordCounter>. - Splitting strings.
add_textbreaks the text into words withsplit(" "), which returns anArray<String>of the pieces between the spaces (seeString).
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.