Skip to content

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.

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"]
}

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),
None,
}

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`
}
Result.None -> {}
}

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"
}

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 = Ok(Data<str>{.data = "hello"})
if val {
Result.Ok(v) -> { @println("{v.data}") } // outputs "hello"
Result.Err(e) -> { @eprintln("error: {e}") }
}

You can add constraints on your type parameters using interfaces.