ktm v0.28.3

Generics

Functions, structs and methods can take type parameters: one definition, written once, used with many types. Each use is compiled for its types, producing the same code you would write by hand, with no void * and no casts. This chapter covers generic functions, how the types are worked out at a call, generic structs and their methods, methods with type parameters of their own, generic bases, the library's generic containers, and the errors generics give.

Generic functions

Type parameters go in <...> after the function's name; they can be used anywhere a type can in the parameters, the return type and the body.

T max<T>(T a, T b) {
    return a > b ? a : b;
}

i32 main() {
    printf("%d\n", max(3, 7));
    printf("%.1f\n", max(1.5, 0.5));
    printf("%c\n", max('a', 'z'));
    return 0;
}
7
1.5
z

Each type max is called with gets its own compiled copy, an instance: max<i64>, max<f64> and max<u8> above. They are as fast as three hand-written functions.

A generic function can take pointers to its type parameter:

void swap<T>(T *a, T *b) {
    T t = *a;
    *a = *b;
    *b = t;
}

i32 main() {
    i64 x = 1, y = 2;
    swap(&x, &y);
    u8 *s = "left", *t = "right";
    swap(&s, &t);
    printf("%d %d %s %s\n", x, y, s, t);
    return 0;
}
2 1 right left

And slices of it:

T sum<T>(T[] xs) {
    T total = 0;
    for (i64 i = 0; i < xs.len; i++) {
        total += xs[i];
    }
    return total;
}

i32 main() {
    i64 ints[3] = {1, 2, 3};
    f64 floats[2] = {0.5, 0.25};
    printf("%d %.2f\n", sum(ints), sum(floats));
    return 0;
}
6 0.75

Several type parameters are separated by commas:

B convert<A, B>(A a, fn(A) B f) {
    return f(a);
}

f64 half(i64 x) {
    return (f64)x / 2.0;
}

i32 main() {
    printf("%.1f\n", convert(7, &half));
    return 0;
}
3.5

Working out the types

At a call, ktm works the type parameters out from the arguments: through plain values, pointers, slices, fn types and generic structs. When the arguments disagree, or don't mention a parameter at all, write the types after the name:

T max<T>(T a, T b) {
    return a > b ? a : b;
}

i32 main() {
    printf("%.1f\n", max<f64>(1, 2.5));
    printf("%d\n", max<i64>(-3, 2));
    return 0;
}
2.5
2

With max<f64>, the 1 becomes 1.0, as for any f64 parameter. A type parameter that only appears in the return type has to be written, since no argument says what it is:

T zero<T>() {
    T z;
    return z;
}

i32 main() {
    i64 n = zero<i64>();
    f64 f = zero<f64>();
    printf("%d %.1f\n", n, f);
    return 0;
}
0 0.0
does not compileT zero<T>() {
    T z;
    return z;
}

i32 main() {
    i64 n = zero();
    return 0;
}
can't work out the type arguments of `zero`: nothing says what `T` is

Generic structs

A struct can take type parameters too. The type is always written with its arguments, nested ones included: Pair<u8 *, f64>, Stack<Stack<i64>>.

struct Pair<K, V> {
    K key;
    V value;
};

i32 main() {
    Pair<u8 *, f64> pi = {"pi", 3.14};
    Pair<i64, bool> flag = {7, true};
    printf("%s=%.2f %d=%d\n", pi.key, pi.value, flag.key, flag.value);
    printf("%d %d\n", sizeof(Pair<u8 *, f64>), sizeof(Pair<u8, u8>));
    return 0;
}
pi=3.14 7=1
16 2

Each set of arguments is its own struct type, laid out for its types: Pair<u8, u8> is 2 bytes.

A generic function can make and return one; auto keeps the full type:

struct Pair<K, V> {
    K key;
    V value;
};

Pair<K, V> pairOf<K, V>(K k, V v) {
    Pair<K, V> p = {k, v};
    return p;
}

i32 main() {
    auto e = pairOf("e", 2.72);
    printf("%s=%.2f\n", e.key, e.value);
    printf("%s\n", typeof(e));
    return 0;
}
e=2.72
Pair<u8 *, f64>

typeof(x) is the type of x as text, which helps to see what an instance is.

A generic struct can hold another generic struct:

struct Box<T> {
    T value;
};

struct Shelf<T> {
    Box<T> left;
    Box<T> right;
};

i32 main() {
    Shelf<i64> s = {{1}, {2}};
    Box<Box<u8 *>> nested = {{"deep"}};
    printf("%d %d %s\n", s.left.value, s.right.value, nested.value.value);
    return 0;
}
1 2 deep

Methods of a generic struct

A method of a generic struct is written with the struct's type parameters after its name, Stack<T>.push; inside, T is whatever the instance's T is.

import mem;

