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:

python
    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.

python
    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)           // Nothing

Lists Are Generic Too

A list doesn't care what it contains:

python
    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:

python
    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:

python
    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 E

The shape is always the same:

    Container A  →  (A → B)  →  Container B

Transform the contents without changing the container's structure.

Why This Matters

Generic types let us:

  1. Write once, use everywhere: One map function works for all functors
  2. Reason abstractly: We know map preserves structure without knowing what's inside
  3. Compose freely: Generic functions combine with each other regardless of the specific types
elixir
    // 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 C

Chess Through Generic Eyes

Our chess types can use generics:

python
    // 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.