Learn Dodo Dodo 0.1.4

Generics and reusable behavior

Write type-parameterized functions and structs, infer arguments, and understand static method protocols.

On this page

A generic function or type works with more than one concrete type. Write type parameters in angle brackets, then use those names wherever a type is expected. The compiler specializes generic code for the concrete types your program uses and checks each specialization.

A generic function

package main

fn identity<T>(value: T) -> T {
    value
}

fn main() {
    core.assert_eq(identity(42i32), 42i32)
    core.assert_eq(identity::<u8>(7), 7u8)
    let answer: u32 = identity(42)
    core.assert_eq(answer, 42u32)
}

T is a type parameter. identity(42i32) infers T = i32 from the argument. identity::<u8>(7) provides it explicitly. The expected result type in let answer: u32 = ... also guides inference. Generic calls canonically use ::<...>; generic declarations and type names use <...> without ::.

Inference uses the current expression and its expected type. It does not infer a function’s public signature, or look ahead to later uses of a variable. If a constructor has no argument that determines T, provide explicit arguments or an expected result type. Conflicting type information is an error.

Generic structs and associated functions

package main

struct Slot<T> {
    value: T

    fn new(value: T) -> Self {
        Slot<T> { value }
    }

    fn get(&self) -> &T {
        &self.value
    }
}

fn main() {
    inferred := Slot { value: 42i32 }
    explicit := Slot.new::<u8>(7)
    core.assert_eq(*inferred.get(), 42i32)
    core.assert_eq(*explicit.get(), 7u8)
}

The literal infers its type from the field initializer. Slot<u8> is a concrete type in a type annotation; an explicit associated function call puts its type arguments on the function: Slot.new::<u8>(...). Self inside a generic struct means that struct with its current arguments.

Enums can also declare type parameters, for example enum Choice<T> { Empty, Value(T) }. Different specializations are distinct types: Slot<u8> cannot be assigned to Slot<u32> by an implicit conversion.

Operations are checked for each concrete type

Generic code does not erase types or bypass ownership checks. A generic function that applies + can only be used with types for which that operation exists. An operation that copies T only works when the concrete T is copyable. Likewise, a collection’s element restrictions still apply to every specialization.

For example, this is a useful numeric helper:

package main

fn add<T>(left: T, right: T) -> T {
    left + right
}

fn main() {
    core.assert_eq(add(20u32, 22u32), 42u32)
    core.assert_eq(add(1.25f64, 0.75f64), 2.0f64)
}

It does not make addition available for arbitrary structs. Dodo has no traits, general constraint declarations, overload resolution, or specialization.

Reusable behavior through public methods

Many standard-library APIs accept a generic object with an expected method. The concrete object supplies that method, and compilation checks the call:

package main

struct Doubler {
    pub fn apply(&self, value: i32) -> i32 {
        value * 2
    }
}

fn transform<F>(operation: &F, value: i32) -> i32 {
    operation.apply(value)
}

fn main() {
    doubler := Doubler {}
    core.assert_eq(transform(&doubler, 21), 42i32)
}

This is a statically checked method protocol. There is no runtime interface object or implicit allocation. A private struct may expose public methods for this purpose without publishing its type name. Across package boundaries the called method must be public.

The standard library uses this pattern for readers and writers, formatters, allocators, hash/equality policies, and callbacks. Read each API’s required signature carefully; a similar method name with a different receiver or return type does not satisfy it. Closures and general first-class function values are unavailable; use an object with fields to carry callback state.

Generic ownership contracts

Returning borrowed data still requires a valid source. from(value) on a generic function refers to the concrete argument’s dependencies: a borrow-free specialization contributes none, while a borrow-carrying specialization retains its sources. A type parameter never permits returning a reference to destroyed local storage.

Allocated container methods may use the narrow requires_plain(T, ...) contract to restrict mutation to borrow-free, Result-free payloads. stores(...) tracks sources deposited into typed storage. These are checked storage effects, not general trait bounds; their syntax and rules are in container element safety.

All used generic bodies must be available in the loaded source packages. There are no separately compiled generic interfaces. Recursive expansion is bounded; exceeding the compiler’s limit produces a diagnostic instead of unbounded compilation. For concrete language restrictions and limits, see compiler support.

Type to search all documentation.

Keyboard shortcuts

Search documentation
Ctrl K or /
Move through results
↑ ↓
Open selected result
Enter
Close a dialog
Esc
Show these shortcuts
?