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