Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

Learn RustChapter 8: Organising code

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.

Two functions with the same body
fn last_words(items: &[String], n: usize) -> &[String] {
let start = items.len().saturating_sub(n);
&items[start..]
}
 
fn last_counts(items: &[u32], n: usize) -> &[u32] {
let start = items.len().saturating_sub(n);
&items[start..]
}
 
fn main() {
let words = vec![String::from("rust"), String::from("is"), String::from("fun")];
let counts = vec![3, 1, 4, 1, 5];
println!("{:?}", last_words(&words, 2));
println!("{:?}", last_counts(&counts, 3));
}
$ 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:

One generic function, used with two types
fn last_n<T>(items: &[T], n: usize) -> &[T] {
let start = items.len().saturating_sub(n);
&items[start..]
}
 
fn main() {
let words = vec![String::from("rust"), String::from("is"), String::from("fun")];
let counts = vec![3, 1, 4, 1, 5];
println!("{:?}", last_n(&words, 2));
println!("{:?}", last_n(&counts, 3));
}
$ 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.

PredictA third type, for free
fn last_n<T>(items: &[T], n: usize) -> &[T] {
let start = items.len().saturating_sub(n);
&items[start..]
}
 
fn main() {
let letters = vec!['a', 'b', 'c'];
println!("{:?}", last_n(&letters, 5));
}

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.

PredictAdd two of anything
fn total<T>(a: T, b: T) -> T {
a + b
}
 
fn main() {
println!("{}", total(2, 3));
}

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.

Compiler CoachThe compiler, translated

This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.

fn bigger<T>(a: T, b: T) -> T {
if a > b { a } else { b }
}
 
fn main() {
println!("{}", bigger(3, 7));
println!("{}", bigger("pear", "apple"));
}
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 →

  1. binary operation `>` cannot be applied to type `T`: a binary operation is one with a value on each side, like a > b or a + b. You used > on two values of type T, and not every type can be compared.
  2. The two T labels under a and b point at the values: both are of the unknown type, so the compiler cannot promise > exists for them.
  3. 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.
  4. Notice that the error is inside bigger, not at the calls in main: 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>&lt;</code> and <code>&gt;</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>&gt;</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:

A bound in the other common place: "anything printable"
use std::fmt::Display;
 
fn show_all<T: Display>(items: &[T]) {
for item in items {
println!("- {item}");
}
}
 
fn main() {
show_all(&["/help", "/last 3"]);
show_all(&[2.5, 0.5]);
}
$ cargo run
- /help
- /last 3
- 2.5
- 0.5

What 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:

Type InspectorA slice of anything is two numbers; the items are not

sizes measured with size_of on this compiler

