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 likePair<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:12The 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.