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
| Python | SSPUR |
|---|---|
int, float | Int (64-bit, overflow traps), F64 |
str, bool, None | Str, Bool, and Opt[T] instead of None |
list, tuple | List[T] (immutable), (A, B) |
dict, set | Map[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 payloads | a sum type, type Shape = Circle{r: F64} | Dot |
match (3.10) | match, which must be exhaustive |
lambda x: x * 2 | x => x * 2, or _ * 2 inside a call |
| list comprehension | xs.filter(...).map(...) |
raise / try / except | raise / catch, with the error type in the signature as fail[E] |
assert in a test file | test name = expr next to the code |
hypothesis | sspur fuzz, driven by the function’s contract |
print | log, which is an effect: ! log |
open(path).read() | read_file(path), a Res under the fs effect |
pip install | sspur 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, notlen(x). Any function can be called as a method:x.f(a)isf(x, a).none,some(x),true,falseare lowercase.- Strings interpolate by default:
"{n} items". A literal{is\{. - There is no mutation of shared data.
var xis 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.