Algebraic Choices
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 | :writeColon-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.
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.