Docs/Data & ownership

Generic code

Write an algorithm or container once and use it with concrete types.

On this page

A generic declaration names a type parameter instead of choosing a particular type. The compiler checks the declaration's operations for the concrete types used by callers.

Parameterize a function#

Valk
fn repeat[T](value: T) Array[T] {
    return Array[T]{ value, value }
}

fn main() {
    let names = repeat[String]("Ada")
    let counts = repeat[int](3)
    println(names[0])
    println(counts.sum()) // 6
}

The [T] after the function name introduces the type parameter. In repeat[String], every use of T in this instance becomes String.

Repeating a class value repeats its reference. Use an explicit copy operation when you need independent objects; generic syntax does not change the assignment rules.

Infer a type from an argument#

Use $T in a parameter type to introduce a type parameter that the compiler infers from a call:

Valk
fn first(items: Array[$T]) T !LookupError {
    return items.get(0) !>
}

fn main() {
    let names = Array[String]{ "Ada", "Sam" }
    println(first(names) !? "Nobody")
}

Here, the argument is an Array[String], so T is String and the function returns a string. The lookup error still needs handling if the array is empty.

Parameterize a class#

Valk
class Box[T] {
    value: T

    fn get() T {
        return this.value
    }
}

fn main() {
    let name = Box[String] { value: "Ada" }
    let score = Box[int] { value: 42 }
    println(name.get())
    println(score.get())
}

Use generic containers when the stored type should remain known to callers. Use an interface when callers only need a shared set of operations, or a tagged union when they must handle a specific set of alternatives.

Keep type requirements visible#

A generic function can only be instantiated with a type that supports the operations its body performs. For example, adding two T values requires an addition operation for that type. Begin with a concrete implementation, then introduce a type parameter where the same behavior makes sense for several types.

The compiler also exposes type metadata, such as T.$is_nullable, for #if conditions inside generic code. Use this when the generated behavior really needs to differ by type; ordinary application code can usually keep the same implementation.