struct Stack<T> {
    T *items;
    i64 len;
    i64 cap;
};

void Stack<T>.push(T x) {
    if (self.len == self.cap) {
        self.cap = self.cap ? self.cap * 2 : 4;
        self.items = mem.realloc(self.items, sizeof(T) * self.cap);
    }
    self.items[self.len++] = x;
}

T Stack<T>.pop() {
    return self.items[--self.len];
}

bool Stack<T>.empty() {
    return self.len == 0;
}

void Stack<T>.free() {
    mem.free(self.items);
}

i32 main() {
    scoped Stack<i64> nums;
    for (i64 i = 1; i <= 5; i++) {
        nums.push(i * i);
    }
    while (!nums.empty()) {
        printf("%d ", nums.pop());
    }
    printf("\n");
    scoped Stack<u8 *> words;
    words.push("first");
    words.push("second");
    printf("%s\n", words.pop());
    return 0;
}
25 16 9 4 1 
second

Only the methods an instance uses are compiled for it.

Methods with type parameters of their own

A method can have type parameters of its own, after its name as for a function. It works on a plain struct, and on a generic one next to the struct's parameters:

struct Counter { i64 n; };

i64 Counter.addAll<V>(V[] xs) {
    for (i64 i = 0; i < xs.len; i++) {
        self.n += (i64)xs[i];
    }
    return self.n;
}

i32 main() {
    Counter c = {0};
    u8 bytes[3] = {1, 2, 3};
    f64 floats[2] = {1.5, 2.5};
    printf("%d\n", c.addAll(bytes));
    printf("%d\n", c.addAll(floats));
    return 0;
}
6
9

On a generic struct, a method can map one instance to another:

struct Pair<T> {
    T a;
    T b;
};

Pair<U> Pair<T>.map<U>(fn(T) U f) {
    Pair<U> p = {f(self.a), f(self.b)};
    return p;
}

f64 half(i64 x) {
    return (f64)x / 2.0;
}

u8 *size(f64 x) {
    return x >= 1.0 ? "big" : "small";
}

i32 main() {
    Pair<i64> p = {1, 5};
    Pair<u8 *> s = p.map(&half).map(&size);
    printf("%s %s\n", s.a, s.b);
    return 0;
}
small big

Generic functions over generic structs

A function can take a generic struct and work out its parameters from it:

struct Box<T> {
    T value;
};

T unbox<T>(Box<T> *b) {
    return b.value;
}

void fill<T>(Box<T> *b, T v) {
    b.value = v;
}

i32 main() {
    Box<f64> b = {2.5};
    Box<u8 *> s;
    fill(&s, "packed");
    printf("%.1f %s\n", unbox(&b), unbox(&s));
    return 0;
}
2.5 packed

A generic base

A generic struct can extend another, generic or not, and get its methods (see Methods):

struct List<T> {
    T items[4];
    i64 len;
};

struct Counted<T> : List<T> {
    i64 adds;
};

void List<T>.add(T x) {
    self.items[self.len++] = x;
}

void Counted<T>.addCounted(T x) {
    self.add(x);
    self.adds++;
}

i32 main() {
    Counted<i64> c;
    c.addCounted(10);
    c.addCounted(20);
    printf("%d %d %d %d\n", c.len, c.items[0], c.items[1], c.adds);
    return 0;
}
2 10 20 2

The library's generic containers

The containers are generic: vec.Vec<T>, map.Map<K, V>, set.Set<T>, list.List<T> (see the library). Their constructors take the types:

import map, vec;

i32 main() {
    scoped vec.Vec<u8 *> names = vec.new<u8 *>();
    names.push("ann");
    names.push("bob");
    scoped map.Map<u8 *, i64> ages = map.new<u8 *, i64>();
    ages.set("ann", 41);
    for (i64 i = 0; i < names.len; i++) {
        u8 *n = names.get(i);
        printf("%s %d\n", n, ages.getOr(n, -1));
    }
    return 0;
}
ann 41
bob -1

json.decode<T> is generic too: it decodes into whatever struct T is (see Attributes).

Errors are checked per instance

A generic body is checked for each set of types it is used with. A body that works for numbers fails for a struct, at the operator that doesn't apply, with a note naming the instance and the call that asked for it:

does not compilestruct Point { i64 x, y; };

T max<T>(T a, T b) {
    return a > b ? a : b;
}

i32 main() {
    Point a = {1, 2};
    Point b = {3, 4};
    max(a, b);
    return 0;
}
max<Point>

There are no constraints on type parameters: the instance either compiles or doesn't.

In modules and libraries

A generic declared pub in a module is used as module.name, like any declaration: coll.Stack<i64>, coll.pairOf(1, 2) (see Modules and packages). A library built with ktm -lib keeps its generics as source, and each program compiles the instances it uses.

Not in ktm

Rules