TypeBytesLayout
&[u8]16address (8 B) · length (8 B) → heap: u8, u8, u8, …
&[u32]16address (8 B) · length (8 B) → heap: u32, u32, u32, …
&[String]16address (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.

Chatterbox v0.8 after lesson 8.3: the whole program (green lines are new in this lesson)
You type
Sam
one
two
/last 1
/last 0
/quit
Chatterbox prints (a real run)
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/main.rs13 new lines
// Chatterbox: a small chat program, built one lesson at a time.
mod command;
mod message;
mod store;
 
use command::{Command, parse};
use message::Message;
use std::collections::HashMap;
use std::io;
 
/// What the bot is called. A constant: fixed for the whole program.
const BOT: &str = "Chatterbox";
/// The version as a pair of numbers: (major, minor).
const VERSION: (u32, u32) = (0, 8);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
/// What /help prints.
const HELP: &str = "commands: /help, /again, /history, /count, /stats, /remember <name> <fact>, /recall <name>, /find <word>, /last <n>, /say <text>, /shout <text>, /debug <text>, /quit (or /h, /q, /exit)";
 
/// The first line Chatterbox prints.
fn banner() -> String {
format!("{} v{}.{}", BOT, VERSION.0, VERSION.1)
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str) -> String {
let mut text = format!("Hi {name}! I'm {BOT}.");
text.push_str(" Ask me anything.");
text
}
 
/// What the bot says back to an ordinary message (not a command).
fn reply(message: &str) -> String {
if message.len() > MAX_LEN {
format!("that is too long (the limit is {MAX_LEN} bytes)")
} else if message.ends_with('?') {
String::from("Good question! I can only echo for now. Ask me again in chapter 9.")
} else {
format!("you said: {message}")
}
}
 
/// The last `n` items of any slice: messages, numbers, words, anything.
/// `<T>` says "for any type T"; the body never needs to know which one.
fn last_n<T>(items: &[T], n: usize) -> &[T] {
let start = items.len().saturating_sub(n);
&items[start..]
}
 
fn main() -> Result<(), io::Error> {
let mut messages_seen: u32 = 0;
let quiet = false;
let mut last: Option<String> = None; // the last thing you said, for /again
let mut history: Vec<Message> = store::load(store::FILE)?; // last time's conversation, if any
let mut facts: HashMap<String, String> = HashMap::new(); // name -> fact, for /recall
 
println!("{}", banner());
println!("What should I call you?");
let mut name_line = String::new();
io::stdin().read_line(&mut name_line)?;
let name = name_line.trim();
if !quiet {
println!("{}", greeting(name));
}
if !history.is_empty() {
println!(
"(I remember {} messages from last time. /history shows them.)",
history.len()
);
}
 
let mut line = String::new();
loop {
line.clear();
let bytes = io::stdin().read_line(&mut line)?;
if bytes == 0 {
break; // no more input: the keyboard was closed
}
let message = line.trim();
let cmd = match parse(message) {
Ok(cmd) => cmd,
Err(e) => {
println!("bot: {e}");
continue;
}
};
match cmd {
Command::Empty => {} // nothing typed: nothing to answer
Command::Quit => {
println!("bot: bye, {name}!");
break;
}
Command::Help => println!("bot: {HELP}"),
Command::Again => {
if let Some(text) = &last {
println!("bot: {}", reply(text));
} else {
println!("bot: nothing to repeat yet");
}
}
Command::History => {
if history.is_empty() {
println!("bot: nothing said yet");
} else {
for (i, m) in history.iter().enumerate() {
println!("{}. {}", i + 1, m.to_line());
}
}
}
Command::Count => {
let bytes: usize = history.iter().map(|m| m.text.len()).sum();
println!("bot: {} messages, {} bytes of text", history.len(), bytes);
}
Command::Stats => {
// One chain of steps: your messages -> their words -> a count per word.
let mut counts: HashMap<String, u32> = HashMap::new();
for word in history
.iter()
.filter(|m| m.author == "you")
.flat_map(|m| m.text.split_whitespace())
{
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
let mut top: Vec<(String, u32)> = counts.into_iter().collect();
// most used first; ties in alphabetical order, so the answer never changes
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let words: Vec<String> = top
.iter()
.take(3)
.map(|(word, n)| format!("{word} ({n})"))
.collect();
if words.is_empty() {
println!("bot: nothing said yet");
} else {
println!("bot: your top words: {}", words.join(", "));
}
}
Command::Remember(key, fact) => {
println!("bot: got it: {key} is {fact}");
facts.insert(key, fact);
}
Command::Recall(Some(key)) => match facts.get(&key) {
Some(fact) => println!("bot: {key} is {fact}"),
None => println!("bot: I don't know anything about {key}"),
},
Command::Recall(None) => {
if facts.is_empty() {
println!("bot: I don't remember anything yet. Try /remember");
} else {
// A HashMap has no order, so sort the names before printing them.
let mut names = Vec::new();
for name in facts.keys() {
names.push(name.as_str());
}
names.sort();
println!("bot: I remember: {}", names.join(", "));
}
}
Command::Find(word) => {
// The closure borrows `word` from here: a small function made on the spot.
let hits: Vec<&Message> = history
.iter()
.filter(|m| m.author == "you" && m.text.contains(&word))
.collect();
if hits.is_empty() {
println!("bot: you never said {word}");
} else {
for m in hits {
println!("{}", m.to_line());
}
}
}
Command::Last(n) => {
for m in last_n(&history, n) {
println!("{}", m.to_line());
}
}
Command::Say(text) => println!("bot: {text}"),
Command::Shout(text) => println!("bot: {}", Message::new("you", &text).shout()),
Command::Debug(text) => println!("bot: {:?}", parse(&text)),
Command::Chat(text) => {
messages_seen += 1;
let said = Message::new("you", &text);
let answer = Message::new("bot", &reply(&said.text));
println!("bot: {}", answer.text);
history.push(said);
history.push(answer);
// Message::new made the history its own copy, so `text` itself can
// be handed to `last` for /again: no extra copy needed.
last = Some(text);
}
}
}
println!("messages so far: {messages_seen}");
store::save(store::FILE, &history)?;
let s = if history.len() == 1 { "" } else { "s" };
println!("saved {} message{s} to {}", history.len(), store::FILE);
Ok(())
}
 
// Tests: `cargo test` builds and runs them; the program itself never sees them.
#[cfg(test)]
mod tests {
use super::*;
 
#[test]
fn reply_echoes_and_answers_questions() {
assert_eq!(reply("hi"), "you said: hi");
assert!(reply("are you there?").starts_with("Good question!"));
}
 
#[test]
fn reply_refuses_long_messages() {
let long = "x".repeat(MAX_LEN + 1);
assert!(reply(&long).starts_with("that is too long"));
}
 
#[test]
fn last_n_takes_from_the_end() {
assert_eq!(last_n(&[1, 2, 3, 4], 2), &[3, 4]);
assert_eq!(last_n(&["a", "b"], 5), &["a", "b"]);
assert!(last_n(&[1, 2, 3], 0).is_empty());
}
}
src/command.rsunchanged
//! Commands: what a line of input means, and what can be wrong with it.
use std::fmt;
 
/// Everything a line of input can mean. One enum, so a `match` on it must
/// handle every kind of line: the compiler checks that nothing is forgotten.
#[derive(Debug)]
pub enum Command {
Empty,
Help,
Quit,
Again,
History,
Count,
Stats,
Remember(String, String),
Recall(Option<String>),
Find(String),
Last(usize),
Say(String),
Shout(String),
Debug(String),
Chat(String),
}
 
/// Everything that can be wrong with a line of input. parse() returns one of
/// these instead of a Command, and main prints it.
#[derive(Debug)]
pub enum CommandError {
Unknown(String),
Missing(String, String),
Usage(String),
NotACount(String),
}
 
/// How a CommandError reads when printed with {}.
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CommandError::Unknown(word) => {
write!(f, "I don't know the command {word}. Try /help.")
}
CommandError::Missing(verb, example) => {
write!(f, "{verb} what? Try /{verb} {example}")
}
CommandError::Usage(form) => write!(f, "try {form}"),
CommandError::NotACount(text) => write!(f, "'{text}' is not a count. Try /last 3"),
}
}
}
 
