Chapter 10 — Generics and Interfaces

A generic is code written once that works for many types. You’ve been using them already — Option<T>, Result<T, E>, and [T] are all generic. Now let’s write our own.

Generic structs and enums

Put one or more type parameters in angle brackets after the name:

struct Box<T> {
    value: T
    fn get(self) -> T { return self.value }
    fn replaced(self, n: T) -> Box<T> { return Box<T> { value: n } }
}

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

fn main() -> int {
    let b = Box<int> { value: 3 }
    let p = Pair<int, int> { first: 3, second: 4 }
    return b.replaced(7).get() + p.first + p.second   // 7 + 3 + 4 = 14
}
=> 14

Each instantiationBox<int>, Box<string>, Pair<int, bool> — is its own distinct type. You write the type arguments explicitly when you construct (Box<int> { value: 3 }), and they nest happily (Box<Box<int>>). Enums are generic the same way, which is the whole secret behind Option<T> and Result<T, E> being library types rather than magic.

Generic functions, and inference

Functions and methods can be generic too, and here you usually don’t write the type arguments — Ingle infers them from what you pass and what you expect back:

fn identity<T>(move x: T) -> T {
    return x
}

fn main() -> int {
    println(identity("hi"))   // T inferred as string
    return identity(7)        // T inferred as int
}
hi
=> 7

No “turbofish,” no identity<int>(7) ceremony at the call. The type just flows in.

The move you may not have expected. Did you notice move x up there? A plain parameter only borrows, and you can’t return a borrowed value — it would escape the function. For a concrete type you’d hit the same rule; for a generic T, which Ingle treats as an owned value by default, returning the argument means taking it move. This is real and the compiler enforces it. The whole ownership story is the next chapter; for now, just know that “a generic function that hands its argument back out” is written move.

Bounds: asking a type parameter to do something

Inside a generic body, T is opaque — you can pass it around, store it, return it, but you can’t (say) call methods on it, because you’ve no idea what type it’ll be. A bound fixes that by requiring T to implement an interface:

interface Ord {
    fn compare(self, other: Self) -> int
}

struct Version implements Ord {
    number: int
    fn compare(self, other: Version) -> int { return self.number - other.number }
}

fn max<T: Ord>(move a: T, move b: T) -> T {   // T must implement Ord
    if a.compare(b) >= 0 { return a }         // ...so now we may call compare on it
    return b
}

<T: Ord> says “T, but only types that implement Ord.” Now a.compare(b) is legal, because every possible T is guaranteed to have it. And the guarantee is enforced from the other side too — try to use a type that doesn’t qualify and you’re stopped:

fn main() -> int { return max(1, 2) }   // error: type argument does not satisfy the generic bound

int doesn’t implement Ord (or any interface — yet), so max(1, 2) is rejected at compile time, by name. No surprises at runtime.

The Copy bound

There’s one special bound, Copy, for the opposite need: “this T can be freely duplicated, so I don’t need move.”

fn id<T: Copy>(x: T) -> T {   // no 'move' needed — Copy means it can be duplicated
    return x
}

One detail here is worth knowing: Copy means every type except a struct or an array. Numbers copy bit-for-bit; strings, enums, and closures are immutable and shared, so “copying” one is just a cheap bump of a reference count. Only the unique-owner aggregates — structs and arrays — are not Copy. Hand one to a Copy-bounded function and you get a crisp refusal:

struct User { name: string  age: int }
let v = id(User { name: "ada", age: 36 })
error: type argument is not Copy — only scalars, strings, enums, and closures satisfy a 'Copy' bound (not a struct or array)

A type parameter can carry several bounds at once, joined with +: <K: Hash + Eq> asks for a type that can both hash itself and compare itself for equality, and Copy composes right alongside them (T: Ord + Copy). Bounds aren’t limited to free functions, either — a generic struct can be bounded too, and the standard library leans on exactly that: the hash map is declared struct Map<K: Hash + Eq, V>, which is what lets it hash and compare keys of any qualifying type. (We’ll meet it properly in Chapter 15.) What’s not here yet: bounds on a generic struct’s individual methods and on generic enums, and inference stays call-site only (no turbofish, by design). The honest list is in Chapter 23.

Static versus dynamic, in one breath. A bound (<T: Ord>) and an interface-as-a-value (Chapter 7) are the two halves of polymorphism, and Ingle keeps them distinct. A bound is resolved when the concrete type is known — the compiler hands the call a hidden table of the right methods (a “witness”), so there’s no runtime lookup and no boxing. An interface value is for when the type isn’t known until runtime — there the lookup happens through the value’s own method table. Same interfaces, two dispatch strategies: reach for a bound when you can, a value type when you must.

How generics are compiled, today. Because every value has a uniform representation, Ingle compiles a generic onceBox<int> and Box<string> share one compiled body. This is the “erased” strategy, and it’s why adding generics doesn’t blow up your compile times. (The plan is for release builds to also offer monomorphization — a specialised copy per type for maximum speed — but that’s a future build mode, not something you write differently. You’ll never change your code for it.)

Fireside trivia. The angle brackets <T> for generics are a C++ inheritance, and they’ve caused parser writers grief ever since, because < is also “less than.” Is a < b > c a comparison or a generic? Ingle resolves it with a lookahead rule: it reads a generic only when a balanced <...>, every token inside it type-legal, is immediately followed by {. That rule is total rather than a guess — no expression begins with {, so a > { can never continue a comparison, and a non-type token inside the brackets proves the < was less-than. Every language in this family has a scar in exactly this spot.