Browse documentation
Docs/Language

Algebraic Choices

Payload-bearing CHOICE declarations model a value that is exactly one of a closed set of constructors:

Watch it in Grid

See the concept in Grid.

Use the film for the product interaction and this article for the complete contract and reference detail.

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