/// The text after a command word, or the error that says it is missing.
fn need(line: &str, verb: &str, example: &str) -> Result<String, CommandError> {
match arg(line) {
Some(text) => Ok(String::from(text)),
None => Err(CommandError::Missing(
String::from(verb),
String::from(example),
)),
}
}
 
/// How many messages /last should show. "two" or "-1" is an error.
fn parse_count(text: &str) -> Result<usize, CommandError> {
match text.parse::<usize>() {
Ok(n) => Ok(n),
Err(_) => Err(CommandError::NotACount(String::from(text))),
}
}
 
/// Turn one line of input into a Command, or say what is wrong with it.
/// Each `?` means: if this step failed, return its error right here.
pub fn parse(line: &str) -> Result<Command, CommandError> {
let cmd = match first_word(line) {
"" => Command::Empty,
"/help" | "/h" => Command::Help,
"/quit" | "/q" | "/exit" => Command::Quit,
"/again" => Command::Again,
"/history" => Command::History,
"/count" => Command::Count,
"/stats" => Command::Stats,
"/remember" => {
let usage = CommandError::Usage(String::from("/remember <name> <fact>"));
let rest = match arg(line) {
Some(rest) => rest,
None => return Err(usage),
};
match arg(rest) {
Some(fact) => Command::Remember(String::from(first_word(rest)), String::from(fact)),
None => return Err(usage),
}
}
"/recall" => match arg(line) {
Some(name) => Command::Recall(Some(String::from(name))),
None => Command::Recall(None),
},
"/find" => Command::Find(need(line, "find", "hello")?),
"/last" => Command::Last(parse_count(&need(line, "last", "3")?)?),
"/debug" => Command::Debug(need(line, "debug", "/say hi")?),
"/say" => Command::Say(need(line, "say", "hello")?),
"/shout" => Command::Shout(need(line, "shout", "hello")?),
word => {
if is_command(word) {
return Err(CommandError::Unknown(String::from(word)));
}
Command::Chat(String::from(line))
}
};
Ok(cmd)
}
 
