Skip to content
Playground

Structs & Roles ​

This page covers nominal struct types, companion methods defined with with:, roles and implementations, and where constraints on type variables.

Structs ​

yulang
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:

yulang
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:

yulang
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:

yulang
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:

yulang
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: ​

yulang
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:

yulang
role Add 'a:
    our a.add: 'a -> 'a

role Eq 'a:
    our a.eq: 'a -> bool

Each 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:

yulang
role Index 'container 'key:
    type value
    our container.index: 'key -> value

impl ​

Implement a role for a concrete type:

yulang
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 i

The 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:

yulang
struct box 'a { value: 'a } with:
    impl Index int:
        type value = 'a
        our b.index i = b.value

Constraints with where ​

Constrain a function's type variables with a where clause inside the body:

yulang
my twice(x: 'a) =
    where 'a: Add
    x.add x

where 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.

Yulang