Table of Contents

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(10, 20), 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(10, 20), 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

  1. Write a generic function swap(p: Pair) -> Pair that swaps the elements of a pair.
  1. Write a generic Stack with push, pop, peek (look at top without removing), and size methods.
  1. Create a generic enum Option { Some(T), None }. Write a function unwrap_or(opt: Option, default: T) -> T that returns the value inside Some or the default if None.
  1. Write a generic find_first(items: Array, predicate: Fn) -> Option that returns the first matching element wrapped in Option.Some, or Option.None.
  1. Create a Result enum and use it to write a safe_get(arr: Array, index: int) -> Result that returns Err for out-of-bounds access.

Summary

Next chapter: Networking — TCP servers →

← Previous: Traits and impl blocks Next: Networking — TCP servers →