Skip to content
Kavachकवच

Generics

Also known as: generic template type parameter parametric polymorphism type variable

● Intermediate5 minBefore this: Functions, RecordsSpec 2.9

A generic function or type works for any type: fn first<T>(items: [T]) -> T? works on a list of anything.

fn first<T>(items: [T]) -> T? {
    items.get(0)
}

type Pair<A, B> {
    left: A
    right: B
}

fn main() {
    print(first([3, 1, 2]) ?? 0)
    print(first(["x", "y"]) ?? "")
    const p = Pair(left: "age", right: 31)
    print("{p.left}: {p.right}")
}

The program ran and printed:

kavach run generics-fn.kav

3xage: 31
generics-fn.kav: Kavach code, then what running it prints

<T> is a placeholder for a type. Each call fills it in: first([3, 1, 2]) uses Int, first(["x", "y"]) uses String. Types like Pair<A, B> work the same way. A method can have its own placeholders too:

type Picker {
    seed: Int

    fn pick<T>(self, items: [T]) -> T {
        items[self.seed % items.len()]
    }
}

fn first<T>(xs: [T]) -> T? {
    xs.get(0)
}

fn apply(f: fn([Int]) -> Int?, xs: [Int]) -> Int {
    f(xs) ?? -1
}

fn main() {
    const p = Picker(seed: 5)
    print(p.pick([10, 20, 30]))
    print(p.pick(["x", "y"]))
    print(apply(first, [7, 8]))
}

The program ran and printed:

kavach run generics-methods.kav

30y7
generics-methods.kav: Kavach code, then what running it prints

Type arguments written where a value is made

A generic type's arguments are usually worked out from its fields and from where the value goes. When no field shows them, write them where the value is made, or give the variable its type. Inside the type's methods, I is then the type the value was made with:

use std.json

type Shout {
    text: String
}

/// JSON kept as text until it is needed. No field holds an `I`,
/// so `I` is written where a box is made.
type Box<I> {
    raw: String

    fn get(self) -> I ! JsonError {
        try json.decode<I>(self.raw)
    }
}

fn main() ! Error {
    const a = Box<Shout>(raw: "\{\"text\":\"hi\"}")
    const b: Box<Shout> = Box(raw: "\{\"text\":\"there\"}")
    print("{(try a.get()).text} {(try b.get()).text}")
}

The program ran and printed:

kavach run generics-construction.kav

hi there
generics-construction.kav: Kavach code, then what running it prints

The count must match the type's parameters:

type Box<I> {
    raw: String
}

fn main() {
    const c = Box<String, Int>(raw: "")
    print(c.raw)
}

The compiler refused the program:

kavach run generics-arity.kav

error: `Box` takes 1 type argument, not 2
generics-arity.kav: Kavach code, then what running it prints
Go deeper the why, for readers who want it

A generic function is compiled once for each type it is used with, so it runs as fast as if you had written a copy for that type yourself. To hold values of different types together in one list instead, use an interface value.