Documentation / Choices

Choices

A choice creates a nominal tagged union. Each value carries one named variant with an optional record payload, much like an enum with data in Rust.

Pattern matching opens payloads without nullable fields or unchecked casts. Equality remains available only when every possible payload type supports it.

Choose the explanation level: Choice declarations

Choice declarations

A choice defines a value that can take one of several named forms.

Status ::
    Ready,
    Waiting,
    Failed | message String |,
;

Ready and Waiting are unit variants.

Failed carries one payload field named message.

A choice is its own type. Another choice with similar variants is still a different type.

A choice forms a nominal tagged union.

Each value contains one declared variant.

Status ::
    Ready,
    Waiting,
    Failed | message String |,
;

Variant forms

A unit variant has no payload:

Ready

A payload variant uses a record body:

Failed | message String |

Contract

  • The declaration form is Name :: variants ;.
  • Choice and variant names use PascalCase.
  • A choice must declare at least one variant.
  • Variant names must be unique.
  • A payload record must contain at least one field.
  • Payload field names must be unique.
  • Payload fields require explicit types.
  • Payload fields are immutable.
  • Payload fields do not support defaults.
  • Mutable payload field declarations are invalid.
  • Shorthand payload declarations such as Failed String are invalid.
  • Constructor-style declarations such as Failed(String) are invalid.
  • Recursive choice declarations are deferred.
  • A choice has nominal identity. Matching variants and payload shapes do not make two choices interchangeable.

Generic choices

Choices may introduce declaration-site generic parameters.

Maybe type A ::
    Some | value A |,
    None,
;

Generic inference and instance restrictions are documented by Generics.

Choose the explanation level: Variant construction

Variant construction

Create a unit variant without parentheses:

current = Status::Ready

Create a payload variant with its values:

failure = Status::Failed("network")

Named arguments also work:

failure = Status::Failed(message = "network")

Payload values cannot be changed after construction. Match the choice to read them.

Construct choice values through the choice name and variant name.

Unit variants

A unit variant is a value without a constructor call.

current = Status::Ready

This form is invalid:

-- INVALID: unit variants are not constructor calls
current = Status::Ready()

Payload variants

A payload variant requires constructor parentheses.

failed = Status::Failed("network")
named = Status::Failed(message = "network")

Argument rules

  • Positional arguments fill payload fields in declaration order.
  • Positional arguments must come before named arguments.
  • A named argument must match a payload field.
  • Each payload field must be supplied exactly once.
  • Payload fields have no defaults.
  • Extra or missing arguments are invalid.
  • Argument values are checked and contextually coerced against field types.

Constructed payload fields are immutable.

Direct payload field mutation is invalid.

Use pattern matching to inspect payload fields.

Choose the explanation level: Payload patterns

Payload patterns

Match a payload variant to use its fields.

if status is:
    Ready => io.line("ready")
    Failed(message) => io.line(message)
;

The capture name matches the field name.

Use as when the local name should be clearer:

Failed(message as error_text) => io.line(error_text)

Payload values are immutable. Pattern matching reads them without exposing direct field mutation.

Pattern matching inspects choice payloads.

if status is:
    Ready => io.line("ready")
    Waiting => io.line("waiting")
    Failed(message) => io.line(message)
;

Capture contract

  • A payload pattern must include every declared field.
  • Captures stay in declaration order.
  • Each source capture name must exactly match its declared field name.
  • as local_name renames only the arm-local binding.
  • Duplicate local binding names are invalid.
  • Payload captures are immutable arm-local bindings.
  • A valid payload arm covers the complete variant regardless of payload values.
Response ::
    Pending |
        retry_count Int,
        message String,
    |,
    Complete,
;

if response is:
    Pending(retry_count, message as pending_message) =>
        io.line(pending_message)
    Complete => io.line("done")
;

Payload captures also work in inline and block single-predicate value matches:

message = if response is Failed(message) then message else "ok"

message = if response is Failed(message):
    then message
else
    then "ok"
;

Deferred and rejected forms

Direct payload field access is deferred:

message = response.message

Payload field mutation is rejected.

Wildcard payload captures are not supported.

Named assignment is not match syntax:

Failed(message = text) => io.line(text)

Nested payload patterns and nested exhaustiveness are deferred.

See Patterns and exhaustiveness for match-level rules.

Choose the explanation level: Choice equality

Choice equality

Choices can use is and is not when their payloads are equality-safe.

first = Status::Ready
second = Status::Ready

same = first is second

Two choice values are equal when they use the same variant and their payload values are equal.

A choice with a collection, struct or another unsupported payload cannot use structural equality. Pattern matching works.

Two values of the same choice type support structural equality when every possible payload field supports equality.

left = Status::Ready
right = Status::Ready

same = left is right
different = left is not right

Structural comparison

Equality compares:

  1. the active variant tag
  2. each payload field for that variant

Two different nominal choice types are never comparable, even when their variants have matching shapes.

A choice cannot be compared with a non-choice value.

Choices support equality and inequality. They do not support ordering operators.

Supported payload equality

  • Int
  • Float
  • Bool
  • Char
  • String
  • choices whose payloads all support equality
  • options whose inner type supports equality

The rule applies after generic choice parameters are replaced by concrete type arguments.

Unsupported payload equality

  • structs
  • collections
  • maps
  • functions
  • fallible result carriers
  • external opaque types

If any variant contains an unsupported payload field, equality for the whole choice type is rejected.

Payload matching remains available regardless of equality support.

Option payloads participate in choice equality when their inner type supports equality. The rule recurses through the option payload instead of treating the option as an unsupported constructed type. This applies to both some and none values inside an equality-safe choice payload.