Generics
Generics let you write code that works with any type. Nic uses monomorphization—generic code is specialized at compile time for each concrete type used.
Generic Functions
Use type parameters in square brackets:
nic
fn identity[T](x: T) -> T {
return x;
}
fn main() -> unit {
let a: i32 = identity[i32](42);
let b: string = identity[string]("hello");
return;
}Type Inference
Often the type parameter can be inferred:
nic
fn identity[T](x: T) -> T {
return x;
}
fn main() -> unit {
let a = identity(42); // T inferred as i32
let b = identity("hello"); // T inferred as string
return;
}Multiple Type Parameters
nic
fn make_pair[T, U](a: T, b: U) -> (T, U) {
return (a, b);
}
fn main() -> unit {
let pair: (i32, string) = make_pair(42, "hello");
let (x, y) = pair; // destructure to access elements
return;
}Generic Structs
nic
struct Pair[T, U] {
first: T,
second: U
}
struct Box[T] {
value: T
}
fn main() -> unit {
let p: Pair[i32, string] = Pair{first: 1, second: "one"};
let b: Box[f64] = Box{value: 3.14};
return;
}Generic Enums
nic
enum Option[T] {
Some(T),
None
}
enum Result[T, E] {
Ok(T),
Err(E)
}
fn safe_divide(a: f64, b: f64) -> Result[f64, string] {
if b == 0.0 {
return Err("division by zero");
}
return Ok(a / b);
}Working with Generic Types
nic
fn unwrap_or[T](opt: Option[T], default: T) -> T {
return match opt {
Some(x) -> x,
None -> default
};
}
fn map_option[T, U](opt: Option[T], f: fn(T) -> U) -> Option[U] {
return match opt {
Some(x) -> Some(f(x)),
None -> None
};
}Generic Functions with Constraints
Add trait bounds to require certain capabilities:
nic
trait Display {
show(self) -> string;
}
fn print_all[T: Display](items: []T) -> unit {
for let i = 0; i < items.len; i = i + 1 {
println(items[i].show());
}
return;
}Higher-Order Generics
Combine generics with function types:
nic
fn apply[T, U](f: fn(T) -> U, x: T) -> U {
return f(x);
}
fn compose[A, B, C](f: fn(B) -> C, g: fn(A) -> B) -> fn(A) -> C {
return fn(x: A) -> C { return f(g(x)); };
}
fn main() -> unit {
let double = fn(x: i32) -> i32 { return x * 2; };
let result: i32 = apply(double, 21); // 42
return;
}Recursive Generic Types
nic
enum List[T] {
Nil,
Cons(T, *List[T])
}
fn length[T](list: *List[T]) -> i32 {
return match *list {
Nil -> 0,
Cons(_, tail) -> 1 + length(tail)
};
}
fn map_list[T, U](list: *List[T], f: fn(T) -> U) -> *List[U] {
return match *list {
Nil -> new Nil,
Cons(x, tail) -> new Cons(f(x), map_list(tail, f))
};
}Monomorphization
Nic compiles generics by creating specialized versions for each type:
nic
fn identity[T](x: T) -> T {
return x;
}
fn main() -> unit {
identity(42); // generates identity_i32
identity(3.14); // generates identity_f64
identity("hi"); // generates identity_string
return;
}This gives you:
- Zero runtime overhead
- Full type safety
- Optimized code for each type
Summary
| Feature | Syntax |
|---|---|
| Generic function | fn name[T](x: T) -> T { } |
| Multiple params | fn name[T, U](x: T, y: U) -> (T, U) |
| Generic struct | struct Name[T] { field: T } |
| Generic enum | enum Name[T] { A(T), B } |
| With constraint | fn name[T: Trait](x: T) -> T |
| Explicit type | func[i32](42) |
Next
Learn about Classes for reference types with methods.