Everyday Types
Primitives
| Type | Values |
|---|---|
Int | 64-bit signed integers: 42, -3, 0xff, 1_000. Overflow traps |
F64 | Doubles: 3.14, 2.0 |
Bool | true, false |
Str | UTF-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.