Generics
Functions, structs, and enums can be parameterized over types. Type parameters are declared in angle brackets after the function or type name, and can then be used like any other type within the declaration.
void foo<T>(T t) {
}
T bar<T>(T t) {
return t;
}
void main() {
foo<int>(1);
foo<bool>(false);
// Type arguments can usually be inferred from the call arguments,
// so they don't have to be specified explicitly.
var b = bar("bar"); // b has type 'string'
println(b); // prints bar
}Generic structs work the same way. The type arguments are specified when instantiating the struct, or inferred when possible.
void main() {
var _list = List<int>();
var _boolList = List([true, false]);
var _map = Map<int, int>();
}Generic enums work the same way. The type parameters can be used in the associated values of any case.
enum Opt<T> {
Some(T value),
None,
}
void main() {
var _present = Opt.Some(1);
Opt<int> _absent = None;
}Generic constraints
A type parameter can be constrained to only accept types that
implement one or more interfaces. Constraints
are written after a colon following the parameter name, joined with
+.
interface Fooable {
int foo();
}
struct X: Fooable {
int foo() {
return 42;
}
}
void callFoo<T: Fooable>(T& f) {
println(f.foo());
}
void main() {
var x = X();
callFoo(x); // prints 42
}Multiple constraints are all required:
interface Fooable {
int foo();
}
struct Both: Fooable, Hashable {
int foo() { return 42; }
uint64 hash() { return 0; }
}
void callBoth<T: Fooable + Hashable>(T& value) {
println(value.foo());
println(value.hash());
}
void main() {
callBoth(Both());
}Standard library types use constraints in the same way, for example
Map requires its keys to be hashable:
struct MapEntry<Key: Hashable, Value> {
Key key;
Value value;
}Integer parameters
Generic parameters can also take integer values instead of types. An
integer parameter is declared like a function parameter, with an integer
type and a name (e.g. int N). It can then size an array
(T[N]) within the declaration. Integer arguments are given
explicitly or inferred from matching array sizes. The standard library
uses this for fixed-size arrays:
void main() {
int[3] a = [10, 20, 30];
println(a.size()); // prints 3
println(a[2]); // prints 30
}Only non-negative integer literals (and references to other integer parameters) are accepted as integer arguments. Integer parameters can be used as compile-time values, including as an array size.
Generic interfaces
Interfaces can also take type parameters. This pays off when the
interface needs to talk about a type: Iterator<T>'s
value() returns T, so one interface serves
every element type while keeping everything statically typed. A generic
algorithm constrained on Iterator<int> accepts any
implementation, standard or hand-written, and knows value()
returns int:
int sumFirst<It: Iterator<int>>(It& iterator, int n) {
var total = 0;
var i = 0;
while i < n && iterator.hasValue() {
total += iterator.value();
iterator.increment();
i++;
}
return total;
}
struct Counter: Iterator<int> {
int current;
bool hasValue() {
return current > 0;
}
int value() {
return current;
}
void increment() {
current--;
}
}
void main() {
println(sumFirst((1..10).iterator(), 3)); // prints 6
var counter = Counter(3);
println(sumFirst(counter, 2)); // prints 5
}Without the type parameter, each element type would need its own
interface, and value() couldn't declare what it returns.
See Iterators for more on the
Iterator interface.