User-Defined Types
Part of: Reef Language Reference Last reviewed on version: 0.9.0
Overview
Reef provides several ways to define custom types:
| Type | Purpose | Example |
|---|---|---|
| Struct/Record | Group related fields | Point { x, y } |
| Enum | Sum types with variants | Option { Some, None } |
| Array | Indexed collection | [1, 2, 3] |
| Set | Unique value collection | {1, 2, 3} |
| Subrange | Constrained integers | 0..23 |
| Type Alias | Alternative name | type Age = int |
| Passive object | Class, single inheritance | object Button extends Widget — 040_OBJECTS.md |
Structs and Records
Structs group related data into a single type.
Declaration
type Point = struct
x: float
y: float
end Point
// Alternative syntax (Pascal/Oberon style)
type Person = record
name: string
age: int
end Person
struct and record are interchangeable - use whichever you prefer.
Creating Instances
With new (heap allocated):
let p = new Point()
p.x = 10.0
p.y = 20.0
With struct literal syntax:
let p = Point { x: 10.0, y: 20.0 }
Field Access
let p = Point { x: 3.0, y: 4.0 }
// Read fields
let x_val = p.x
let y_val = p.y
// Write fields (if mutable)
mut q = Point { x: 0.0, y: 0.0 }
q.x = 5.0
q.y = 12.0
Methods
Structs can have methods defined inside them:
type Point = struct
x: float
y: float
// Method - uses self to access fields
fn distance(): float
return (self.x ** 2 + self.y ** 2) ** 0.5
end distance
// Another method
fn add(other: Point): Point
return Point { x: self.x + other.x, y: self.y + other.y }
end add
// Procedure (no return value)
proc print()
print("Point(")
print_float(self.x)
print(", ")
print_float(self.y)
println(")")
end print
end Point
// Usage
let p = Point { x: 3.0, y: 4.0 }
let dist = p.distance() // 5.0
p.print() // Point(3.0, 4.0)
let q = Point { x: 1.0, y: 1.0 }
let sum = p.add(q) // Point { x: 4.0, y: 5.0 }
Struct Literal Validation
The compiler validates struct literals:
type Config = struct
host: string
port: int
timeout: int
end Config
// ERROR: Missing field 'timeout'
let c = Config { host: "localhost", port: 8080 }
// ERROR: Unknown field 'debug'
let c = Config { host: "localhost", port: 8080, timeout: 30, debug: true }
// ERROR: Duplicate field 'port'
let c = Config { host: "localhost", port: 8080, port: 9090, timeout: 30 }
// CORRECT: All fields provided once
let c = Config { host: "localhost", port: 8080, timeout: 30 }
Enums (Sum Types)
Enums define types with multiple variants. Each value is exactly one variant.
Simple Enums
type Color = enum
Red
Green
Blue
end Color
type Direction = enum
North
South
East
West
end Direction
Enums with Data
Variants can carry data:
type Option = enum
Some(int)
None
end Option
type Result = enum
Ok(string)
Err(string)
end Result
type Shape = enum
Circle(float) // radius
Rectangle(float, float) // width, height
Point // no data
end Shape
Constructors
Constructors are prefixed with the type name:
// Simple enum
let color = Color_Red()
let dir = Direction_North()
// Enum with data
let some_value = Option_Some(42)
let no_value = Option_None()
let ok = Result_Ok("success")
let err = Result_Err("something went wrong")
let circle = Shape_Circle(5.0)
let rect = Shape_Rectangle(10.0, 20.0)
let point = Shape_Point()
Note: Zero-argument constructors (Color_Red, Shape_Point, ...) still
need call parentheses — the bare name alone is a Type Error ("'Color_Red' is
a function, not a variable"). This applies in match patterns too: a bare
Color_Red pattern is REJECTED with a dedicated type error —
Bare enum constructor pattern 'Color_Red' is not allowed — write
'Color_Red()' to match the variant, or rename the binding.
— because the same bare form used to parse as a variable-binding pattern
(a catch-all), which the exhaustiveness checker trusted even though the
generated code for that arm still tested the real tag; a match missing a
variant elsewhere could be wrongly accepted as exhaustive and crash at
runtime if that missing variant was ever matched against (fixed as
Always write Color_Red() so the checker sees the real
constructor pattern.
Pattern Matching with Enums
The primary way to use enums is with match:
fn describe_color(c: Color): string
match c
Color_Red() => return "It's red" end
Color_Green() => return "It's green" end
Color_Blue() => return "It's blue" end
end match
end describe_color
fn get_value(opt: Option): int
match opt
Option_Some(x) =>
return x
end
Option_None() =>
return 0
end
end match
return 0
end get_value
fn area(shape: Shape): float
match shape
Shape_Circle(r) =>
return 3.14159 * r * r
end
Shape_Rectangle(w, h) =>
return w * h
end
Shape_Point() =>
return 0.0
end
end match
return 0.0
end area
Generic Enums
Enums can be parameterized:
type Option[T] = enum
Some(T)
None
end Option
type Result[T, E] = enum
Ok(T)
Err(E)
end Result
// Usage
proc main()
let int_opt: Option[int] = Option_Some(42)
let str_opt: Option[string] = Option_Some("hello")
end main
An enum value that carries a GC-managed payload (a string, array, struct, active object, or nested payload enum) must not be a module-level
let/mut/const— the compiler rejects that. Payload-free and all-scalar enums are allowed as module globals.Heap-embedded payloads (struct / Active Object / object fields, array elements, closure captures) are traced as of 0.9. The remaining restriction is the module-global case only.
Pattern Matching
Pattern matching is Reef's way to destructure and inspect values.
Match Statement
match value
pattern1 =>
// code for pattern1
end
pattern2 =>
// code for pattern2
end
end match
Match Expression
Match can also be an expression that returns a value:
let result = match x
1 => 100 end
2 => 200 end
_ => 0 end
end match
Pattern Types
Wildcard - matches anything:
match value
_ =>
println("matches anything")
end
end match
Variable - binds the value to a name:
match value
x =>
println("value is ${x}")
end
end match
Literal - matches specific values:
match number
0 => println("zero") end
1 => println("one") end
42 => println("the answer") end
_ => println("something else") end
end match
match text
"yes" => true end
"no" => false end
_ => false end
end match
match flag
true => println("on") end
false => println("off") end
end match
Constructor - matches enum variants:
match option
Option_Some(value) =>
println("got ${value}")
end
Option_None() =>
println("nothing")
end
end match
Exhaustiveness Checking
The compiler ensures all cases are covered. This example is missing Closed
and is rejected:
type Status = enum
Active
Pending
Closed
end Status
fn describe(s: Status): string
match s
Status_Active() => return "active" end
Status_Pending() => return "pending" end
end match
end describe
Error message:
Type Error: Non-exhaustive match statement. Missing patterns:
Status_Closed
Hint: Add missing cases or use '_' wildcard
CORRECT: All cases covered:
type Status = enum
Active
Pending
Closed
end Status
fn describe(s: Status): string
match s
Status_Active() => return "active" end
Status_Pending() => return "pending" end
Status_Closed() => return "closed" end
end match
end describe
// ALSO CORRECT: Using wildcard for remaining cases
fn is_active(s: Status): bool
match s
Status_Active() => return true end
_ => return false end
end match
end is_active
Arrays
Arrays are ordered, indexed collections of the same type.
Array Literals
let numbers = [1, 2, 3, 4, 5]
let names = ["Alice", "Bob", "Carol"]
let flags = [true, false, true]
// Empty array needs type annotation
let empty = new [int](0) // an empty literal [] cannot infer T
Indexing
Arrays are zero-indexed:
let nums = [10, 20, 30, 40, 50]
let first = nums[0] // 10
let third = nums[2] // 30
let last = nums[4] // 50
// Modify (if array is mutable)
mut arr = [1, 2, 3]
arr[1] = 99 // [1, 99, 3]
Dynamic Arrays
Allocate arrays at runtime with new:
let size = 100
let arr = new [int](size) // Array of 100 ints
// Initialize
for i in 0 to size
arr[i] = i * 2
end for
Iteration
let items = ["a", "b", "c"]
for i in 0 to 3
println(items[i])
end for
Array Type Syntax
[int] // Array of int
[string] // Array of string
[Point] // Array of Point structs
[[int]] // Array of arrays of int (nested)
Array/String Slicing ✅ NEW
Slicing extracts a portion of an array or string using [start..end] syntax.
Syntax:
arr[start..end] // Elements from start to end-1
arr[start..] // Elements from start to the end
arr[..end] // Elements from the beginning to end-1
String Slicing Examples:
proc main()
let s = "Hello World"
let hello = s[0..5] // "Hello"
let world = s[6..] // "World"
let hel = s[..3] // "Hel"
println(hello) // Hello
println(world) // World
println(hel) // Hel
end main
Array Slicing Examples:
proc main()
let nums = [10, 20, 30, 40, 50]
let first_three = nums[0..3] // [10, 20, 30]
let last_two = nums[3..] // [40, 50]
let middle = nums[1..4] // [20, 30, 40]
end main
Notes:
- Indices are zero-based
- The end index is exclusive (like Python)
- Out-of-bounds access returns empty results (no runtime error)
- Slices create a new copy of the data
Sets
Sets are collections of unique values with efficient membership testing.
Set Type
setof[int] // Set of integers
setof[char] // Set of characters
setof[Color] // Set of enum values
Set Literals
type Color = enum
Red
Green
Blue
end Color
let digits: setof[int] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
let vowels: setof[char] = {'a', 'e', 'i', 'o', 'u'}
let primary: setof[Color] = {Color_Red(), Color_Green(), Color_Blue()}
// Range notation
let teens: setof[int] = {13..19}
// Mixed (individual values and ranges)
let special: setof[int] = {0, 10, 20..25, 100}
Membership Test
let evens: setof[int] = {0, 2, 4, 6, 8}
if 4 in evens
println("4 is even")
end if
if 5 in evens
println("never prints")
end if
Set Operations
let a: setof[int] = {1, 2, 3}
let b: setof[int] = {2, 3, 4}
let union = a + b // {1, 2, 3, 4}
let intersect = a * b // {2, 3}
let diff = a - b // {1}
Current Limitations
- Sets are limited to 32 elements (implemented as bitsets)
- Elements must be comparable (int, char, enum values)
Subranges
Subranges define constrained integer types.
Declaration
type Digit = subrange 0 9
type Hour = 0..23 // Alternative syntax
type Month = subrange 1 12
type Percent = subrange 0 100
Usage
type Digit = subrange 0 9
type Hour = 0..23
type Month = subrange 1 12
let d: Digit = 5 // OK
let h: Hour = 23 // OK
let m: Month = 6 // OK
// A subrange value is assignable to int
let x: int = d // OK, d treated as int
Assignment compatibility is not arithmetic compatibility: d + 10 is
currently rejected ("Arithmetic operators require numeric operands of
matching types"). Widen first (let x: int = d) and do the arithmetic on
the int.
With Sets
Subranges are often used with sets:
type Digit = subrange 0 9
let evens: setof[Digit] = {0, 2, 4, 6, 8}
let d: Digit = 4
if d in evens
println("even digit")
end if
Bounds Checking
Subrange bounds are documentation today, not enforcement: the declared
range is recorded in the type (and drives assignment compatibility with
int), but neither a literal nor a variable is range-checked, at compile
time or at run time.
type Digit = subrange 0 9
let ok: Digit = 5 // in range
let over: Digit = 15 // ALSO accepted today -- not range-checked
let x = 15
let unchecked: Digit = x // accepted; no runtime check either
Enforcement (compile-time for literals, optional runtime checks for variables) is a future feature.
Type Aliases
Create alternative names for existing types:
type Age = int
type Name = string
type Coordinate = float
type UserID = int
type ErrorMessage = string
Aliases are purely for documentation - the underlying type is unchanged:
type Age = int
type Year = int
let age: Age = 25
let year: Year = 2025
// These are both just int, so this works
let sum = age + year // 2050
Choosing the Right Type
| Need | Use |
|---|---|
| Group related fields | struct |
| Multiple exclusive states | enum |
| State with associated data | enum with data |
| Ordered collection | Array [T] |
| Unique values, membership test | Set setof[T] |
| Constrained integer | Subrange |
| Self-documenting code | Type alias |
Example: Modeling a Game
// Enum for discrete states
type GameState = enum
Menu
Playing
Paused
GameOver
end GameState
// Struct for grouped data
type Player = struct
name: string
x: int
y: int
health: int
fn is_alive(): bool
return self.health > 0
end is_alive
end Player
// Subrange for bounded values
type Health = subrange 0 100
// Set for game options
type Difficulty = enum
Easy
Normal
Hard
end Difficulty
let enabled_modes: setof[Difficulty] = {Difficulty_Easy(), Difficulty_Normal()}
Common Patterns
Option Pattern
Represent optional values with the canonical generic Option[T] from
core.option. Use Option[T] when absence is not an error — a lookup that may
find nothing:
import core.option
fn find(arr: [int], target: int): option.Option[int]
for i in 0 to 10 // assuming max 10
if arr[i] == target
return Option_Some(i)
end if
end for
return @option.Option[int].None()
end find
option.unwrap(opt) returns the value on Some and panics on None; use
option.unwrap_or(opt, default) or a match when None is expected.
A real stdlib example: sys.env.get_env(name): option.Option[string] returns
Some(value) if the environment variable is set, None if it is unset.
Result Pattern
Represent success or failure with the canonical generic Result[T, E] from
core.result. The stdlib pairs E with the shared structured core.error.Error
(a kind + message + code), so use Result[T, core.error.Error] when a
failure carries a reason:
import core.result
import core.error
fn divide(a: int, b: int): result.Result[int, error.Error]
if b == 0
return @result.Result[int, error.Error].Err(
error.error(error.ErrorKind_InvalidInput(), "division by zero"))
end if
return @result.Result[int, error.Error].Ok(a / b)
end divide
result.unwrap_ok(r) returns the value on Ok and panics on Err
(unwrap_err is symmetric); use result.unwrap_or or a match to handle the
error path. Construct variants with the @Type[args].Variant(...) form (a
module-qualified @alias.Type[args].Variant(...) also works); the bare
alias.Variant(...) shorthand (e.g. result.Result_Err(...)) does not
reliably infer the full Result[T, E] signature in a return position — it
type-checks the success-type parameter as unresolved (Result[?, Error]) —
so the explicit @Type[args].Variant(...) form above is the safe idiom.
A real stdlib example: core.convert.toInt(s): result.Result[int, error.Error]
returns Ok(n) on a parseable prefix, Err(...) on invalid input (e.g. an
empty string).
State Machine
Model state transitions:
type ConnectionState = enum
Disconnected
Connecting
Connected
Error(string)
end ConnectionState
fn next_state(current: ConnectionState, event: string): ConnectionState
match current
ConnectionState_Disconnected() =>
if event == "connect"
return ConnectionState_Connecting()
end if
return current
end
ConnectionState_Connecting() =>
if event == "success"
return ConnectionState_Connected()
elif event == "fail"
return ConnectionState_Error("Connection failed")
end if
return current
end
ConnectionState_Connected() =>
if event == "disconnect"
return ConnectionState_Disconnected()
end if
return current
end
ConnectionState_Error(_) =>
if event == "retry"
return ConnectionState_Connecting()
end if
return current
end
end match
return current
end next_state
Previous: 025_STRING_INTERPOLATION.md Next: 035_ERROR_HANDLING.md