Generics
Part of: Reef Language Reference Last reviewed on version: 0.8.0 Status: Implemented
Overview
Reef supports parametric polymorphism (generics) for both types and functions, implemented through compile-time monomorphization. This provides type-safe abstractions with zero runtime overhead.
Generic Functions ✅ NEW
Single Type Parameter:
fn identity[T](value: T): T
return value
end identity
// Usage with explicit type arguments
// IMPORTANT: Use [:Type] syntax (colon required to disambiguate from array access)
let x = identity[:int](42)
let s = identity[:string]("hello")
Multiple Type Parameters:
fn first[T, U](a: T, b: U): T
return a
end first
fn second[T, U](a: T, b: U): U
return b
end second
let f = first[:int, string](100, "ignored") // Returns 100
let s = second[:int, string](999, "result") // Returns "result"
Working with Generic Types:
type Box[T] = struct
value: T
end Box
fn wrap[T](item: T): Box[T]
let box: Box[T] = new Box[T]()
box.value = item
return box
end wrap
fn unwrap[T](box: Box[T]): T
return box.value
end unwrap
let boxed = wrap[:int](42)
let value = unwrap[:int](boxed) // value = 42
Note on Syntax: The [:Type] syntax is required because fn[int](arg) is ambiguous with array access. The colon prefix signals a generic type argument.
Requirements:
- ✅ Explicit type arguments supported at call site
- ✅ Type inference for constructors and functions (NEW in 1.3.0)
- ⚠️ Type constraints not yet supported
Type Inference ✅ NEW in 1.3.0
Reef now supports automatic type argument inference for both generic constructors and generic function calls.
Constructor Inference
When calling a generic enum constructor, the type parameter is inferred from the argument:
type Option[T] = enum
Some(T)
None
end Option
// Type inferred from argument - no explicit type needed!
let some_int = Option_Some(42) // Inferred: Option[int]
let some_str = Option_Some("hello") // Inferred: Option[string]
// Explicit type still required for None (no value to infer from)
let none_int = @Option[int].None()
Function Call Inference
When calling a generic function, type arguments are inferred from the actual argument types:
fn unwrap_or[T](opt: Option[T], default: T): T
match opt
Option_Some(v) => return v end
_ => return default end
end match
end unwrap_or
fn is_some[T](opt: Option[T]): bool
match opt
Option_Some(_) => return true end
_ => return false end
end match
end is_some
// Type args inferred from arguments - no [:int] needed!
let opt = Option_Some(42)
if is_some(opt) // Inferred: is_some[:int]
let val = unwrap_or(opt, 0) // Inferred: unwrap_or[:int]
end if
Multi-Parameter Type Inference
Works with multiple type parameters:
type Result[T, E] = enum
Ok(T)
Err(E)
end Result
fn unwrap_result[T, E](res: Result[T, E], default: T): T
match res
Result_Ok(v) => return v end
_ => return default end
end match
end unwrap_result
let ok_val = @Result[int, string].Ok(42)
let value = unwrap_result(ok_val, 0) // Inferred: unwrap_result[:int, string]
When Explicit Types Are Still Needed
-
None/Err constructors - No value to infer type from:
let none = @Option[int].None() // Must specify [int] let err = @Result[int, string].Err("oops") // Must specify full type -
Ambiguous inference - When the compiler can't determine the type:
let x = identity[:int](42) // Explicit when needed
Default Parameters with Generics ✅ NEW in 1.4.0
Generic functions support default parameters, just like regular functions. Default parameters work with both explicit type arguments and type inference.
Syntax
fn name[T](required: T, optional: type = default_value): return_type
// ...
end name
Examples
Generic function with default parameter:
fn wrap_with_label[T](value: T, label: string = "value"): string
return label
end wrap_with_label
proc main()
// With explicit type argument
println(wrap_with_label[:int](42)) // "value" (uses default)
println(wrap_with_label[:int](42, "number")) // "number"
// With type inference
let x: int = 42
println(wrap_with_label(x)) // "value" (infers T=int)
println(wrap_with_label(x, "count")) // "count"
end main
Multiple defaults with generics:
type Option[T] = enum
Some(T)
None
end Option
fn unwrap_with_message[T](opt: Option[T], default: T, msg: string = "using default"): T
match opt
Option_Some(v) =>
println("found value")
return v
end
_ =>
println(msg)
return default
end
end match
end unwrap_with_message
proc main()
let some_val = Option_Some(42)
let none_val = @Option[int].None()
// Type inference + default parameter
let v1 = unwrap_with_message(some_val, 0) // prints "found value", returns 42
let v2 = unwrap_with_message(none_val, 99) // prints "using default", returns 99
let v3 = unwrap_with_message(none_val, 0, "fallback!") // prints "fallback!", returns 0
end main
Rules
-
Default parameters must come after required parameters - Same as regular functions.
-
Default values can use non-generic types - The default doesn't need to involve type parameter T.
-
Type inference still works - Default parameters don't interfere with type argument inference.
See 020_FUNCTIONS.md for more details on default parameters.
Generic Types
Generic Structs
type Box[T] = struct
value: T
end Box
Instantiation:
let int_box = new Box[int]()
let string_box = new Box[string]()
Generic Active Objects
active object Container[T]
item: T
exclusive proc set(value: T)
self.item = value
end set
shared fn get(): T
return self.item
end get
end Container
Usage:
let counter = new Container[int]()
let cache = new Container[string]()
Generic passive objects are not in 0.9: object Stack[T] and
extends Container[int] are type errors. Put the type parameter on a
generic struct, or on an Active Object as above. See
Passive objects — What 0.9 does not do.
Monomorphization
Reef uses compile-time specialization for all generics:
Types:
Box[int]→Box_int(C struct)Box[string]→Box_string(C struct)
Functions:
identity[int]→identity_int(int value)(C function)identity[string]→identity_string(char* value)(C function)
Characteristics:
- ✅ Zero runtime overhead (no virtual dispatch, no boxing)
- ✅ Full type safety (type checking per instantiation)
- ✅ Each specialization is independent (optimized separately)
- ⚠️ Code bloat possible with many instantiations
- ⚠️ Longer compilation time with complex generics
Implementation: Similar to C++ templates and Rust generics.
Limitations
Not Yet Implemented:
- Type constraints/traits (
T: Comparable) - Higher-kinded types
- Generic type aliases
Non-Goals (by design):
-
Method-level generic type parameters. A method (type-bound procedure) may reuse its enclosing type's parameters, but it may not introduce its own —
proc foo[T](...)on a struct/AO is not supported. Reef's lineage (A2 Oberon) parameterizes only modules, never procedures, and Reef narrows that to generic types and generic top-level functions. The idiom for a "generic operation on a receiver" is a generic free function with an explicit receiver argument:// Not supported: a method with its own type parameter // exclusive proc check[T](self, res: Result[T, Error]) // ✗ // Idiom: a generic free function taking the receiver explicitly fn check_ok[T](runner: TestRunner, res: Result[T, Error]) // ✓(A common non-generic alternative is to collapse to a concrete type first — e.g.
runner.assert_eq_bool(result.is_ok(r), true, msg), sinceis_ok[T,E]is a generic free function returningbool.)
Same-named generics across modules — supported. Two modules may each define a
generic type with the same short name (e.g. both defining a Dup[T]): each
module's definition is independently specialized under a module-qualified symbol,
so the two never collide. One boundary remains: a single module that can see
both definitions (defines one and imports the other, or imports both) cannot use
the bare name — the compiler rejects it as ambiguous, naming the candidate
modules. Keep same-named generics in modules that aren't visible to each other,
or rename one.
Type constraints — supported. Constrain a type parameter to types
implementing a trait with a where clause, placed after the return type.
Note this is not the inline [T: Trait] form some languages use:
module constrained
trait Describable
fn describe(): string;
end Describable
type Point = struct
x: int
y: int
end Point
impl Describable for Point
fn describe(): string
return "a point"
end describe
end impl
fn show[T](item: T): string where T: Describable
return item.describe()
end show
proc main()
let p = Point { x: 1, y: 2 }
println(show(p))
end main
end module
See 050_TRAITS.md for trait declaration, impl blocks, and the
current limitations of the trait system.
Next: 060_CLOSURES.md Previous: 050_TRAITS.md