Records & ADTs
Records are milang’s primary data structure. They hold named fields, support
structural updates, and form the basis of algebraic data types (ADTs).
Record Literals
A record is a set of field = value pairs inside braces, separated by ; or
newlines:
point = {x = 3; y = 4}
person = {name = "Alice"; age = 30}
build = {target = c, os = linux, arch = x86_64}
point = {x = 3, y = 4}
person = {name = Alice, age = 30}
Field Access
Use dot notation to read a field. Dots chain for nested records.
point = {x = 3; y = 4}
a = point.x
b = point.y
build = {target = c, os = linux, arch = x86_64}
point = {x = 3, y = 4}
a = 3
b = 4
Positional Access
Fields can also be accessed by declaration order using _0, _1, etc.:
pair = {first = "hello"; second = "world"}
a = pair._0
b = pair._1
build = {target = c, os = linux, arch = x86_64}
pair = {first = hello, second = world}
a = hello
b = world
Record Update
The <- operator creates a new record with selected fields replaced. Fields
not mentioned are carried over unchanged.
base = {x = 1; y = 2; z = 3}
moved = base <- {x = 10; z = 30}
build = {target = c, os = linux, arch = x86_64}
base = {x = 1, y = 2, z = 3}
moved = {x = 10, y = 2, z = 30}
Destructuring
Bind fields from a record directly into the current scope. Use {field} for
same-name bindings, or {local = field} to rename:
point = {x = 3; y = 4}
{x; y} = point
sum = x + y
__p_sum = <closure>
build = {target = c, os = linux, arch = x86_64}
point = {x = 3, y = 4}
_destruct_23 = {x = 3, y = 4}
x = 3
y = 4
Renaming during destructuring:
point = {x = 3; y = 4}
{myX = x; myY = y} = point
result = myX + myY
build = {target = c, os = linux, arch = x86_64}
point = {x = 3, y = 4}
_destruct_23 = {x = 3, y = 4}
myX = 3
myY = 4
result = 7
Scope-as-Record
When a function body has no explicit result expression — just indented bindings — the named bindings are collected into an implicit record:
makeVec x y =
magnitude = x + y
product = x * y
v = makeVec 3 4
build = {target = c, os = linux, arch = x86_64}
makeVec = <closure>
v = {magnitude = 7, product = 12}
Bare expressions (not bound to a name) execute for their side effects and are
not included in the returned record. This is how main works — see the
Scopes chapter.
ADTs (Algebraic Data Types)
An uppercase name bound to braces containing uppercase constructors declares a tagged union:
Shape = {Circle radius; Rect width height; Point}
c = Circle 5
r = Rect 3 4
p = Point
build = {target = c, os = linux, arch = x86_64}
Shape = _module_ {Circle = <closure>, Rect = <closure>, Point = Point {}}
Circle = <closure>
Rect = <closure>
Point = Point {}
c = Circle {radius = 5}
r = Rect {width = 3, height = 4}
p = Point {}
Each constructor becomes a function that produces a tagged record. Zero-field
constructors (like Point above) are plain tagged records with no arguments.
Constructors are also available namespaced under the type name
(e.g. Shape.Circle).
Constructors as Functions
Because constructors are just functions, they work naturally with map and
other higher-order functions:
values = map (\x -> Just x) [1, 2, 3]
__p_values = <closure>
build = {target = c, os = linux, arch = x86_64}
Pattern Matching on Tags
Use the -> operator to match on a value’s constructor tag. After a tag
matches, the record’s fields are accessible via dot notation or destructuring:
Shape = {Circle radius; Rect width height}
area shape = shape ->
Circle = 3.14 * shape.radius * shape.radius
Rect = shape.width * shape.height
a = area (Circle 5)
b = area (Rect 3 4)
build = {target = c, os = linux, arch = x86_64}
Shape = _module_ {Circle = <closure>, Rect = <closure>}
Circle = <closure>
Rect = <closure>
area = <closure>
a = 78.5
b = 12
Named-field destructuring in alternatives:
Shape = {Circle radius; Rect width height}
area shape ->
Circle {radius} = 3.14 * radius * radius
Rect {width; height} = width * height
a = area (Circle 5)
b = area (Rect 3 4)
build = {target = c, os = linux, arch = x86_64}
Shape = _module_ {Circle = <closure>, Rect = <closure>}
Circle = <closure>
Rect = <closure>
area = <closure>
a = 78.5
b = 12
See the Pattern Matching chapter for the full range of patterns, guards, and list matching.
Record Introspection
Several built-in functions let you inspect records dynamically:
| Function | Returns |
|---|---|
fields r | List of {name, value} records ([] for non-records) |
fieldNames r | List of field-name strings |
tag r | Constructor tag string, or "" for untagged values |
getField r "name" | Just value if present, Nothing if missing |
setField r "name" val | New record with field set |