Structs & Roles
This page covers nominal struct types, companion methods defined with with:, roles and implementations, and where constraints on type variables.
Structs
struct point { x: int, y: int }Structs are nominal record types. Construct one with point { x: 3, y: 4 }.
Tuple structs
Tuple structs are nominal products with positional fields:
struct user_id(int)
struct point(int, int)
my unwrap value = case value:
user_id(id) -> id
(unwrap(user_id(7)), point(3, 4))A one-field tuple struct still has a constructor and a matching constructor pattern. Parentheses do not collapse user_id(int) into its payload type.
Type parameters
Structs can have type parameters:
struct pair 'a 'b { fst: 'a, snd: 'b }
struct box 'a { value: 'a }Record payloads on variants
An enum variant can give its payload named fields. Construction and matching use the same record shape:
enum event:
moved { x: int, y: int }
named { name: str }
my coordinates value = case value:
event::moved { x, y } -> (x, y)
event::named { name } -> (name.len, 0)
coordinates(event::moved { x: 3, y: 4 })The parser also accepts named record payloads on error variants, but the checker currently rejects that declaration as unsupported syntax. Record payloads are therefore available on enum variants, not on error variants.
Structural projections
value.(...) builds a tuple by selecting several members from value. value.{...} builds a record and can rename those selections:
my source = { x: 3, y: \n -> n + 1 }
source.(x, y(4)) // (3, 5)
source.{ first: x, next: y(8) } // {first: 3, next: 9}Each expression inside the projection is evaluated relative to the source. The projection can select a field directly or call a selected function.
Methods via with:
struct point { x: int, y: int } with:
our p.norm2 = p.x * p.x + p.y * p.y
our p.scale n = point { x: p.x * n, y: p.y * n }with: attaches definitions to the struct's companion module. Methods use a receiver name — here p — that stands for the value being called on. Receiver style bindings are registered as methods; examples conventionally use our when the method should be visible from outside the companion body.
The same with: machinery also works for type declarations, so standard types such as list, str, and ref can define methods in their companion modules.
Roles
A role is an interface — a set of methods (and optionally associated types) that a type can implement. Roles are declared with the role name first, then the type variable they parameterize:
role Add 'a:
our a.add: 'a -> 'a
role Eq 'a:
our a.eq: 'a -> boolEach method header our a.method: <type> uses a receiver name (a) for the implementing type.
A role may also declare an associated type and span multiple type parameters:
role Index 'container 'key:
type value
our container.index: 'key -> valueimpl
Implement a role for a concrete type:
impl Add int:
our x.add y = std::int::add x y
impl Index str int:
type value = char
our s.index i = std::text::str::index_raw s iThe first type after the role name fills the role's first type variable; further types fill the rest.
A struct can attach an impl inside its with: block. The enclosing struct is prepended as the role's first type argument; any role arguments written after the role name fill the remaining role inputs:
struct box 'a { value: 'a } with:
impl Index int:
type value = 'a
our b.index i = b.valueConstraints with where
Constrain a function's type variables with a where clause inside the body:
my twice(x: 'a) =
where 'a: Add
x.add xwhere clauses also work inside role bodies and impl bodies. In a role body, the constraint is inherited by the role methods. In an impl body, the constraint becomes a prerequisite for that impl candidate.