Generic Inductive Definitions

Essential
Last updated: Tags: Rust, ADT, Generics

Prerequisites

Instead of writing a separate IntList, StringList, and FloatList, you write one List<T> that works for any element type. A type parameter is a placeholder that is filled in when the type is used.

The syntax

A type parameter is a name in angle brackets after the type name:

enum Option<T> {
    None,
    Some(T),
}

T is the placeholder. Option<i32>, Option<String>, and Option<bool> are three different concrete types produced from one definition.

A struct with two parameters:

struct Pair<A, B> {
    first: A,
    second: B,
}

let p: Pair<i32, &str> = Pair { first: 42, second: "hello" };

A generic recursive list

The canonical example that shows why generics and inductive types fit together is a singly-linked list:

enum List<T> {
    Nil,
    Cons(T, Box<List<T>>),
}

Nil is the empty list. Cons(x, rest) prepends x to rest. The recursive field Box<List<T>> requires Box for the same reason as any recursive inductive type: to give the compiler a finite, known size.

Building a list of integers and a list of strings reuses the same definition:

let ints: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
let strs: List<&str> = List::Cons("a", Box::new(List::Nil));

One definition, many concrete types

When the compiler sees List<i32> and List<&str>, it generates separate machine code for each — a process called monomorphization. The generic definition is a template; each distinct instantiation produces a concrete, specialized type.

This means generic types in Rust have no runtime cost from the type parameter: the generated code for List<i32> is exactly what you would write if List had been defined specifically for i32.

impl blocks for generic types

To add methods to a generic type, the type parameter appears twice: once after impl (introducing it) and once after the type name (using it):

impl<T> List<T> {
    fn is_empty(&self) -> bool {
        matches!(self, List::Nil)
    }
}

The <T> after impl says “for any type T”; the <T> after List says “this impl applies to List<T>”. Both are required.

The connection to polymorphism

A generic type definition is the type-level analog of a generic function. Just as fn identity<T>(x: T) -> T works for any T, List<T> works for any element type. Both express “parametrised over a type” — functions at the term level, type definitions at the type level. The same monomorphization mechanism applies to both.