Gleam Generic Types
Generic types write once and work for any value type. Instead of writing a separate function for lists of integers, lists of strings, and lists of booleans, you write one generic function that works for all of them. This is the foundation of reusable, type-safe code.
The Problem Generics Solve
Without generics — repeated code for each type:
──────────────────────────────────────────────────
pub fn first_int(list: List(Int)) -> Option(Int) { ... }
pub fn first_str(list: List(String)) -> Option(String) { ... }
pub fn first_bool(list: List(Bool)) -> Option(Bool) { ... }
With generics — one function for all types:
──────────────────────────────────────────────────
pub fn first(list: List(a)) -> Option(a) { ... }
The letter a is a type parameter. It stands for "any type." When you call first([1, 2, 3]), Gleam fills in a = Int. When you call first(["x", "y"]), Gleam fills in a = String.
Type Parameters by Convention
Naming Conventions
──────────────────────────────────────────────────
Single parameter: a
Second parameter: b
Third parameter: c
Descriptive names: value, key, error, element
Type parameters are lowercase single letters or lowercase words. Uppercase names are reserved for concrete types like Int, String, and Bool.
Generic Functions
pub fn identity(value: a) -> a {
value
}
pub fn constant(value: a, _ignored: b) -> a {
value
}
identity() — type flows through:
──────────────────────────────────────────────────
identity(42) → Gleam infers a = Int → returns 42
identity("hello") → Gleam infers a = String → returns "hello"
identity(True) → Gleam infers a = Bool → returns True
Generic Data Structures
Custom types use type parameters to hold any kind of value:
type Wrapper(a) {
Wrapper(value: a)
}
let int_wrap: Wrapper(Int) = Wrapper(value: 99)
let str_wrap: Wrapper(String) = Wrapper(value: "Gleam")
type Stack(a) {
Stack(items: List(a))
}
pub fn push(stack: Stack(a), item: a) -> Stack(a) {
Stack(items: [item, ..stack.items])
}
pub fn pop(stack: Stack(a)) -> #(Option(a), Stack(a)) {
case stack.items {
[] -> #(None, stack)
[top, ..rest] -> #(Some(top), Stack(items: rest))
}
}
Stack Operations
──────────────────────────────────────────────────
Empty stack: Stack(items: [])
push(stack, 1) → Stack(items: [1])
push(stack, 2) → Stack(items: [2, 1])
push(stack, 3) → Stack(items: [3, 2, 1])
pop() → top = Some(3), stack = Stack([2, 1])
pop() → top = Some(2), stack = Stack([1])
Multiple Type Parameters
type Either(a, b) {
Left(a)
Right(b)
}
let success: Either(String, Int) = Left("All good")
let code: Either(String, Int) = Right(404)
Generic Functions with Constraints
Gleam does not have type classes, but you can pass comparison or transformation functions to work around this:
pub fn find(list: List(a), target: a, equal: fn(a, a) -> Bool) -> Option(a) {
case list {
[] -> None
[head, ..tail] ->
case equal(head, target) {
True -> Some(head)
False -> find(tail, target, equal)
}
}
}
// Usage:
let result = find([1, 2, 3, 4], 3, fn(a, b) { a == b })
// Some(3)
How the Standard Library Uses Generics
Standard Library Generic Types
──────────────────────────────────────────────────
List(a) → a list of any element type
Option(a) → Some(a) or None
Result(a, b) → Ok(a) or Error(b)
Map(k, v) → keys of type k, values of type v
Every list, option, and result you use is a generic type. The a in List(a) becomes Int for List(Int), String for List(String), and so on.
Practical Example — Generic Pair Swap
type Pair(a, b) {
Pair(first: a, second: b)
}
pub fn swap(pair: Pair(a, b)) -> Pair(b, a) {
Pair(first: pair.second, second: pair.first)
}
pub fn map_first(pair: Pair(a, b), f: fn(a) -> c) -> Pair(c, b) {
Pair(first: f(pair.first), second: pair.second)
}
pub fn main() {
let p = Pair(first: "hello", second: 42)
let swapped = swap(p)
// Pair(first: 42, second: "hello")
let doubled = map_first(Pair(first: 5, second: "x"), fn(n) { n * 2 })
// Pair(first: 10, second: "x")
}
Key Points
Generic Types Essentials
──────────────────────────────────────────────────
1. Type parameters use lowercase names: a, b, value
2. Write once — works for any concrete type
3. Gleam infers the concrete type at each call site
4. Generic custom types: type Box(a) { Box(value: a) }
5. Generic functions: pub fn wrap(x: a) -> Box(a)
6. Multiple parameters: type Map(k, v), Result(ok, err)
7. No runtime cost — all resolved at compile time
Generics eliminate duplication without sacrificing type safety. The compiler still knows the exact type at every call site — generics are just a way of writing the same safe code once instead of many times.