/// True when the text is a command: it starts with a slash.
fn is_command(text: &str) -> bool {
text.starts_with('/')
}
 
/// The first word of the text: a slice (&str) pointing into it, not a copy.
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(i) => &text[..i],
None => text,
}
}
 
/// The text after the command word, if there is any: Some("hello") for
/// "/say hello", None for a bare "/say". No more "" standing in for nothing.
fn arg(line: &str) -> Option<&str> {
match line.find(' ') {
Some(i) => {
let rest = line[i + 1..].trim();
if rest.is_empty() { None } else { Some(rest) }
}
None => None,
}
}
 
#[cfg(test)]
mod tests {
use super::*;
 
#[test]
fn plain_text_is_chat() {
assert!(matches!(parse("hello there"), Ok(Command::Chat(t)) if t == "hello there"));
}
 
#[test]
fn help_has_a_short_form() {
assert!(matches!(parse("/help"), Ok(Command::Help)));
assert!(matches!(parse("/h"), Ok(Command::Help)));
}
 
#[test]
fn say_needs_text() {
assert_eq!(
parse("/say").unwrap_err().to_string(),
"say what? Try /say hello"
);
}
 
#[test]
fn last_needs_a_number() {
assert!(matches!(parse("/last 3"), Ok(Command::Last(3))));
assert_eq!(
parse("/last two").unwrap_err().to_string(),
"'two' is not a count. Try /last 3"
);
}
 
#[test]
fn unknown_commands_are_errors() {
assert_eq!(
parse("/dance").unwrap_err().to_string(),
"I don't know the command /dance. Try /help."
);
}
 
#[test]
fn remember_takes_a_name_and_a_fact() {
assert!(
matches!(parse("/remember pizza extra cheese"), Ok(Command::Remember(k, v)) if k == "pizza" && v == "extra cheese")
);
assert!(parse("/remember pizza").is_err());
}
}
src/message.rsunchanged
//! A Message: one line of the conversation.
 
/// One line of the conversation: who said it, and what they said.
pub struct Message {
pub author: String,
pub text: String,
}
 
impl Message {
/// Make a Message from two pieces of borrowed text.
pub fn new(author: &str, text: &str) -> Message {
Message {
author: String::from(author),
text: String::from(text),
}
}
 
/// The message as one transcript line: "you: hello".
pub fn to_line(&self) -> String {
format!("{}: {}", self.author, self.text)
}
 
/// The text in capitals, for /shout.
pub fn shout(&self) -> String {
self.text.to_uppercase()
}
}
src/store.rsunchanged
//! Saving the conversation to a file and reading it back.
use crate::message::Message;
use std::fs;
use std::io;
 
