burn / docs

Language

Generics

Functions, records and structs can take type parameters in <...>, so one piece of code works for many types.

Generic functions

fun firstOr<T>(items: [T], fallback: T): T {
    if (len(items) == 0) {
        return fallback
    }
    return items[0]
}

fun mapList<T, U>(items: [T], f: fun(T): U): [U] {
    [U] out = []
    for x in items {
        out.push(f(x))
    }
    return out
}

print(firstOr([3, 4], 0), firstOr([], "none"))
print(mapList([1, 2, 3], fun(x: int): string { return "#${x}" }))

Burn works out the type arguments from the arguments of each call, so calls look like normal calls. When two arguments disagree, numbers widen like elsewhere: pairUp(1, 2.5) with fun pairUp<T>(a: T, b: T) makes T a float.

A generic function is called directly; it cannot be stored in a variable or passed as a value (`id` has type parameters, so it cannot be used as a value). Wrap it in a lambda with concrete types to pass it on.

Generic records and structs

def type and def struct take type parameters after the name:

def type Pair<A, B> {
    first: A
    second: B
}

def struct Stack<T>() {
    [T] items = []

    fun push(item: T) {
        items.push(item)
    }

    fun pop(): T? {
        if len(items) == 0 {
            return null
        }
        return items.pop()
    }
}

fun swap<A, B>(p: Pair<A, B>): Pair<B, A> {
    return Pair { first: p.second, second: p.first }
}

var p = Pair { first: 1, second: "one" }
Pair<string, bool> q = Pair { first: "yes", second: true }
print(swap(p), q)

var s = new Stack<int>()
s.push(1)
s.push(2)
print(s.pop(), s.pop(), s.pop())
  • Inside the type, the parameters are used like any type: fields, method parameters and results, and nested types such as Node<T>? next.
  • Type arguments are inferred from the constructor arguments or the field values. Write them when nothing tells Burn what they are: new Stack<int>() for an empty stack, or a declared type like Pair<string, bool> q.
  • Values print with their type arguments: Pair<string, int> { first: "one", second: 1 }.
  • A generic struct can implement interfaces, and then works wherever the interface is expected.

What a type parameter can do

There are no constraints to declare. A type parameter can be used in any way that every type it is actually used with allows:

fun largest<T>(items: [T]): T {
    var best = items[0]
    for x in items {
        if (x > best) {
            best = x
        }
    }
    return best
}

print(largest([3, 9, 2]), largest(["pear", "apple"]))

largest works for int, float and string, which all have >. Calling it with [true] is an error that points at the comparison and names the version that failed:

error: cannot order bool and bool with `>`
 --> app.bn:4:12
  |
4 |         if (x > best) {
  |            ^^^^^^^^^^
  = note: this is the version `largest<bool>`, used at app.bn:12

The same holds for methods: a generic function can call x.area() on a T, and it compiles for every type that has an area method.

How generics are compiled

The compiler makes a separate copy of a generic function or type for each set of type arguments it is used with: largest<int> and largest<string> are two functions in the compiled program, each checked and optimized for its types. This is what C++ and Rust do, and it means:

  • generic code runs exactly as fast as code written by hand for one type; there is no boxing and no type check at runtime;
  • errors are reported for the version that fails, with the place that asked for it;
  • a generic function that is never called produces no code, and is not checked beyond its syntax and signatures.

Limits

  • Methods cannot have their own type parameters; only the struct's parameters can be used in them. Write a generic function outside the struct instead and call it with method syntax.
  • Generic structs cannot extend another struct, and cannot have static members.
  • Enums, interfaces and type aliases (def type Name = ...) cannot take type parameters.
  • Type arguments cannot be written on a function call (first<int>(xs)); give the argument a type instead.