Lesson 8.3 · 12 min
Code for any type: generics
Chatterbox's /last works out "the last few items of a list", but only for its list of messages. The day you want the last few words, or numbers, you would write the same lines again with a different type in them. Rust lets you write them once, with a blank where the type goes.
Adds to ChatterboxOne last_n() works for any kind of list.
Compiler errors you will meet E0369
The same function, twice
Since lesson 7.1, the arithmetic behind /last has lived inside one arm of main's big match, written for the history: a list of Message. "The last n items" is a useful idea far beyond messages, though. Here it is pulled out into functions, for a list of words and for a list of numbers.
$ cargo run
["is", "fun"]
[4, 1, 5]One new method first: saturating_sub(n) subtracts, but stops at zero instead of going below it. Asking for the last 5 of 3 items gives a start of 0, which means "everything". It is the if n >= history.len() { 0 } else { ... } from lesson 7.1 in one word.
Now look at the two bodies. They are identical, character for character. Only the type in the first line differs. And the bodies never look at an item: they count items and cut the slice at a position. Whatever the items are, the work is the same. Writing it twice is not just tedious; the day you fix a mistake in one copy, the other keeps it.
A blank where the type goes
So write it once, and leave the type blank:
$ cargo run
["is", "fun"]
[4, 1, 5]<T> after the name declares a type parameter: a placeholder name for "some type, which whoever calls the function chooses". Read the first line out loud as: for any type T, last_n takes a slice of T and a count, and gives back a slice of T. T is only a name, by custom a single capital letter. A function with a type parameter is called generic.
You never say what T is. At each call, the compiler looks at what you passed: &words is a slice of String, so here T is String and the answer is a slice of String too. It works this out for you, the same way it works out the type of a let.
What does this print?
['a', 'b', 'c'][]['c']
Reveal the answer
The program prints: ['a', 'b', 'c'] Here T is char ('a' in single quotes is one character, where "a" in double quotes would be text), a type last_n never mentions. Asking for 5 of 3 letters gives a start of 0, because saturating_sub stops at zero, so the whole list comes back.
You have been using generics all along
Look back at the types you have written since chapter 6. Vec<Message>, Option<String>, Result<usize, CommandError>, HashMap<String, String>. The angle brackets were type parameters every time. The standard library wrote Vec<T> once, "a growable list of T", and you filled in the blank: Message. Option<T> is "a T, or nothing". Result<T, E> has two blanks, one for success and one for the reason, and HashMap<K, V> has two, the key and the value.
Even "3".parse::<u32>() from lesson 7.1 was this: parse is generic, and ::<u32> fills in its blank by hand because nothing else in the line says which number type you want. You can do the same with your own: last_n::<u32>(&[], 3).
What T is allowed to do
There is a catch, and it is what keeps generics honest. Inside last_n, the compiler does not know what T will be. So the body may only do with a T what works for every possible type. Holding one, moving it, lending it: fine. Adding two together? Not every type can be added. Predict.
Does this compile — and if it does, does it run?
- It compiles and runs
- Compile error E0369
- Compile error E0308
Reveal the answer
It does not compile: E0369. Even though this call passes two whole numbers, the compiler checks total once, for every type T could ever be, and most types cannot be added (what is a Message plus a Message?). So a + b is refused: E0369, "binary operation cannot be applied". A generic function must be correct for all its types, not just the ones you happen to use today.
error[E0369]: cannot add `T` to `T` --> src/main.rs:2:7 | 2 | a + b | - ^ - T | | | T | help: consider restricting type parameter `T` with trait `Add` | 1 | fn total<T: std::ops::Add<Output = T>>(a: T, b: T) -> T { | +++++++++++++++++++++++++++
The same happens with comparing. Here is a function that should return the bigger of two values, for numbers and for words alike, and what the compiler makes of it.
This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.
error[E0369]: binary operation `>` cannot be applied to type `T` --> src/main.rs:2:10 | 2 | if a > b { a } else { b } | - ^ - T | | | T | help: consider restricting type parameter `T` with trait `PartialOrd` | 1 | fn bigger<T: std::cmp::PartialOrd>(a: T, b: T) -> T { | ++++++++++++++++++++++
E0369 in the compiler error atlas →
binary operation `>` cannot be applied to type `T`: a binary operation is one with a value on each side, likea > bora + b. You used>on two values of typeT, and not every type can be compared.- The two
Tlabels underaandbpoint at the values: both are of the unknown type, so the compiler cannot promise>exists for them. help: consider restricting type parameter `T`: "restricting" means narrowing "any type T" down to "any type T that can be compared". The help names the exact ability to ask for,PartialOrd, and where to write it.- Notice that the error is inside
bigger, not at the calls inmain: the function itself made a promise it cannot keep.
Fixes that were compiled for real
- Ask for types that can be compared: T: PartialOrd — compiles. <code>T: PartialOrd</code> reads "any type T that can be compared with <code><</code> and <code>></code>". Numbers and text both can, so both calls work, and a type that cannot be compared is now refused at the call instead.
- Give up the blank: one type only — compiles. It compiles, because <code>u32</code> can certainly be compared. But it only works for <code>u32</code>: the line with the two words had to go. Back to one copy per type.
- Trap: ask for the wrong ability — still E0369. <code>Display</code> means "can be printed with <code>{}</code>". Both values can be printed, but printing is not comparing, so <code>></code> is still refused. The ability has to match what the body does.
The part after the colon, T: PartialOrd, is a bound: a condition on which types may fill the blank. PartialOrd and Display are traits, named abilities a type can have. The very next lesson is about traits, including how to give your own types an ability. For now, here is one more bound in action. This function prints any list, as long as its items can be printed:
$ cargo run
- /help
- /last 3
- 2.5
- 0.5What the compiler does with it
A program that runs does not have blanks in it. So what happens to last_n? When the compiler builds Chatterbox, it sees every call and every type T is used with, and for each one it writes a separate copy of the function with T filled in, exactly as if you had written them by hand: a last_n for Message, one for u32, and so on. You wrote the two functions at the top of this lesson once; the compiler wrote them for you. The name for this is monomorphisation, from Greek for "making into one shape".
Why a copy per type, and not one shared function? Look at &items[start..]. To find where item number start begins, the machine multiplies start by the size of one item. The slice itself is the same size for every T, as the inspector shows, but the items it points at are not:
sizes measured with size_of on this compiler
| Type | Bytes | Layout |
|---|---|---|
&[u8] | 16 | address (8 B) · length (8 B) → heap: u8, u8, u8, … |
&[u32] | 16 | address (8 B) · length (8 B) → heap: u32, u32, u32, … |
&[String] | 16 | address (8 B) · length (8 B) → heap: String, String, … |
Each copy has the right item size built in, so each is exactly as fast as one you would have written by hand. Generics cost nothing while the program runs. What they do cost is paid earlier: a little more work for the compiler, and a slightly bigger program, one copy per type you actually use.
Chatterbox v0.8: one last_n for any list
Chatterbox gets last_n exactly as you have seen it, in main.rs under reply, with a /// comment explaining the <T>. The /last arm shrinks from nine lines to a loop over last_n(&history, n): here T is Message. The if that guarded against subtracting below zero is gone, because saturating_sub does that job.
The new test, last_n_takes_from_the_end, calls it with numbers and with words, never with a message at all. That is the point of a generic function: it can be tested on the simplest types there are and trusted on the rest. In the run, /last 1 shows the bot's last answer, and /last 0 shows nothing, since zero items is a perfectly good answer.
Sam one two /last 1 /last 0 /quit
Chatterbox v0.8 What should I call you? Hi Sam! I'm Chatterbox. Ask me anything. bot: you said: one bot: you said: two bot: you said: two bot: bye, Sam! messages so far: 2 saved 4 messages to chatterbox.log
Show the whole program (open it after you have tried the exercise below: it contains the answer)
src/command.rsunchanged
src/message.rsunchanged
src/store.rsunchanged
Download this version (zip)Unzip, then cargo run inside the folder.
This is Chatterbox v0.8 with the body of last_n replaced by todo!(). Write it, for any type T: give back the last n items of the slice, or all of them when there are fewer than n, and nothing when n is 0. Asking for more than there is must never crash.
This is Chatterbox v0.8 with the body of last_n replaced by todo!(). Write it, for any type T: give back the last n items of the slice, or all of them when there are fewer than n, and nothing when n is 0. Asking for more than there is must never crash.
Hidden tests (5): check_an_empty_list check_it_works_on_messages check_more_than_there_is_gives_everything check_the_last_two_numbers check_zero_gives_nothing
This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.
Say it out loud, then reveal
What does <code><T></code> after a function name mean?Reveal
Why is <code>a + b</code> refused inside <code>fn total<T></code>?Reveal
Name two generic types you used before this lesson.Reveal
Vec<T>, Option<T>, Result<T, E> or HashMap<K, V>.Does a generic function run slower than one written for a single type?Reveal
That is lesson 8.3. Tick it off so the course remembers where you are.
Go deeper
Questions people ask
What are generics in Rust?
Generics let you write a function or type once with a placeholder type, usually called T, and use it with many real types. Vec, Option and Result are all generic.
What does error E0369 mean in Rust?
A binary operation such as + or > was used on a type that might not support it, often a generic T. Add a bound like T: PartialOrd for comparing, or write the function for a specific type.
What is a trait bound in Rust?
A bound, written T: SomeTrait, limits which types a generic parameter may be. Inside the function you may then use that ability, such as comparing with PartialOrd or printing with Display.
Do Rust generics have a runtime cost?
No. Rust uses monomorphisation: the compiler makes a separate copy of the generic code for each type it is used with. The cost is a little more compile time and a somewhat larger program.
What is saturating_sub in Rust?
A subtraction on whole numbers that stops at the smallest possible value instead of going below it. For usize, 3.saturating_sub(5) is 0 rather than a crash.