Skip to content

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

FeatureSyntax
Generic functionfn name[T](x: T) -> T { }
Multiple paramsfn name[T, U](x: T, y: U) -> (T, U)
Generic structstruct Name[T] { field: T }
Generic enumenum Name[T] { A(T), B }
With constraintfn name[T: Trait](x: T) -> T
Explicit typefunc[i32](42)

Next

Learn about Classes for reference types with methods.

Released under the MIT License.