15.1The problem generics solve

Last updated June 13, 2026

Here's a problem you can feel before you can name. Write a function that returns the larger of two i32s:

fn largest_i32(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

Fine. Now you need the same thing for f64. And for char. And maybe u8. Each is the identical logic, if a > b { a } else { b }, with only the type changed:

fn largest_i32(a: i32, b: i32) -> i32 { if a > b { a } else { b } }
fn largest_f64(a: f64, b: f64) -> f64 { if a > b { a } else { b } }
fn largest_char(a: char, b: char) -> char { if a > b { a } else { b } }

Three functions, one idea, copied three times. This is exactly the duplication that lesson 2.5 taught functions to eliminate, except functions can't help here: the thing that varies isn't a value, it's the type. You can't pass a type as an argument to an ordinary function. Generics are how you do precisely that, and this lesson is about the problem clearly enough that the solution feels inevitable.

Why you can't just use one function

Your instinct might be to write one function that takes "any type." The naive attempt fails:

fn largest(a: ???, b: ???) -> ??? {
    if a > b { a } else { b }
}

What type goes in the blanks? It can't be i32, that rejects char. There's no built-in "any type" you can write there, and even if there were, the function body needs to work for whatever type shows up. The duplication is real, and copy-paste is the wrong fix for the reasons lesson 2.5 gave: three copies means three places to fix a bug, three places to update, three chances to let them drift apart.

Key insight

The repetition here is across types, not values. A function parameter lets one function body work for many values of a fixed type (largest_i32 handles every i32). A generic lets one function body work for many types. Generics are to types what parameters are to values: a way to write the logic once and supply the specifics at the call. Once you see that parallel, the syntax is just notation for it.

A type parameter

The fix is to give the function a type parameter: a stand-in name for a type that the caller fills in, the way an ordinary parameter is a stand-in for a value. By convention the name is a single uppercase letter, usually T (for "type"):

fn largest<T>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

Read <T> after the function name as "this function is generic over some type T." Then a: T and b: T mean both arguments are that same type, and -> T means it returns that type. T isn't a real type; it's a placeholder the compiler replaces with the actual type at each call: i32 when you call largest(3, 7), char when you call largest('a', 'z'). One definition, every type.

There's a catch, and it's the most important thing in this lesson: that function does not yet compile. We'll meet the exact error in the next lesson, but the shape of the problem is already visible. Inside the body we wrote a > b, comparing two values of type T, and the compiler's reasonable objection is: how do you know an arbitrary T can be compared with >? You can compare i32s, but what if someone calls largest with a type that has no notion of "greater than"? The generic promises to work for any T, but the body assumes T supports >, and those two claims are in tension.

The cliffhanger

That tension is the throughline of the next two chapters. Generics (this chapter) let you write code over an unknown type T. But the moment the body does anything with a T (compares it, prints it, adds it), you need a way to tell the compiler "T isn't truly any type; it's any type that can be compared" (or printed, or added). That promise is called a trait bound, and traits are chapter 16. So this chapter teaches the generics machinery on examples that only store and move values of type T (no comparing, no printing), where no bound is needed, and the largest function, which needs to compare, is deliberately left as the unfinished business that chapter 16 resolves in its first payoff lesson.

Author's note

It would be tidier to teach generics and traits as one chapter, and some courses do. We split them on purpose: generics are a syntax you can learn cleanly on "containers that just hold a T", and traits are a big enough idea to deserve their own chapter. The price is that largest hangs unfinished across the boundary. Treat that as a feature. The itch of "but I can't compare them yet" is exactly what makes the trait-bounds lesson land when it arrives.

Quiz time

Question #1

What kind of repetition do generics remove that ordinary function parameters can't?

Show solution

Repetition across types. A normal parameter lets one function handle many values of a single fixed type; a generic lets one function definition handle many types. When the only thing that varies between copies of a function is the type it operates on, generics let you write it once.

Question #2

In fn first<T>(items: T) -> T, what is T, and when does it become a concrete type?

Show solution

T is a type parameter: a placeholder for a type that the caller supplies, named by convention with a single uppercase letter. It becomes concrete at each call site, where the compiler substitutes the actual type used (e.g. i32 or String), as the next lessons show.

Question #3

Why doesn't fn largest<T>(a: T, b: T) -> T { if a > b { a } else { b } } compile, in plain terms?

Show solution

The body uses a > b, which assumes T can be compared with >, but the signature claims largest works for any T, and not every type supports >. The compiler won't let the body assume a capability the type parameter doesn't promise. Fixing it requires a trait bound (chapter 16) that says "T is any type that can be ordered", which is the cliffhanger this chapter ends on.

Next lesson writes generic functions that the compiler does accept, the ones that only hold and pass values of type T, and meets the exact error that largest produces, confirming the diagnosis above.