/// Where the history is kept, next to wherever you run Chatterbox.
pub const FILE: &str = "chatterbox.log";
 
/// The history as text, one "author: text" line per message.
fn to_lines(history: &[Message]) -> String {
let mut out = String::new();
for m in history {
out.push_str(&m.to_line());
out.push('\n');
}
out
}
 
/// Text in that format, back into messages. Lines that do not fit are skipped.
fn from_lines(text: &str) -> Vec<Message> {
let mut out = Vec::new();
for line in text.lines() {
if let Some((author, said)) = line.split_once(": ") {
out.push(Message::new(author, said));
}
}
out
}
 
/// The saved history, or an empty one the very first time (no file yet).
pub fn load(path: &str) -> Result<Vec<Message>, io::Error> {
match fs::read_to_string(path) {
Ok(text) => Ok(from_lines(&text)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
 
/// Write the whole history to the file, replacing what was there.
pub fn save(path: &str, history: &[Message]) -> Result<(), io::Error> {
fs::write(path, to_lines(history))
}

Download this version (zip)Unzip, then cargo run inside the folder.

Try itYour turn: last_n in Chatterbox

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.

// Chatterbox: a small chat program, built one lesson at a time.
mod command {
//! Commands: what a line of input means, and what can be wrong with it.
use std::fmt;
 
/// Everything a line of input can mean. One enum, so a `match` on it must
/// handle every kind of line: the compiler checks that nothing is forgotten.
#[derive(Debug)]
pub enum Command {
Empty,
Help,
Quit,
Again,
History,
Count,
Stats,
Remember(String, String),
Recall(Option<String>),
Find(String),
Last(usize),
Say(String),
Shout(String),
Debug(String),
Chat(String),
}
 
/// Everything that can be wrong with a line of input. parse() returns one of
/// these instead of a Command, and main prints it.
#[derive(Debug)]
pub enum CommandError {
Unknown(String),
Missing(String, String),
Usage(String),
NotACount(String),
}
 
/// How a CommandError reads when printed with {}.
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CommandError::Unknown(word) => {
write!(f, "I don't know the command {word}. Try /help.")
}
CommandError::Missing(verb, example) => {
write!(f, "{verb} what? Try /{verb} {example}")
}
CommandError::Usage(form) => write!(f, "try {form}"),
CommandError::NotACount(text) => write!(f, "'{text}' is not a count. Try /last 3"),
}
}
}
 
/// The text after a command word, or the error that says it is missing.
fn need(line: &str, verb: &str, example: &str) -> Result<String, CommandError> {
match arg(line) {
Some(text) => Ok(String::from(text)),
None => Err(CommandError::Missing(
String::from(verb),
String::from(example),
)),
}
}
 
/// How many messages /last should show. "two" or "-1" is an error.
fn parse_count(text: &str) -> Result<usize, CommandError> {
match text.parse::<usize>() {
Ok(n) => Ok(n),
Err(_) => Err(CommandError::NotACount(String::from(text))),
}
}
 
/// Turn one line of input into a Command, or say what is wrong with it.
/// Each `?` means: if this step failed, return its error right here.
pub fn parse(line: &str) -> Result<Command, CommandError> {
let cmd = match first_word(line) {
"" => Command::Empty,
"/help" | "/h" => Command::Help,
"/quit" | "/q" | "/exit" => Command::Quit,
"/again" => Command::Again,
"/history" => Command::History,
"/count" => Command::Count,
"/stats" => Command::Stats,
"/remember" => {
let usage = CommandError::Usage(String::from("/remember <name> <fact>"));
let rest = match arg(line) {
Some(rest) => rest,
None => return Err(usage),
};
match arg(rest) {
Some(fact) => Command::Remember(String::from(first_word(rest)), String::from(fact)),
None => return Err(usage),
}
}
"/recall" => match arg(line) {
Some(name) => Command::Recall(Some(String::from(name))),
None => Command::Recall(None),
},
"/find" => Command::Find(need(line, "find", "hello")?),
"/last" => Command::Last(parse_count(&need(line, "last", "3")?)?),
"/debug" => Command::Debug(need(line, "debug", "/say hi")?),
"/say" => Command::Say(need(line, "say", "hello")?),
"/shout" => Command::Shout(need(line, "shout", "hello")?),
word => {
if is_command(word) {
return Err(CommandError::Unknown(String::from(word)));
}
Command::Chat(String::from(line))
}
};
Ok(cmd)
}
 
/// True when the text is a command: it starts with a slash.
fn is_command(text: &str) -> bool {
text.starts_with('/')
}
 
/// The first word of the text: a slice (&str) pointing into it, not a copy.
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(i) => &text[..i],
None => text,
}
}
 
