Skip to content
Generics

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.