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

Everyday Types

Primitives

TypeValues
Int64-bit signed integers: 42, -3, 0xff, 1_000. Overflow traps
F64Doubles: 3.14, 2.0
Booltrue, false
StrUTF-8 text: "hi {name}". \{ is a literal brace
Unit(), the value of something done only for its effect
fn describe(n: Int, ratio: F64 where _ >= 0.0 and _ <= 1.0, ok: Bool, label: Str) -> Str
= "{label}: {n} items, {(ratio * 100.0).round}% done, ok={ok}"

test prim = describe(3, 0.25, true, "build") == "build: 3 items, 25% done, ok=true"
test convert = 3.to_f64 / 2.0 == 1.5 and 2.7.floor == 2 and "42".to_int == some(42)

Conversions are always written out: n.to_f64, x.round, x.floor, s.to_int (which gives an Opt[Int], since the text may not be a number), and .str on any value. x.round traps for NaN, an infinity or a value outside Int’s range, so sspur check warns unless it can see that x is in range, as the refinement on ratio shows here; x.checked_round gives an Opt[Int] instead.

Lists and tuples

List[T] is an immutable list. Methods return new lists and never change the old one. xs[i] indexes it and traps when i is out of range. A tuple (A, B) groups a few values of different types; read it with t.0 or take it apart with (a, b) = t.

test lists = do
  xs = [3, 1, 2]
  xs.len == 3 and xs.sort == [1, 2, 3] and xs.push(4) == [3, 1, 2, 4] and xs[0] == 3

test tuples = do
  pair = ("pen", 3)
  (name, qty) = pair
  pair.0 == "pen" and name.len == qty

Absence and failure as values

There is no null. A value that may be missing is an Opt[T]: some(x) or none. A result that may be an error is a Res[T, E]: ok(x) or err(e). You get the value out with or(default), match, or ok_or, which turns a none into a raised error.

fn first_even(xs: List[Int]) -> Opt[Int]
= xs.find(_ % 2 == 0)

test opt = first_even([1, 4, 6]) == some(4) and first_even([1, 3]) == none and first_even([]).or(0) == 0
test res = json.decode[Int]("12") == ok(12) and json.decode[Int]("twelve").is_err

Library functions that can fail in an expected way, like s.to_int, read_file(path) or json.decode[T](s), return Opt or Res. Your own errors usually use raise instead, which Effects and Errors covers.

Refinements and newtypes

where adds a condition to a type. _ stands for the value. A refinement can go on a parameter, a record field or a type alias, and it is checked every time a value enters it. new makes a distinct type with the same representation: an Sku is a string inside, but a plain Str can’t be passed where an Sku is expected. Build one with Sku("a7") and unwrap it with .raw.

type Percent = Int where _ >= 0 and _ <= 100
type Sku = new Str

fn discount(price: Int where _ >= 0, pct: Percent) -> Int
= price - price * pct / 100

fn sku_label(s: Sku) -> Str
= "SKU-{s.raw}"

test alias = discount(200, 25) == 150 and sku_label(Sku("a7")) == "SKU-a7"

Passing a value that breaks a refinement stops the program and names the clause:

type Percent = Int where _ >= 0 and _ <= 100

fn discount(price: Int where _ >= 0, pct: Percent) -> Int
= price - price * pct / 100

fn main() -> Unit ! log
= log("{discount(200, 120)}")
$ sspur run refine.ssp
runtime error: contract violated: type Percent in parameter 'pct' of discount where _ >= 0 and _ <= 100 (value = 120)

sspur verify can often prove at compile time that a refinement always holds, and then native code skips the check. Contracts and Tests shows how.

More built-in types

Map[K, V] and Set[T] (ordered), HashMap and HashSet, Heap, Time and Duration, BigInt, Dec for money, Ratio and Regex are all built in, with nothing to import. Collections and Pipelines covers the collections, and the standard library reference lists every method.