Docs/Types

Generics

Write a function or container once for many types.

On this page

A generic function or class takes type parameters in [ ]. The compiler makes a separate version for each type it is used with and checks that version.

Generic functions#

Valk
fn repeat[T](value: T, times: uint) Array[T] {
    let list = Array[T]{}
    each 0..times as _ : list.append(value)
    return list
}

fn join[A, B](a: A, b: B) String {
    return a + "/" + b
}

fn main() {
    let names = repeat[String]("Ada", 2)
    println(names.length)            // 2
    println(join[int, String](1, "x")) // 1/x
}

Type parameter names are usually one capital letter, but any name works. Inside the body T is used like any type, including ?T or Array[T].

Infer the type from an argument#

Write $T in a parameter type to let the compiler take T from the argument. Later parameters and the result then use plain T:

Valk
fn largest(items: Array[$T]) ?T {
    let best: ?T = null
    each items as item {
        if !isset(best) || item > best : best = item
    }
    return best
}

fn pair(a: $T, b: T) Array[T] {
    return Array[T]{ a, b }
}

fn main() {
    println(largest(Array[int]{ 3, 9, 4 }) ?? 0)   // 9
    println(pair("a", "b").length)                 // 2
}

$T also works inside other types: ?$T, &[$T], HashMap[$K, $V]. An &[$T] parameter takes arrays, slices and fixed arrays alike.

Generic classes#

Valk
class Stack[T] {
    items: Array[T] (.{})

    fn push(value: T) {
        this.items.append(value)
    }

    fn pop() ?T {
        return this.items.pop_last() !? null
    }
}

fn main() {
    let stack = Stack[String]{}
    stack.push("a")
    stack.push("b")
    println(stack.pop() ?? "")  // b
    let numbers = Stack[int]{}
    numbers.push(7)
    println(numbers.items.length) // 1
}

Structs, traits, and modes take type parameters the same way.

Generic methods#

A method can have type parameters of its own. Built-in collections use this for functions like map:

Valk
fn main() {
    let numbers = Array[int]{ 1, 2, 3 }
    let labels = numbers.map[String](fn(n: int) String { return "#%n" })
    println(labels.join(", ")) // #1, #2, #3
}

What the type must support#

The body may only use operations that every type it is used with supports. largest above uses >, so it works for numbers and strings, and for a class only when it defines an ordering operator. Using it with an unsupported type is a compile error at the call.

Type-specific code#

Compile-time conditions on T choose code per type. For example T.$is_nullable is true when T allows null:

Valk
fn describe[T](value: T) String {
    #if T.$is_nullable
    return "may be null"
    #else
    return "always set"
    #end
}

fn main() {
    println(describe[?String](null)) // may be null
    println(describe[int](5))        // always set
}

See Compile-time code for other conditions.

Generic types are invariant#

Array[A] and Array[B] are different types even when an A converts to a B. Convert the elements instead of the container.