Mojo Generics
Generics let you write one function or struct that works correctly for many different types without duplicating code. You define the logic once using a type placeholder, and the compiler generates a separate optimized version for each concrete type you actually use. The result is type-safe, fast, and DRY (Don't Repeat Yourself).
The Vending Machine Analogy
Without generics — one machine per item: CandyMachine.dispense() → only dispenses candy ChipsMachine.dispense() → only dispenses chips DrinkMachine.dispense() → only dispenses drinks With generics — one universal machine: VendingMachine[T].dispense() → dispenses ANY item T VendingMachine[Candy].dispense() → dispenses candy VendingMachine[Chips].dispense() → dispenses chips VendingMachine[Drink].dispense() → dispenses drinks One blueprint, infinite specializations.
Generic Functions
A generic function uses a type parameter in square brackets. The compiler substitutes the actual type when you call the function.
fn swap[T: AnyType](inout a: T, inout b: T):
var temp = a
a = b
b = temp
fn main():
var x = 10
var y = 20
swap(x, y)
print(x, y) # 20 10
var p = "hello"
var q = "world"
swap(p, q)
print(p, q) # world hello
Compiler generates two separate functions: swap[Int]: var temp: Int = a; a = b; b = temp swap[String]: var temp: String = a; a = b; b = temp Same source, different machine code — each optimal for its type.
Generic Structs
struct Pair[T: AnyType, U: AnyType]:
var first: T
var second: U
fn __init__(inout self, first: T, second: U):
self.first = first
self.second = second
fn main():
var name_age = Pair[String, Int]("Alice", 30)
var lat_lng = Pair[Float64, Float64](35.6895, 139.6917)
var flag_count = Pair[Bool, Int](True, 42)
print(name_age.first, name_age.second) # Alice 30
print(lat_lng.first, lat_lng.second) # 35.6895 139.6917
print(flag_count.first, flag_count.second) # True 42
Generic Stack
struct Stack[T: AnyType]:
var _data: List[T]
fn __init__(inout self):
self._data = List[T]()
fn push(inout self, item: T):
self._data.append(item)
fn pop(inout self) raises -> T:
if len(self._data) == 0:
raise Error("Stack is empty")
return self._data.pop()
fn peek(self) raises -> T:
if len(self._data) == 0:
raise Error("Stack is empty")
return self._data[len(self._data) - 1]
fn size(self) -> Int:
return len(self._data)
fn is_empty(self) -> Bool:
return len(self._data) == 0
fn main() raises:
var int_stack = Stack[Int]()
int_stack.push(1)
int_stack.push(2)
int_stack.push(3)
print(int_stack.peek()) # 3
print(int_stack.pop()) # 3
print(int_stack.size()) # 2
var str_stack = Stack[String]()
str_stack.push("a")
str_stack.push("b")
print(str_stack.pop()) # b
Constrained Generics with Traits
Use a trait as the type parameter bound to guarantee the generic function can call specific methods on its argument. Without a bound, the compiler only knows the type is "some type" and cannot allow any method calls.
trait Printable:
fn to_string(self) -> String: ...
struct Dog(Printable):
var name: String
fn __init__(inout self, n: String): self.name = n
fn to_string(self) -> String: return "Dog: " + self.name
struct Point(Printable):
var x: Int
var y: Int
fn __init__(inout self, x: Int, y: Int): self.x = x; self.y = y
fn to_string(self) -> String:
return "Point(" + String(self.x) + "," + String(self.y) + ")"
# T must implement Printable — compiler enforces this
fn display[T: Printable](item: T):
print(item.to_string())
fn main():
var d = Dog("Rex")
var p = Point(3, 7)
display(d) # Dog: Rex
display(p) # Point(3,7)
Trait bound diagram:
display[T: Printable](item: T)
↑
"T must implement Printable"
│
Compiler checks at call site:
display(Dog) → Dog implements Printable? ✓ allowed
display(Int) → Int implements Printable? ✗ compile error
Generic min and max
trait Comparable:
fn __lt__(self, other: Self) -> Bool: ...
fn minimum[T: Comparable](a: T, b: T) -> T:
return a if a < b else b
fn maximum[T: Comparable](a: T, b: T) -> T:
return b if a < b else a
fn clamp[T: Comparable](value: T, lo: T, hi: T) -> T:
return minimum(maximum(value, lo), hi)
Multiple Type Parameters
fn zip_apply[T: AnyType, U: AnyType, V: AnyType](
a: T,
b: U,
func: fn(T, U) -> V
) -> V:
return func(a, b)
fn main():
fn concat(s: String, n: Int) -> String:
return s + String(n)
var result = zip_apply[String, Int, String]("Score: ", 100, concat)
print(result) # Score: 100
Generics vs Overloading
Overloading — write separate functions per type:
fn describe(x: Int) -> String: return "int " + String(x)
fn describe(x: Float64) -> String: return "float " + String(x)
fn describe(x: String) -> String: return "string " + x
(3 definitions, must update all when logic changes)
Generics — write one function with a type parameter:
fn describe[T: Stringable](x: T) -> String:
return str(x)
(1 definition, works for any Stringable type including future ones)
Use overloading when each type needs different logic.
Use generics when the logic is identical for all types.
Key Takeaways
Generics use type parameters in square brackets to write one function or struct for many types. The compiler generates a separate optimized implementation for each concrete type used. Use AnyType as the bound for maximum flexibility, or a trait name to restrict the parameter to types with specific capabilities. Trait-bounded generics enable method calls on the type parameter. Generics eliminate code duplication while preserving both type safety and performance — the same goals as hand-written per-type code, achieved in a fraction of the lines.
