Algebraic data types and pattern matching

An algebraic data type defines exactly how values of a nominal type can be constructed. Pattern matching safely recovers the information carried by a constructor.

Enumerations and empty types

type Direction = North + East + South + West;
type Void;

Direction has four nullary constructors. Void has none, so a valid runtime value of Void cannot be constructed.

Constructors with fields

type Shape =
    Circle * radius:Float64
  + Rectangle * width:Float64 * height:Float64;

+ separates constructors. * introduces fields belonging to the preceding constructor. Construct values positionally or, where supported by the constructor, with named fields:

Circle(2.0)
Rectangle { width = 3.0, height = 4.0 }

Named construction checks that every required field appears exactly once.

Type parameters and recursion

type Maybe(a:Type) = Nothing + Just * value:a;

type List(a:Type) =
    Nil
  + Cons * head:a * tail:List(a);

Parameters make one declaration reusable. Recursive occurrences describe inductive structure; positivity and required termination checks prevent unsound type-level recursion.

Implicit constructors

When a type body begins with a lowercase field, tulam creates one constructor with the type’s name:

type Point = x:Float64 * y:Float64;
value origin = Point(0.0, 0.0);

There is no record declaration keyword. Nominal record-like data uses type.

Matching fields

function area(shape:Shape) : Float64 = match shape
    | Circle * radius -> 3.14159 * radius * radius
    | Rectangle * width * height -> width * height;

Named patterns improve clarity when order is not the main idea:

Point { x = px, y = py }

Nested patterns decompose several layers at once. Use a wildcard _ for a field whose value is irrelevant.

Spread and derived instances

type Point3D = ..Point * z:Float64;
type Color = Red + Green + Blue deriving Eq, Show;

Spread copies visible nominal fields in order; it is layout composition, not inheritance or an open structural row. deriving requests ordinary checked instances from derivation logic supplied by the algebra. It does not bypass coherence or laws.

Dependent and generalized constructors

Later fields may refer to earlier ones, and a constructor may state a refined result type:

type Sized(a:Type) = size:Nat * items:Vec(a,size);

type Vec(a:Type,n:Nat) =
    VNil : Vec(a,Z)
  + VCons * head:a * tail:Vec(a,n) : Vec(a,Succ(n));

Matching a GADT constructor refines indices for its branch. Chapters 16 and 17 explain the dependent reasoning involved.

Choosing data or classes

Use algebraic data when a known family of alternatives and exhaustive matching are central. Use a class when nominal extension and dynamic dispatch are central. Neither is a disguised version of the other.

Common mistakes

Do not use commas between constructors or constructor fields: + and * own those roles in a type declaration. Do not confuse nominal spread with an open structural row, and do not omit a constructor branch from a closed match.

Recap

A data declaration fixes a nominal set of constructors. Construction proves which alternative exists; exhaustive matching recovers its fields safely and may refine dependent indices.

Normative details: Language Reference §5.