Part 13: Generics
Generic in Part 4 gave a function a type parameter, so one body could serve int, float64 and char. This part takes the idea all the way. Types get type parameters of their own — a Pair<T>, a Reading<T> — and so do their methods. Bounds turn "any T" into "any T that can do this", which is what lets a generic call methods, compare scores or print its values. And the language's own forms, T?, T ! E and A | B, turn out to be generic too, which is how a helper can be written once for every optional or every fallible there will ever be.
What you will learn
- Declaring structs and variants with type parameters, and naming the type arguments in a literal.
- Giving a generic type methods with
extend Labeled<T>, including methods with type parameters of their own. - Bounding a type parameter by an interface,
<T: Scored>, and where the compiler checks the bound. - Requiring several interfaces at once with
<T: Scored + Display>. - Writing reusable helpers over
T ! EandT?, which cannot be extended. - Building sums from type parameters,
T | U, and why they collapse whenTandUare the same.
What a bound decides
Everything in this part comes back to one question: what may the body do with a T? The bound is the answer, and the compiler checks it where the generic is used.
flowchart LR
t(["A type parameter T"]) --> none["No bound: <T><br/>store, pass, return a T<br/>(13.1–13.2)"]
t --> one["One bound: <T: Scored><br/>+ call Scored's methods<br/>(13.3)"]
t --> two["Several: <T: Scored + Display><br/>+ everything each one provides<br/>(13.4)"]
one --> call{"At each call:<br/>does the type argument<br/>implement every bound?"}
two --> call
call -- "yes" --> ok["compiled for that type,<br/>calling its methods directly"]
call -- "no" --> err["error at the call,<br/>naming the missing method"]| You want to… | Write | Lesson |
|---|---|---|
| describe "two of something" | struct Pair<T> { … } | Generic type |
give every Pair<T> a method | extend Pair<T> { … } | Generic method |
call a method on a T | <T: Interface> | Generic bound |
| call methods from two interfaces | <T: A + B> | Multiple bounds |
| write one helper for every optional or fallible | func F<T, E>(outcome: T ! E) | Generic outcome |
| return one of two types | -> T | U, matched with else | Generic sum |
Lessons
| Lesson | What you will learn | |
|---|---|---|
| 13.1 | Generic type | a struct with a type parameter: Pair<T> |
| 13.2 | Generic method | methods on a generic type, and methods with type parameters of their own |
| 13.3 | Generic bound | require a type parameter to implement an interface |
| 13.4 | Multiple bounds | require several interfaces at once with A + B |
| 13.5 | Generic outcome | write helpers that work for any optional or result |
| 13.6 | Generic sum | generic sums, and what happens when both members are the same type |
Before you start
Finish Part 12: Interfaces: a bound is an interface, and Display is the one most generics end up needing. The lessons also build on Generic and Callback from Part 4, Struct and Variant match from Part 6, the outcome tools of Part 8 and Part 9, and Sum type from Part 10. Each lesson's package is in the Examples repository's Generics/ folder:
cd Examples/Generics/GenericType
rux run
After this part
Part 14: Text puts generics to work straight away: String, StringView and StringBuilder are ordinary types from the Text package, and every value you print goes through the Display interface you bounded on here. Later, Part 17: Collections is built from generic types — a vector, a map and a set of any element type.
For the full rules behind this part, see Generic functions, Interfaces and Methods in the Rux Reference.