Skip to content

Generics

Generic functions take one or more type parameters between <>. The compiler infers T from the call-site arguments — no explicit annotation needed. Functions with multiple type parameters (<A, B>) track each type independently:

identity<T>, head<T>, and pair_first<A, B> work with int, str, bool, and arrays.

10-generic-fn.zolo
Playground
// Feature: Generic functions

// Syntax: `fn name<T>(x: T) -> T { ... }`

// When to use: logic that works with ANY type (identity, containers,

// swaps) preserving the concrete type through the return value.


// Identity — `T` is inferred at the call site.

fn identity<T>(x: T) -> T {
  return x
}

print(identity(42))  // 42

print(identity("hi"))  // hi

print(identity(true))  // true


// More than one arg of the same `T`.

fn first<T>(a: T, b: T) -> T {
  return a
}

print(first(1, 2))  // 1

print(first("a", "b"))  // a


// Generic with array.

fn head<T>(arr: [T]) -> T {
  return arr[0]
}

print(head([10, 20, 30]))  // 10

print(head(["x", "y"]))  // x


// Multiple type params.

fn pair_first<A, B>(a: A, b: B) -> A {
  return a
}

print(pair_first(7, "right-side"))  // 7

// expected:

// 42

// hi

// true

// 1

// a

// 10

// x

// 7

Generic structs store any type without losing information about which one it is. The concrete type is inferred from the construction literal. The impl block of a generic struct carries along the type parameter:

Box<T>, Pair<A, B>, and Stack<T> with impl Stack including an associated method and an instance method.

11-generic-struct.zolo
Playground
// Feature: Generic structs
// Syntax: `struct Name<T> { field: T }`
// When to use: container that holds any type while preserving it.

struct Box<T> {
  value: T,
}

// Concrete type comes from usage.
let bi = Box { value: 42 }  // Box<int>
let bs = Box { value: "hello" }  // Box<str>
print(bi.value)  // 42
print(bs.value)  // hello

// Multiple type parameters.
struct Pair<A, B> {
  first: A,
  second: B,
}

let p = Pair { first: 1, second: "one" }
print(p.first)  // 1
print(p.second)  // one

// `impl` for generic struct — the `<T>` is inferred from the type name.
// NOTE: `len` is dispatched at the runtime level for Array/Map, so we
// expose the size under a distinct name (`size`) to avoid the clash.
struct Stack<T> {
  items: [T],
}

impl Stack {
  fn make() -> Stack {
    return Stack { items: [] }
  }

  fn size(self) -> int {
    return self.items.len()
  }
}

let s = Stack::make()
print(s.size())  // 0

Generic enums are the foundation of parametrized sum types — the same mechanism the stdlib uses for Option<T> and Result<T, E>. match destructures the payload and binds the variable to the concrete type:

Maybe<T> with variants Just(T) and Nothing; Either<L, R> with two parameters.

12-generic-enum.zolo
Playground
// Feature: Generic enums
// Syntax: `enum Name<T> { Variant(T), Other }`
// When to use: parameterized sum types — this is how the stdlib
// defines `Option<T>` and `Result<T, E>`.

enum Maybe<T> {
  Just(T),
  Nothing,
}

// Construction: the `T` type comes from the payload.
let just_int = Maybe.Just(42)
let just_str = Maybe.Just("hello")
let none: Maybe<int> = Maybe.Nothing

let r1 = match just_int {
  Maybe::Just(v) => "got {v}",
  Maybe::Nothing => "none",
}
print(r1)  // got 42

let r2 = match just_str {
  Maybe::Just(v) => "got '{v}'",
  Maybe::Nothing => "none",
}
print(r2)  // got 'hello'

let r3 = match none {
  Maybe::Just(v) => "got {v}",
  Maybe::Nothing => "none",
}
print(r3)  // none

// Result<T, E> with two type parameters.
enum Either<L, R> {
  Left(L),
  Right(R),
}

let lf = Either.Left("error")
let rt = Either.Right(99)

print(match lf {
  Either::Left(s) => "L:{s}",
  Either::Right(n) => "R:{n}",
})

// L:error

print(match rt {
  Either::Left(s) => "L:{s}",
  Either::Right(n) => "R:{n}",
})
// R:99

Challenge

Write a generic function swap<A, B>(a: A, b: B) -> Pair<B, A> that returns the values in reversed order. Use the Pair struct from the generic structs example.

enespt-br