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

  1. 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
    
  2. 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

  1. Default parameters must come after required parameters - Same as regular functions.

  2. Default values can use non-generic types - The default doesn't need to involve type parameter T.

  3. 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), since is_ok[T,E] is a generic free function returning bool.)

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