Algebraic data types

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 | :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.

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 CHOICE declarations.
  • Imported .gs modules 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 reports GRID_ADT_IMPORT_CONFLICT.