Records and Sum Types
Records
A record type lists named fields. Build a value with the type name and every field; {name} is short for {name: name}. Values never change in place: with makes a copy with some fields replaced, and it can reach into nested records (address.city) and lists (tags[0]).
type Address = {city: Str, zip: Str}
type User = {name: Str, age: Int where _ >= 0, address: Address, tags: List[Str]}
fn birthday(u: User) -> User
= u with age := u.age + 1
fn move_to(u: User, city: Str) -> User
= u with address.city := city, tags[0] := "moved"
test records = do
home = Address{city: "Pune", zip: "411001"}
name = "Asha"
u = User{name, age: 30, address: home, tags: ["new"]}
birthday(u).age == 31 and move_to(u, "Delhi").address.city == "Delhi" and u.address.city == "Pune"
move_to returns a new user, and u still lives in Pune. A with update re-checks refinements, so u with age := -1 traps just like building a user with a negative age.
Sum types
A sum type is a choice between variants. Each variant can carry its own fields or none. Variant names are capitalized and unique across the program, so Dot always means Shape’s Dot.
type Shape = Circle{r: F64} | Rect{w: F64, h: F64} | Dot
type Status = Active | Suspended{reason: Str}
fn area(s: Shape) -> F64
= match s
| Circle{r} => 3.14 * r * r
| Rect{w, h} => w * h
| Dot => 0.0
test sums = area(Rect{w: 2.0, h: 3.0}) == 6.0 and area(Dot) == 0.0
match takes a value apart; Pattern Matching is about it. A sum type with one variant and no fields, like type ParseErr = Bad, is a common error type.
Generic types
Types can take type parameters in square brackets. A recursive sum type like Tree[T] is the usual way to build trees:
type Pair[A, B] = {first: A, second: B}
type Tree[T] = Leaf | Node{left: Tree[T], value: T, right: Tree[T]}
fn insert(t: Tree[Int], x: Int) -> Tree[Int]
= match t
| Leaf => Node{left: Leaf, value: x, right: Leaf}
| Node{left, value, right} => if x < value then Node{left: insert(left, x), value, right} else Node{left, value, right: insert(right, x)}
fn size[T](t: Tree[T]) -> Int
= match t
| Leaf => 0
| Node{left, right} => 1 + size(left) + size(right)
test generic = size([5, 2, 8].fold(Leaf, insert)) == 3 and Pair{first: 1, second: "one"}.second == "one"
insert is passed to fold by name, as in the previous page.
Printing
Every value prints as SSPUR source. That makes logs and test failures easy to read and to paste back into code:
fn main() -> Unit ! log
= do
log("{Rect{w: 1.0, h: 2.0}}")
log("{Suspended{reason: "late fees"}}")
log("{Pair{first: 1, second: [true]}}")
$ sspur run records.ssp
Rect{w: 1.0, h: 2.0}
Suspended{reason: "late fees"}
Pair{first: 1, second: [true]}
== compares any two values of the same type field by field. To sort records or give them a custom printed form, derive or implement Ord and Show (Generics and Traits). Next: Pattern Matching.