Generics
The problem
You have already written functions like find_max that work on arrays of integers. But what if you want the same logic for strings? Or for structs? Without generics, you would need to write a separate function for each type:
fn max_int(a: int, b: int) -> int { if a > b { return a } return b } fn max_string(a: String, b: String) -> String { if a > b { return a } return b }
The logic is identical — only the types change. Generics solve this duplication.
What are generics?
Generics let you write code that works with any type. Instead of hardcoding int or String, you use a type parameter — a placeholder that gets filled in when the function is used.
fn max_val<T>(a: T, b: T) -> T { if a > b { return a } return b } fn main() { print(max_val<int>(10, 20)) // 20 print(max_val<String>("apple", "banana")) // banana }
The after the function name declares a type parameter called T. When you call max_val, the compiler replaces every T with int and generates a specialized version of the function.
Generic functions
Here are more examples of generic functions:
fn identity<T>(x: T) -> T { return x } fn first_of_two<T>(a: T, b: T) -> T { return a } fn main() { let n: int = identity<int>(42) let s: String = identity<String>("hello") print(n) // 42 print(s) // hello }
Multiple type parameters
Functions can have more than one type parameter:
struct Pair<A, B> { first: A, second: B } fn make_pair<A, B>(a: A, b: B) -> Pair<A, B> { return Pair<A, B> { first: a, second: b } } fn main() { let p1: Pair<String, int> = make_pair<String, int>("age", 30) print(p1.first) // age print(p1.second) // 30 let p2: Pair<int, int> = make_pair<int, int>(10, 20) print(p2.first) // 10 print(p2.second) // 20 }
Generic structs
Structs can be generic too:
struct Box<T> { value: T } fn main() { let int_box: Box<int> = Box<int> { value: 42 } let str_box: Box<String> = Box<String> { value: "hello" } print(int_box.value) // 42 print(str_box.value) // hello }
Generic enums
Enums work with generics naturally. In fact, you have already seen a pattern like this:
enum Option<T> { Some(T), None } fn safe_div(a: int, b: int) -> Option<int> { if b == 0 { return Option.None } return Option.Some(a / b) } fn main() { let r1: Option<int> = safe_div(10, 3) let r2: Option<int> = safe_div(10, 0) let msg1: String = match r1 { Option.Some(v) => "Result: " + int_to_string(v), Option.None => "No result" } let msg2: String = match r2 { Option.Some(v) => "Result: " + int_to_string(v), Option.None => "No result" } print(msg1) // Result: 3 print(msg2) // No result }
With generics, Option works for any type — not just int.
Generics with trait bounds
You can combine generics with traits to say "this works for any type that has certain capabilities":
trait Display { fn to_string(self) -> String } struct Person { name: String, age: int } impl Display for Person { fn to_string(self) -> String { return self.name + " (" + int_to_string(self.age) + ")" } } fn print_all<T: Display>(items: Array) { var i: int = 0 while i < items.length() { let item: T = items[i] print(item.to_string()) i += 1 } } fn main() { let people: Array = [ Person { name: "Alice", age: 30 }, Person { name: "Bob", age: 25 } ] print_all<Person>(people) }
Output:
Alice (30) Bob (25)
How it works: monomorphization
When the compiler sees max_val, it generates a version of max_val where every T is replaced with int. If you also call max_val, it generates a second version. This process is called monomorphization.
The result is that generic code runs at the same speed as code you wrote by hand for each type — there is no runtime cost.
Your code: max_val<T>(a: T, b: T) -> T
Compiler generates: max_val_int(a: int, b: int) -> int
max_val_string(a: String, b: String) -> String
Practical example: a generic stack
struct Stack<T> { items: Array } fn stack_new<T>() -> Stack<T> { return Stack<T> { items: [] } } fn stack_push<T>(s: Stack<T>, item: T) -> Stack<T> { s.items.push(item) return s } fn stack_pop<T>(s: Stack<T>) -> T { return s.items.pop() } fn stack_is_empty<T>(s: Stack<T>) -> bool { return s.items.length() == 0 } fn stack_size<T>(s: Stack<T>) -> int { return s.items.length() } fn main() { var s: Stack<int> = stack_new<int>() s = stack_push<int>(s, 10) s = stack_push<int>(s, 20) s = stack_push<int>(s, 30) print(stack_size<int>(s)) // 3 let top: int = stack_pop<int>(s) print(top) // 30 }
Practical example: a generic result type
enum Result<T, E> { Ok(T), Err(E) } fn parse_number(s: String) -> Result<int, String> { if s.length() == 0 { return Result.Err("Empty string") } // Simple check: only digits var i: int = 0 while i < s.length() { let c: int = s.charAt(i) if c < 48 or c > 57 { return Result.Err("Not a number: " + s) } i += 1 } return Result.Ok(string_to_int(s)) } fn main() { let r1: Result<int, String> = parse_number("42") let r2: Result<int, String> = parse_number("abc") let msg1: String = match r1 { Result.Ok(n) => "Parsed: " + int_to_string(n), Result.Err(e) => "Error: " + e } let msg2: String = match r2 { Result.Ok(n) => "Parsed: " + int_to_string(n), Result.Err(e) => "Error: " + e } print(msg1) // Parsed: 42 print(msg2) // Error: Not a number: abc }
Exercises
- Write a generic
Stackwithpush,pop,peek(look at top without removing), andsizemethods.
- Create a generic
enum Option. Write a function{ Some(T), None } unwrap_orthat returns the value inside(opt: Option , default: T) -> T Someor the default ifNone.
- Write a generic
find_firstthat returns the first matching element wrapped in(items: Array, predicate: Fn) -> Option Option.Some, orOption.None.
- Create a
Resultenum and use it to write asafe_get(arr: Array, index: int) -> Resultthat returnsErrfor out-of-bounds access.
Summary
- Generics let you write code that works with any type:
fn f.(x: T) -> T - Type parameters go in angle brackets:
,,. - Structs and enums can be generic:
struct Box,enum Option. - Trait bounds constrain generics:
means "T must implement Display." - Nyx uses monomorphization — generic code compiles to specialized versions with zero runtime cost.
- Common generic patterns:
Optionfor optional values,Resultfor error handling,Pairfor tuples.
Next chapter: Networking — TCP servers →