Algebraic Data Types¶
Every type declaration in graphcal is an n-variant tagged union.
A record-shaped struct is just a single-variant union whose sole
constructor's name matches the type's name; a unit marker is a
single unit constructor. The functional core never distinguishes
record from union — there is only one shape.
Tagged Unions¶
A tagged union lists its constructors inside the braced body of a
type declaration. Each constructor has an optional payload
(declared with parens or braces) or is a bare unit constructor:
dim Force = Mass * Length / Time^2;
type ManeuverKind {
Impulsive(delta_v: Velocity),
LowThrust(thrust: Force, duration: Time),
Coast,
}
Constructors live in a namespace that is distinct from the type namespace — a single lexeme can name both a type and a constructor without ambiguity.
Records (Single-Variant Unions)¶
Record-shaped data is written as a single-variant union whose sole constructor's name equals the type's name:
dim Velocity = Length / Time;
type TransferResult {
TransferResult(dv1: Velocity, dv2: Velocity, total_dv: Velocity, tof: Time),
}
Construction¶
Construction is always a constructor call — parens with named args:
node result: TransferResult = TransferResult(
dv1: 100.0 m/s,
dv2: 200.0 m/s,
total_dv: 300.0 m/s,
tof: 3600.0 s,
);
Field shorthand is available only for local variables (for example,
for/match bindings) with the same name as the field. Graph nodes
must still be referenced explicitly with @:
node dv1: Velocity = 100.0 m/s;
node result: TransferResult =
TransferResult(dv1: @dv1, dv2: 200.0 m/s, total_dv: 300.0 m/s, tof: 3600.0 s);
Field Access¶
Field access works on a value of a single-variant union — there is exactly one constructor, so the field set is unambiguous:
For multi-variant unions, field access is rejected — destructure
through match instead.
Unit Markers¶
A unit marker is a single-variant union whose constructor takes no payload:
Unit markers are useful as phantom type parameters (e.g., reference frames).
Note:
type T;(semicolon, no body) is not a unit marker — it declares a required type that importers must bind. See Multi-File Projects → Visibility and Bindability.
Constructing Union Values¶
Construct a variant by its constructor name. The parens-with-named-args form is the canonical syntax:
node maneuver: ManeuverKind = LowThrust(thrust: 0.5 N, duration: 3600.0 s);
node coast: ManeuverKind = Coast;
Match Expressions¶
Use match to destructure union types:
node fuel_proxy: Force = match @maneuver {
Impulsive { delta_v: _ } => 0.0 N,
LowThrust { thrust, duration: _ } => thrust,
};
- Each arm matches a member and binds its fields
_discards a field value- Field shorthand:
thrustbinds the field to a local variable of the same name
Exhaustiveness Checking¶
The compiler requires that all constructors are covered:
type Status {
Nominal,
Warning(code: Dimensionless),
}
// ERROR: non-exhaustive -- missing `Warning` arm
node code: Dimensionless = match @status {
Nominal => 0.0,
};
All Arms Must Agree¶
All match arms must produce the same type and dimension:
// ERROR: arms have different dimensions (Force vs Velocity)
node bad: Force = match @maneuver {
Impulsive { delta_v } => delta_v, // Velocity
LowThrust { thrust, duration: _ } => thrust, // Force
};
Generic Types (Phantom Type Parameters)¶
Types can have generic parameters for type-safe phantom typing:
Changing a Phantom Type Parameter¶
There is no phantom-type cast operator. To change a phantom type parameter (for example, to re-label a reference frame), construct a new instance and assign each field explicitly:
node pos_eci: Vec3<Length, Eci> = Vec3<Length, Eci>(x: 7000.0 km, y: 0.0 km, z: 0.0 km);
node pos_body: Vec3<Length, Body> = Vec3<Length, Body>(
x: @pos_eci.x,
y: @pos_eci.y,
z: @pos_eci.z,
);
The verbosity is intentional: a re-labeling is a deliberate, field-by-field act, visible at the call site — not a silent reinterpretation of opaque data.