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

SSPUR for Python programmers

SSPUR reads a lot like Python: indentation for blocks, short keywords, for x in xs. The difference is that everything is checked before it runs. Types are static and inferred inside functions, values are immutable, and a function’s side effects and errors are written in its signature.

What maps to what

PythonSSPUR
int, floatInt (64-bit, overflow traps), F64
str, bool, NoneStr, Bool, and Opt[T] instead of None
list, tupleList[T] (immutable), (A, B)
dict, setMap[K, V], Set[T] (ordered, immutable), HashMap, HashSet
@dataclass(frozen=True)a record type, type Item = {sku: Str, qty: Int}
dataclasses.replace(it, qty=3)it with qty := 3
Enum with payloadsa sum type, type Shape = Circle{r: F64} | Dot
match (3.10)match, which must be exhaustive
lambda x: x * 2x => x * 2, or _ * 2 inside a call
list comprehensionxs.filter(...).map(...)
raise / try / exceptraise / catch, with the error type in the signature as fail[E]
assert in a test filetest name = expr next to the code
hypothesissspur fuzz, driven by the function’s contract
printlog, which is an effect: ! log
open(path).read()read_file(path), a Res under the fs effect
pip installsspur add with a lock pinned by hash

A word count

import re
from collections import Counter

def top_words(text: str, n: int) -> list[tuple[str, int]]:
    counts = Counter(re.findall(r"\w+", text.lower()))
    return sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:n]
fn top_words(text: Str, n: Int) -> List[(Str, Int)]
= text.lower.words.counts.sort_by((-_.1, _.0)).take(n)

Data classes with validation

Pydantic-style validation is part of the type. where conditions are checked whenever a value is built, and sspur verify can prove many of them at compile time:

type Item = {sku: Str, qty: Int where _ > 0, cents: Int where _ >= 0}

fn order_total(items: List[Item]) -> Int
= items.map(_.qty * _.cents).sum

fn restock(it: Item, extra: Int where _ > 0) -> Item
= it with qty := it.qty + extra

Files and errors

Anything that touches the outside world returns a Res and needs its effect in the signature, so first_line says ! fs:

fn first_line(path: Str) -> Str ! fs
= match read_file(path)
  | ok(text) => text.split("\n").first.or("")
  | err(e) => "error: {e}"
$ cat words.txt
The cat and the hat
and the bat sat on the mat
the end
$ sspur run from-python.ssp < words.txt
the 5
and 2
bat 1
error: missing.txt: not found
$ sspur test from-python.ssp
1 passed, 0 failed

Things that will surprise you

  • Every function declares its parameter and return types. Inside a body, types are inferred.
  • x.len, not len(x). Any function can be called as a method: x.f(a) is f(x, a).
  • none, some(x), true, false are lowercase.
  • Strings interpolate by default: "{n} items". A literal { is \{.
  • There is no mutation of shared data. var x is a local you can reassign with :=; lists and records are copied with changes.
  • It compiles to native code. The benchmarks compare it with C++, not Python.

The handbook covers all of this in order.