Same Shape, Different Substance
A cup holds water. A cup holds sand. The cup doesn't care what fills it---its shape accommodates many substances. This is the essence of generics (also called parametric types): structures that work regardless of what they contain.
The Container Pattern
We've already seen this. Maybe doesn't care what it might contain:
Maybe Number: Nothing OR Just(42)
Maybe Piece: Nothing OR Just(white_knight)
Maybe Text: Nothing OR Just("hello")The structure is identical. Only the contents differ.
Maybe A:
Nothing OR Just(value: A)This is a generic type---also called a parametric type---a type with a "hole" that gets filled with another type.
Functions That Work on Any Container
Our map function doesn't care what's inside the Maybe:
map(maybe, transform):
match maybe:
case Nothing: Nothing
case Just(x): Just(transform(x)) This works for Maybe Number, Maybe Piece, Maybe Text---any Maybe at all.
map(Just(5), (x) => x * 2) // Just(10)
map(Just(pawn), (p) => p.color) // Just(white)
map(Just("hi"), (s) => s + "!") // Just("hi!")
map(Nothing, (x) => anything) // NothingLists Are Generic Too
A list doesn't care what it contains:
List Number: [1, 2, 3]
List Piece: [pawn, knight, bishop]
List Text: ["a", "b", "c"]
List A: [... elements of type A ...] And map over lists works the same way:
map([1, 2, 3], (x) => x * 2) // [2, 4, 6]
map(pieces, (p) => p.color) // [white, white, black, ...]The Shape of Transformation
Notice the pattern:
map over Maybe A: Maybe A → (A → B) → Maybe B
map over List A: List A → (A → B) → List B
map over Result A E: Result A E → (A → B) → Result B EThe shape is always the same:
Container A → (A → B) → Container BTransform the contents without changing the container's structure.
Why This Matters
Generic types let us:
- Write once, use everywhere: One
mapfunction works for all functors - Reason abstractly: We know
mappreserves structure without knowing what's inside - Compose freely: Generic functions combine with each other regardless of the specific types
// This works for ANY functor F and ANY types A, B, C:
container // F A
|> map((a) => f(a)) // F B
|> map((b) => g(b)) // F CChess Through Generic Eyes
Our chess types can use generics:
// A move that might not exist
find_best_move(state): Maybe Move
// A list of all legal moves
get_legal_moves(state): List Move
// A move that might fail validation
validate_move(state, move): Result Move Error All three use generic containers. All three work with our generic functions like map and flatMap.
Generic types give us containers that work with any contents. But what about operations that work differently depending on the type? In the next chapter, we explore polymorphic behavior---same name, different implementation.