Generics
Generic functions and structs are used to write pieces of code that can be reused for different types. A generic function looks like this:
func identity[T](value: T) -> T {
return value;
}
The function identity has a type parameter T that that is used as the type
of the argument value. When calling this function, we provide a type for the
type parameter:
func main() {
identity[i32](42);
identity[bool](true);
}
In most situations, the type arguments can be inferred:
func main() {
identity(42);
identity(true);
}
Type Constraints
The identity function simply returns its argument, which clearly isn’t very
useful. In order to do something interesting with our argument, we have to
constrain our type parameter, which means restricting which types are allowed.
The function read_line in the following example has a type parameter T that
accepts any type that implements io.Read from the standard library:
use std.{file.File, io};
pub func read_line[T: io.Read](stream: T) -> String {
var line = "";
var char = stream.read_u8().unwrap();
while char != '\n' {
line += char;
char = stream.read_u8().unwrap();
}
return line;
}
We can now use this function with all types that satisfy this constraint:
func main() {
# Reading a line from stdin
var stream = io.stdin();
println(read_line(stream));
# Reading a line from a file
var file = File.open("file.txt", File.Mode.READ).unwrap();
println(read_line(file));
}
For all other types, we get an error:
func main() {
read_line(0); # ERROR: 'i32' does not satisfy type constraint 'Read'
}
A type constraint can also specify multiple required protocols:
func access[T: io.Read + io.Write](stream: T) {
...
}
Operators
To use operators on generic types, we can constrain their type using the
corresponding protocols from std.protos. Here’s a generic function that
returns the maximum of two numbers:
use std.protos.Order;
func max[T: Order[T]](a: T, b: T) -> T {
if a > b {
return a;
} else {
return b;
}
}
Here’s a list of the protocols in std.protos and the operators they provide:
Protocol |
Operators |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Structs
Structs support type parameters as well:
struct Pair[A, B] {
var a: A;
var b: B;
pub func new(a: A, b: B) -> Pair[A, B] {
return Pair[A, B] { a, b };
}
}
func main() {
var pair1 = Pair[f32, bool] {
a: 0.5,
b: true,
};
var pair2 = Pair[String, i32].new("text", 10);
}
Parameter Sequences
If you want a function to take an arbitrary number of parameters, you can use parameter sequences:
func print_all[T: ...](values: T) {
print("values: ");
meta for value in values {
print(value);
print(" ");
}
print("\n");
}
func main() {
print_all("A", 1.5);
print_all(100);
print_all();
}
The function print_all has a parameter sequence (T: ...) as a generic
parameter. All arguments passed to the function are packed together into a tuple
and stored in the parameter values. A meta for statement is used to iterate
over the values in the tuple and print them.