/// The text after the command word, if there is any: Some("hello") for
/// "/say hello", None for a bare "/say". No more "" standing in for nothing.
fn arg(line: &str) -> Option<&str> {
match line.find(' ') {
Some(i) => {
let rest = line[i + 1..].trim();
if rest.is_empty() { None } else { Some(rest) }
}
None => None,
}
}
 
#[cfg(test)]
mod tests {
use super::*;
 
#[test]
fn plain_text_is_chat() {
assert!(matches!(parse("hello there"), Ok(Command::Chat(t)) if t == "hello there"));
}
 
#[test]
fn help_has_a_short_form() {
assert!(matches!(parse("/help"), Ok(Command::Help)));
assert!(matches!(parse("/h"), Ok(Command::Help)));
}
 
#[test]
fn say_needs_text() {
assert_eq!(
parse("/say").unwrap_err().to_string(),
"say what? Try /say hello"
);
}
 
#[test]
fn last_needs_a_number() {
assert!(matches!(parse("/last 3"), Ok(Command::Last(3))));
assert_eq!(
parse("/last two").unwrap_err().to_string(),
"'two' is not a count. Try /last 3"
);
}
 
#[test]
fn unknown_commands_are_errors() {
assert_eq!(
parse("/dance").unwrap_err().to_string(),
"I don't know the command /dance. Try /help."
);
}
 
#[test]
fn remember_takes_a_name_and_a_fact() {
assert!(
matches!(parse("/remember pizza extra cheese"), Ok(Command::Remember(k, v)) if k == "pizza" && v == "extra cheese")
);
assert!(parse("/remember pizza").is_err());
}
}
}
mod message {
//! A Message: one line of the conversation.
 
/// One line of the conversation: who said it, and what they said.
pub struct Message {
pub author: String,
pub text: String,
}
 
impl Message {
/// Make a Message from two pieces of borrowed text.
pub fn new(author: &str, text: &str) -> Message {
Message {
author: String::from(author),
text: String::from(text),
}
}
 
/// The message as one transcript line: "you: hello".
pub fn to_line(&self) -> String {
format!("{}: {}", self.author, self.text)
}
 
/// The text in capitals, for /shout.
pub fn shout(&self) -> String {
self.text.to_uppercase()
}
}
}
mod store {
//! Saving the conversation to a file and reading it back.
use crate::message::Message;
use std::fs;
use std::io;
 
/// Where the history is kept, next to wherever you run Chatterbox.
pub const FILE: &str = "chatterbox.log";
 
/// The history as text, one "author: text" line per message.
fn to_lines(history: &[Message]) -> String {
let mut out = String::new();
for m in history {
out.push_str(&m.to_line());
out.push('\n');
}
out
}
 
/// Text in that format, back into messages. Lines that do not fit are skipped.
fn from_lines(text: &str) -> Vec<Message> {
let mut out = Vec::new();
for line in text.lines() {
if let Some((author, said)) = line.split_once(": ") {
out.push(Message::new(author, said));
}
}
out
}
 
/// The saved history, or an empty one the very first time (no file yet).
pub fn load(path: &str) -> Result<Vec<Message>, io::Error> {
match fs::read_to_string(path) {
Ok(text) => Ok(from_lines(&text)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e),
}
}
 
/// Write the whole history to the file, replacing what was there.
pub fn save(path: &str, history: &[Message]) -> Result<(), io::Error> {
fs::write(path, to_lines(history))
}
}
use command::{Command, parse};
use message::Message;
use std::collections::HashMap;
use std::io;
 
