Documentation / Structs

Structs

Structs give data a name, a shape and a clear owner. Matching fields don't make two types interchangeable.

Methods stay explicit. They bind to their type as top-level functions, with shared or mutable access visible in the receiver.

Choose the explanation level: Struct declarations

Struct declarations

A struct groups named values into one new type.


    Player = |
        name String,
        score Int,
    |
    

Each field has a name and a type.

Fields can have defaults:


    Player = |
        name String,
        score Int = 0,
    |
    

Player is its own type. Another struct with the same fields is still a different type.

A struct declares a nominal runtime type with named fields.


    Vector2 = |
        x Float,
        y Float = 0.0,
    |
    

Contract

  • The declaration form is Name = | fields |.
  • Struct names use PascalCase.
  • Every field has a name and explicit type.
  • Field names must be unique.
  • A field may provide = default.
  • Every field default must fully evaluate at compile time.
  • A field without a default is required during construction.
  • Matching field shapes do not make two structs interchangeable.
  • A transparent type alias does not create a new struct identity.

    Point = |
        x Int,
        y Int,
    |

    Position = |
        x Int,
        y Int,
    |
    

Point and Position remain different types.

Generic structs

Struct declarations may introduce declaration-site generic parameters.


    Box type A = |
        value A,
    |
    

Generic construction and inference are documented by Generics.

Choose the explanation level: Construction and fields

Construction and fields

Create a struct by calling its type name.


    Player = |
        name String,
        score Int = 0,
    |

    player = Player("Priya")
    

The constructor uses the default score.

Named fields make larger constructors easier to read:


    player = Player(
        name = "Priya",
        score = 10
    )
    

Read fields with a dot:


    name = player.name
    

To change a field, the struct binding must be mutable:


    player ~= Player("Priya")
    player.score = 10
    

A struct constructor uses the struct name and supplies its fields.


    Point = |
        x Int,
        y Int = 0,
    |

    first = Point(10)
    second = Point(x = 10, y = 20)
    

Constructor routing

  • Positional arguments fill fields in declaration order.
  • Positional arguments must come before named arguments.
  • A named argument must match a real field.
  • Each field may be supplied once.
  • Omitted fields use their declared defaults.
  • Every field without a default is required.
  • Extra arguments are invalid.
  • Values are checked and contextually coerced against field types.

A constructor creates a fresh value.

Field reads

Use . to read a field.


    point = Point(10, 20)
    horizontal = point.x
    

A field read uses normal shared access.

Field writes

The root value must be a mutable place.


    point ~= Point(10, 20)
    point.x = 30
    

A field of an immutable binding cannot be assigned.

Field mutability is controlled by the root place and the current access. It is not a separate field type.

Const records use struct-shaped compile-time data but have additional restrictions owned by Constants and compile-time behaviour.

Choose the explanation level: Receiver methods

Receiver methods

A receiver method is a function that lives inside a struct.

You create a struct, then create a function that can only be called from instances of those structs.

Its first parameter is named this:


    Vector2 = |
        x Float,
        y Float,
    |

    length_squared |this Vector2| -> Float:
        return this.x * this.x + this.y * this.y
    ;

    vector = Vector2(3.0, 4.0)
    length = vector.length_squared()
    

The method is written as a normal top-level function in the same file as the type.

Call it with value.method(), not method(value).

Methods exist as a helpful to organise code. They belong to the struct, so can only be called from instances of that type and are passed around with that type.

A receiver method is a top-level function whose first parameter is named this.


    Vector2 = |
        x Float,
        y Float,
    |

    length_squared |this Vector2| -> Float:
        return this.x * this.x + this.y * this.y
    ;

    vector = Vector2(3.0, 4.0)
    length = vector.length_squared()
    

Receiver declaration

  • this must be the first parameter.
  • A signature may contain exactly one this parameter.
  • this T is a shared receiver.
  • this ~T is a mutable receiver.
  • this is reserved for receiver declarations and the receiver method body.
  • A receiver method is called only with receiver syntax.
  • Calling it as method(value, ...) is invalid.

Supported receiver ownership

A source-authored receiver method belongs to a user-defined struct or choice declared in the same source file.

Aligned declaration-site generic nominal receivers are supported when the method belongs to that generic type declaration.

Source-authored receiver methods are rejected for:

  • builtin scalars
  • imported source types
  • dependency package types
  • external opaque types
  • nominal types declared in another file

Use a free function for a type owned elsewhere.

Compiler-owned collection operations and builder-owned builtin operations are not user extension methods.

Constants and receiver methods

A struct instance folded into a constant becomes a data-only const record. Its folded fields remain available under the constant rules, but runtime receiver methods are not available on the const record itself.

Use one projected field when runtime code needs the value. See Const records for the const-record contract.

Visibility

Receiver-method visibility follows receiver-type visibility.

  • A method becomes callable wherever its receiver type is visible.
  • Methods are not imported, aliased or re-exported independently.
  • Namespace imports may expose the receiver type, but its methods are not namespace fields.
  • A module public surface exposes same-file receiver methods when it exposes the receiver type.
  • A field and receiver method on the same type cannot use the same name.
Choose the explanation level: Mutable receivers

Mutable receivers

A method that changes its receiver uses this ~Type.


    reset |this ~Vector2|:
        this.x = 0.0
        this.y = 0.0
    ;
    

The value must be stored in a mutable binding:


    vector ~= Vector2(3.0, 4.0)
    ~vector.reset()
    

The first ~ makes the binding mutable. The second requests exclusive access for this method call.

A shared method is called without ~.

A mutable receiver method declares this ~Type.


    Vector2 = |
        x Float,
        y Float,
    |

    reset |this ~Vector2|:
        this.x = 0.0
        this.y = 0.0
    ;

    vector ~= Vector2(3.0, 4.0)
    ~vector.reset()
    

Contract

  • The receiver root must be a mutable place.
  • The call uses ~receiver.method(...).
  • Omitting ~ for a mutable receiver method is invalid.
  • Adding ~ to a shared receiver method is invalid.
  • A temporary or computed rvalue cannot receive a mutable receiver call.
  • Field writes inside the method are permitted through mutable this.
  • ~ requests exclusive access for the call. It is not move syntax and is not part of type identity.

This differs from a fresh rvalue passed to a mutable free-function parameter. The compiler may materialize that free-function argument into a hidden local. A mutable receiver call still requires an authored mutable place.