Generics
Generics allow you to create a single definition that can apply to multiple types. They are created by adding type parameters inside angle brackets <> in your definitions. You can add generics to functions, enums, and structs.
Functions
Adding type parameters to function definitions allows you to use them in the input and output types:
fn map<type t, type u>(T[] arr, (fn(T) U) apply) U[] {
mut U[] new_arr = []
for i in arr {
new_arr = new_arr <> [apply(arr[i])]
}
return new_arr
}When a generic function is called, its type parameters must be written explicitly:
test "generic function call" {
int[] arr = [1, 2, 3, 4, 5]
str[] arr2 = map<int, str>(arr, fn(int n) str { return "{n}" })
assert arr2 == ["1", "2", "3", "4", "5"]
}Enums
Adding type parameters to enums allows you to use them as the types inside enum members:
enum Result<type t, type e> {
Ok(T)
Err(E)
}When using the enum as a type, you must specify the type parameters explicitly:
Result<int, str> res = Result.Ok(1)
if res {
Result.Ok(n) -> {
// `n` is of type `int`
}
Result.Err(e) -> {
// `e` is of type `str`
}
}Structs
Adding type parameters to structs allows you to use them as the types for their fields:
struct Data<type t> {
T data
}When using the struct as a type, you must specify the type parameters explicitly:
test "generic struct" {
Data<str> data = Data<str>{.data = "hello"}
assert data.data == "hello"
}Chaining generics
You can of course chain together generics with different types and pass type parameters through them:
struct Data<type t> {
T data
}
enum Result<type t, type e> {
Ok(T)
Err(E)
}
Result<data<str>, str> val = Result.Ok(Data<str>{.data = "hello"})
if val {
Result.Ok(v) -> { @println("{v.data}") } // outputs "hello"
Result.Err(e) -> { @eprintln("error: {e}") }
}Interfaces
You can add constraints on your type parameters using interfaces.