/// What the bot is called. A constant: fixed for the whole program.
const BOT: &str = "Chatterbox";
/// The version as a pair of numbers: (major, minor).
const VERSION: (u32, u32) = (0, 8);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
/// What /help prints.
const HELP: &str = "commands: /help, /again, /history, /count, /stats, /remember <name> <fact>, /recall <name>, /find <word>, /last <n>, /say <text>, /shout <text>, /debug <text>, /quit (or /h, /q, /exit)";
 
/// The first line Chatterbox prints.
fn banner() -> String {
format!("{} v{}.{}", BOT, VERSION.0, VERSION.1)
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str) -> String {
let mut text = format!("Hi {name}! I'm {BOT}.");
text.push_str(" Ask me anything.");
text
}
 
/// What the bot says back to an ordinary message (not a command).
fn reply(message: &str) -> String {
if message.len() > MAX_LEN {
format!("that is too long (the limit is {MAX_LEN} bytes)")
} else if message.ends_with('?') {
String::from("Good question! I can only echo for now. Ask me again in chapter 9.")
} else {
format!("you said: {message}")
}
}
 
/// The last `n` items of any slice: messages, numbers, words, anything.
/// `<T>` says "for any type T"; the body never needs to know which one.
fn last_n<T>(items: &[T], n: usize) -> &[T] {
todo!()
}
 
