Algebraic Choices
Payload-bearing CHOICE declarations model a value that is exactly one of a closed set of constructors:
Algebraic Choices
Payload-bearing CHOICE declarations model a value that is exactly one of a
closed set of constructors:
CHOICE Delivery =
Pending |
Delivered(at: date) |
Failed(message: string)
delivery = Delivery.Delivered(dt"2026-07-27")
label = MATCH(
delivery,
Delivery.Pending() -> "pending",
Delivery.Delivered(at) -> "delivered " & TEXT(at),
Delivery.Failed(message) -> "failed: " & message
)
This extends, rather than replaces, symbol choices:
CHOICE health = :green | :amber | :red
CHOICES permissions = :read | :write
Colon-prefixed members keep their existing enum or bit-flag semantics. A declaration with named constructors creates an algebraic choice.
Constructors And Fields
A constructor is always qualified as Type.Constructor(...). Field order is
the constructor's argument order, while field names are used by MATCH
bindings and diagnostics.
CHOICE Result = Accepted(value: number) | Rejected(reason: string)
good = Result.Accepted(42)
bad = Result.Rejected("outside policy")
Constructor arity is checked at compile time. Field contracts are checked when
the value is constructed and fail closed with an ADT/FIELD_TYPE error. The
value is immutable.
Type and constructor names are case-insensitive. Field bindings are ordinary lexical names and remain case-preserving.
Exhaustive Matching
Constructor patterns bind payload fields positionally:
answer = MATCH(
response,
Result.Accepted(value) -> value,
Result.Rejected(reason) -> "rejected: " & reason
)
Each non-_ binding name may appear only once in an arm, case-insensitively.
For example, Pair.Pair(value, VALUE) is rejected instead of silently
shadowing one payload field.
Use _ to ignore one payload field or as a final catch-all arm. Patterns may
contain bindings and _, not literal tests; put a value test in an IF guard:
priority = MATCH(
response,
Result.Accepted(value) IF value > 100 -> "large",
Result.Accepted(_) -> "ordinary",
Result.Rejected(_) -> "rejected"
)
A constructor match must cover every constructor with an unguarded arm, or
include an unguarded _ catch-all. Missing constructors are compile errors.
Constructors from different algebraic types cannot be mixed in one match.
SOME/NONE and OK/ERR retain their native Option/Result behavior and
constructor-pattern syntax.
Recursive Values
An algebraic choice may refer to itself in a field:
CHOICE Tree =
Leaf(value: number) |
Branch(left: Tree, right: Tree)
tree = Tree.Branch(Tree.Leaf(1), Tree.Leaf(2))
Recursive values must still be finite. Grid enforces this in three places:
- workbook dependencies must remain acyclic;
- named definitions do not gain unrestricted recursion from an algebraic declaration;
- nested algebraic values have a fixed maximum depth of 64, including when an intermediate array or native collection contains the next value.
This admits finite trees and lists without introducing an unbounded evaluator
or nontermination into recalculation. Algebraic values are sealed: generic
RECORD_SET cannot rewrite their payload or constructor metadata.
Generic Choices
A choice can take type parameters, so one declaration describes a family of types:
CHOICE Maybe<T> = None | Some(value: T)
CHOICE Result<T, E> =
Ok(value: T) |
Err(error: E)
CHOICE Tree<T> =
Leaf(value: T) |
Branch(left: Tree<T>, right: Tree<T>)
Parameters are type names, distinct within the declaration and case-insensitive,
up to 16 per choice. A declaration without parameters, such as CHOICE Delivery = ... above, is simply a choice with none.
Constructors infer their type arguments from their payload:
leaf = Tree.Leaf(42)
reply = Result.Ok("approved")
When nothing in the payload or its use fixes the arguments, name them explicitly:
# No payload to infer from, so the family states its argument.
empty = Maybe<number>.None()
Matching is by constructor, exactly as for ordinary choices. A pattern may also name the family's arguments; that is checked when the model compiles and does not change which arm runs:
size = MATCH(
leaf,
Tree.Leaf(item) -> 1,
Tree.Branch(_, _) -> 2
)
Payload types follow a value through LET, references, function calls and
recursive fields. At run time a value keeps its family, constructor and field
names, but not its type arguments, so TYPE and equality do not tell
Maybe<number> and Maybe<string> apart.
Type arguments must be complete: partial applications, _ placeholders and
parameters that take parameters are not supported, and two choices cannot
refer to each other recursively. Field types are limited to portable scalars,
collections and previously declared choices; date fields and unit or
refinement tags on a generic field report a diagnostic.
Choices From Installed Packages
An installed data-only package can provide generic choices. Its types keep
their identity when imported under another name with AS or EXPOSING, so
values built from them match the same patterns. A package can also export a
choice for matching only; models can match its values but cannot construct
them.
Reactivity And Provenance
Constructors and matches lower to ordinary dependency-bearing Workbook
operations. Payload references remain visible dependency operands, and each
selected arm reads the subject through the normal formula graph. Therefore an
input edit recomputes both the constructed value and its match result, and
WHY traces through the match, constructor, and original payload bindings.
There is no opaque serialized object or side-channel evaluator.
Limits
- A type may declare at most 256 constructors.
- A constructor may declare at most 64 fields.
- Declarations must appear before their constructors are used, as with
existing
CHOICEdeclarations. - Imported
.gsmodules may export algebraic choices; their type names share the existing flat choice namespace. Same-name imports coalesce only when their closed schemas are identical: type, constructor, field, and contract names compare case-insensitively, constructor order is irrelevant, and payload field order remains significant. An incompatible same-name schema reportsGRID_ADT_IMPORT_CONFLICT.