fn main() -> Result<(), io::Error> {
let mut messages_seen: u32 = 0;
let quiet = false;
let mut last: Option<String> = None; // the last thing you said, for /again
let mut history: Vec<Message> = store::load(store::FILE)?; // last time's conversation, if any
let mut facts: HashMap<String, String> = HashMap::new(); // name -> fact, for /recall
 
println!("{}", banner());
println!("What should I call you?");
let mut name_line = String::new();
io::stdin().read_line(&mut name_line)?;
let name = name_line.trim();
if !quiet {
println!("{}", greeting(name));
}
if !history.is_empty() {
println!(
"(I remember {} messages from last time. /history shows them.)",
history.len()
);
}
 
let mut line = String::new();
loop {
line.clear();
let bytes = io::stdin().read_line(&mut line)?;
if bytes == 0 {
break; // no more input: the keyboard was closed
}
let message = line.trim();
let cmd = match parse(message) {
Ok(cmd) => cmd,
Err(e) => {
println!("bot: {e}");
continue;
}
};
match cmd {
Command::Empty => {} // nothing typed: nothing to answer
Command::Quit => {
println!("bot: bye, {name}!");
break;
}
Command::Help => println!("bot: {HELP}"),
Command::Again => {
if let Some(text) = &last {
println!("bot: {}", reply(text));
} else {
println!("bot: nothing to repeat yet");
}
}
Command::History => {
if history.is_empty() {
println!("bot: nothing said yet");
} else {
for (i, m) in history.iter().enumerate() {
println!("{}. {}", i + 1, m.to_line());
}
}
}
Command::Count => {
let bytes: usize = history.iter().map(|m| m.text.len()).sum();
println!("bot: {} messages, {} bytes of text", history.len(), bytes);
}
Command::Stats => {
// One chain of steps: your messages -> their words -> a count per word.
let mut counts: HashMap<String, u32> = HashMap::new();
for word in history
.iter()
.filter(|m| m.author == "you")
.flat_map(|m| m.text.split_whitespace())
{
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
let mut top: Vec<(String, u32)> = counts.into_iter().collect();
// most used first; ties in alphabetical order, so the answer never changes
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
let words: Vec<String> = top
.iter()
.take(3)
.map(|(word, n)| format!("{word} ({n})"))
.collect();
if words.is_empty() {
println!("bot: nothing said yet");
} else {
println!("bot: your top words: {}", words.join(", "));
}
}
Command::Remember(key, fact) => {
println!("bot: got it: {key} is {fact}");
facts.insert(key, fact);
}
Command::Recall(Some(key)) => match facts.get(&key) {
Some(fact) => println!("bot: {key} is {fact}"),
None => println!("bot: I don't know anything about {key}"),
},
Command::Recall(None) => {
if facts.is_empty() {
println!("bot: I don't remember anything yet. Try /remember");
} else {
// A HashMap has no order, so sort the names before printing them.
let mut names = Vec::new();
for name in facts.keys() {
names.push(name.as_str());
}
names.sort();
println!("bot: I remember: {}", names.join(", "));
}
}
Command::Find(word) => {
// The closure borrows `word` from here: a small function made on the spot.
let hits: Vec<&Message> = history
.iter()
.filter(|m| m.author == "you" && m.text.contains(&word))
.collect();
if hits.is_empty() {
println!("bot: you never said {word}");
} else {
for m in hits {
println!("{}", m.to_line());
}
}
}
Command::Last(n) => {
for m in last_n(&history, n) {
println!("{}", m.to_line());
}
}
Command::Say(text) => println!("bot: {text}"),
Command::Shout(text) => println!("bot: {}", Message::new("you", &text).shout()),
Command::Debug(text) => println!("bot: {:?}", parse(&text)),
Command::Chat(text) => {
messages_seen += 1;
let said = Message::new("you", &text);
let answer = Message::new("bot", &reply(&said.text));
println!("bot: {}", answer.text);
history.push(said);
history.push(answer);
// Message::new made the history its own copy, so `text` itself can
// be handed to `last` for /again: no extra copy needed.
last = Some(text);
}
}
}
println!("messages so far: {messages_seen}");
store::save(store::FILE, &history)?;
let s = if history.len() == 1 { "" } else { "s" };
println!("saved {} message{s} to {}", history.len(), store::FILE);
Ok(())
}
 
// Tests: `cargo test` builds and runs them; the program itself never sees them.
#[cfg(test)]
mod tests {
use super::*;
 
#[test]
fn reply_echoes_and_answers_questions() {
assert_eq!(reply("hi"), "you said: hi");
assert!(reply("are you there?").starts_with("Good question!"));
}
 
#[test]
fn reply_refuses_long_messages() {
let long = "x".repeat(MAX_LEN + 1);
assert!(reply(&long).starts_with("that is too long"));
}
 
#[test]
fn last_n_takes_from_the_end() {
assert_eq!(last_n(&[1, 2, 3, 4], 2), &[3, 4]);
assert_eq!(last_n(&["a", "b"], 5), &["a", "b"]);
assert!(last_n(&[1, 2, 3], 0).is_empty());
}
}

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.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does <code>&lt;T&gt;</code> after a function name mean?Reveal
It declares a type parameter: a placeholder for a type that each caller chooses. The compiler works it out from what is passed in.
Why is <code>a + b</code> refused inside <code>fn total&lt;T&gt;</code>?Reveal
The body must work for every type T, and most types cannot be added. That is error E0369; a bound narrows T to types that can.
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
No. The compiler writes a separate copy for each type it is used with (monomorphisation), so each copy is as fast as a hand-written one.

That is lesson 8.3. Tick it off so the course remembers where you are.

Next: 8.4 Traits →

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.

Learn Rust by building a chat app · Lesson 8.3 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026