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.5 · 12 min

How long a borrow lasts

Back in lesson 4.6, first_word handed back a window into your message, and the compiler somehow knew that window was safe to use. This lesson answers how. The answer has a name, lifetimes, and most of the time you never write one.

Adds to ChatterboxNothing new: you learn why first_word compiles.

Compiler errors you will meet E0106

The question first_word left open

Chatterbox's command.rs still has the function from lesson 4.6: fn first_word(text: &str) -> &str. It does not copy anything. It hands back a borrow: a window onto the first few bytes of text that somebody else owns. A window is only useful while the thing behind it still exists. So how does the compiler know, everywhere first_word is called, that the text is still there when the window is used?

Start with the danger itself, in a tiny program. The curly braces in the middle make an inner block: anything made inside it is cleaned up at its closing brace, exactly like a function's frame in lesson 4.1.

PredictA borrow that leaves the block
fn main() {
let r;
{
let msg = String::from("hello");
r = &msg;
}
println!("{r}");
}

What happens when this program is compiled?

  • It compiles and runs
  • Compile error E0597
  • It compiles, then panics
Reveal the answer

It does not compile: E0597. The text msg owns is cleaned up at the closing brace on line 6, but r is used on line 7. A borrow that outlives its owner would point at freed memory, so the compiler refuses to build the program at all.

error[E0597]: `msg` does not live long enough
 --> src/main.rs:5:13
  |
4 |         let msg = String::from("hello");
  |             --- binding `msg` declared here
5 |         r = &msg;
  |             ^^^^ borrowed value does not live long enough
6 |     }
  |     - `msg` dropped here while still borrowed
7 |     println!("{r}");
  |                - borrow later used here
Ownership Stager would outlive the text it points at

Step through the program. Solid arrows are ownership; dashed arrows are borrows. Use ← → or the slider.

fn main() {
let r;
{
let msg = String::from("hello");
r = &msg;
}
println!("{r}");
}
  1. Line 1. The program starts in main. Line 2 announces r, but r holds nothing yet, so it gets no row until line 5.
  2. Line 4. Inside the inner block, msg is made. Its text lives on the heap and msg holds the one key.
  3. Line 5. r borrows msg: the dashed arrow is a loan, not a key. r: &String borrows msg.
  4. Line 6. The inner block ends. msg was made inside it, so msg goes away and its text is freed. r still lives, because it was made outside the block, and its arrow now points at memory that holds nothing.
  5. Line 7. Printing r would read freed memory: a dangling reference. The compiler stops the build here with E0597, so this picture never happens on a real machine.

A lifetime is a stretch of code

In languages without this check, a reference like r is a classic bug: the program reads memory that has been handed back and maybe reused, and prints garbage, crashes, or worse. Rust rules it out before the program exists.

To do that, the compiler gives every borrow a lifetime: the stretch of the program during which the borrow is good. Here it is from line 5 to the last use on line 7. The owner msg only lives until line 6. The rule is one sentence: a borrow's lifetime must fit inside its owner's. Line 7 breaks it, and that is E0597.

The same rule explains a mistake everyone makes once: making a value inside a function and handing back a borrow of it. When the function returns, its frame is gone, and the value with it.

text lives in greeting's frame, which ends at the return. The fix is to hand back the String itself, the key and all.
fn greeting(name: &str) -> &str {
let text = format!("Hi {name}!");
&text
}
 
fn main() {
println!("{}", greeting("Sam"));
}
error[E0515]: cannot return reference to local variable `text`
 --> src/main.rs:3:5
  |
3 |     &text
  |     ^^^^^ returns a reference to data owned by the current function

Why first_word needs nothing written

Here is the catch. The compiler checks each function on its own, by its first line, its signature. When it checks a call to first_word, it does not read the body. So the signature has to say where the returned borrow comes from. For most functions there is only one sensible answer, and Rust fills it in for you. This is called lifetime elision (elided means "left out"), and it follows three rules:

  1. Every borrowed input gets its own lifetime.
  2. If there is exactly one borrowed input, the output borrows from it.
  3. If the function is a method with &self, the output borrows from self.

first_word(text: &str) -> &str has one borrowed input, so rule 2 says the result borrows from text, and every caller is checked against that. Chatterbox's arg works the same way. And the name(&self) -> &str method you wrote for Replier in lesson 8.4 is rule 3.

The rules run out when there is no single answer. Then the compiler asks you.

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 longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
 
fn main() {
println!("{}", longer("hello there", "hi"));
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:32
  |
1 | fn longer(a: &str, b: &str) -> &str {
  |              ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
  |          ++++     ++          ++          ++

E0106 in the compiler error atlas →

  1. missing lifetime specifier: the output is a borrow, and the compiler cannot tell how long it is good for.
  2. the signature does not say whether it is borrowed from `a` or `b`: two borrowed inputs, so rule 2 does not apply. The body picks one at run time; the signature has to cover both.
  3. consider introducing a named lifetime parameter: the compiler suggests the exact fix: a name, 'a, on both inputs and the output.

Fixes that were compiled for real

  • Name one lifetime for all three — compiles. <code>&lt;'a&gt;</code> introduces the name, like <code>&lt;T&gt;</code> introduced a type in lesson 8.3. It reads: both inputs are good for at least 'a, and so is the result.
  • Return an owned String instead — compiles, with a cost. No borrow, so nothing to describe. It copies the text, which is fine for a short message.
  • Trap: tie the result to a only — still E0621. Now the signature promises the result comes from <code>a</code>, and the body can return <code>b</code>. The compiler holds the body to its own signature.

Notice what 'a does not do: it does not make anything live longer. A lifetime annotation only describes how borrows relate, so the compiler can check each call. With one 'a on both inputs, the result is good only while both are, which is the shorter of the two. Try it.

PredictThe longer one is still alive
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
 
fn main() {
let first = String::from("a long message");
let result;
{
let second = String::from("hi");
result = longer(&first, &second);
}
println!("{result}");
}

first is longer, so longer returns first, which lives to the end. What does the compiler say?

  • It compiles and runs
  • Compile error E0597
  • Compile error E0106
Reveal the answer

It does not compile: E0597. The compiler does not run your program to see which one comes back. The signature says the result may be either input, so it is only good while both are, and second is gone after line 11.

error[E0597]: `second` does not live long enough
  --> src/main.rs:10:33
   |
 9 |         let second = String::from("hi");
   |             ------ binding `second` declared here
10 |         result = longer(&first, &second);
   |                                 ^^^^^^^ borrowed value does not live long enough
11 |     }
   |     - `second` dropped here while still borrowed
12 |     println!("{result}");
   |                ------ borrow later used here
Borrow TimelineHow long does each borrow live?

Each bar is one borrow, drawn from its first use to its last. Scrub the line to see what is alive.

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
 
fn main() {
let first = String::from("a long message");
let result;
{
let second = String::from("hi");
result = longer(&first, &second);
}
println!("{result}");
}
  • first: owner — owner, lines 6–13
  • first: result may borrow this — shared borrow, lines 10–12
  • second: owner (inner block) — owner, lines 9–11
  • second: result may borrow this — shared borrow, lines 10–12

The top row is fine: the loan ends inside its owner. In the bottom row the loan runs past line 11, where second is dropped. Moving the println! inside the block, or making second before the block, fixes it.

Text that lasts forever: 'static

One lifetime has a name built in. Text in quotes, like "/help", is written into the program file itself, so it exists for the whole run. Its type is &'static str: a borrow good for as long as the program runs. Chatterbox's const HELP: &str is one of those; inside a const, Rust fills in 'static for you. In a function with no borrowed input at all, there is nothing to borrow from, so you must say it.

PredictA borrow from nowhere
fn bot_name() -> &str {
"Chatterbox"
}
 
fn main() {
println!("{}", bot_name());
}

This function takes nothing and returns text in quotes. What does the compiler say?

  • It compiles and runs
  • Compile error E0106
  • Compile error E0515
Reveal the answer

It does not compile: E0106. With no borrowed input, no elision rule applies, so the output's lifetime is missing. The compiler suggests &'static str, which is right here: the text is part of the program. It also suggests returning a String, which is usually right when the text is built at run time.

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:18
  |
1 | fn bot_name() -> &str {
  |                  ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn bot_name() -> &'static str {
  |                   +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn bot_name() -> &str {
1 + fn bot_name() -> String {
  |
Try itYour turn: which message said it first?

A stand-alone function Chatterbox could use for a smarter /find: return the first message in history that contains word, as a borrow of that message, or None. The starter does not compile: run it and read the E0106. Decide which input the answer borrows from, write the signature, then the body. One hidden test drops the word before it uses the answer.

A stand-alone function Chatterbox could use for a smarter /find: return the first message in history that contains word, as a borrow of that message, or None. The starter does not compile: run it and read the E0106. Decide which input the answer borrows from, write the signature, then the body. One hidden test drops the word before it uses the answer.

// Which message did you first say this word in?
fn first_mention(history: &[String], word: &str) -> Option<&str> {
// look through history and return the first message that contains word
None
}
 
fn main() {
let history = vec![String::from("I like rust"), String::from("tea time")];
println!("{:?}", first_mention(&history, "tea"));
}

Hidden tests (4): an_empty_history_is_none finds_the_first_mention a_word_never_said_is_none the_word_can_go_before_the_answer_is_used

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

If you have coded before

In Python a value stays alive as long as anything refers to it, and a garbage collector frees it later, so a dangling reference cannot happen, at the cost of that collector running alongside your program. In C, nothing stops one. Rust gets Python's safety without a collector by checking lifetimes while compiling, so the finished program carries no extra work.

What you have now

Chatterbox did not change in this lesson; you can now read it more closely. Every & in it has a lifetime, and the compiler checked each one fits inside its owner. Elision wrote them for first_word, arg and Replier::name, and you write one only when a function returns a borrow and the rules cannot tell where it comes from. As a preview, chapter 9 adds JSON and the real model.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What is a lifetime?Reveal
The stretch of a program during which a borrow is good. The compiler requires every borrow's lifetime to fit inside its owner's.
Why does <code>fn first_word(text: &amp;str) -&gt; &amp;str</code> need no annotation?Reveal
It has exactly one borrowed input, so the elision rules say the output borrows from it.
What does <code>fn longer&lt;'a&gt;(a: &amp;'a str, b: &amp;'a str) -&gt; &amp;'a str</code> promise the caller?Reveal
The result is good for as long as both inputs are, which is the shorter of the two. It does not make anything live longer.
What is <code>&amp;'static str</code>?Reveal
A borrow of text that lasts for the whole program, such as text written in quotes in the code.
Your Chatterbox so far: v0.8 (after lesson 8.4)
Chatterbox v0.8 after lesson 8.4: the whole program
src/main.rs
// Chatterbox: a small chat program, built one lesson at a time.
mod command;
mod message;
mod replier;
mod store;
 
use command::{Command, parse};
use message::Message;
use replier::{Echo, Pretend, Replier};
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>, /model <echo|pretend>, /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
}
 
/// 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
// Box<dyn Replier>: "some Replier, decided while the program runs".
let mut replier: Box<dyn Replier> = Box::new(Echo { max_len: MAX_LEN });
 
println!("{} (replies by: {})", banner(), replier.name());
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 => match &last {
Some(text) => println!("bot: {}", replier.reply(&[Message::new("you", text)])),
None => 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::Model(choice) => {
let next: Option<Box<dyn Replier>> = match choice.as_str() {
"echo" => Some(Box::new(Echo { max_len: MAX_LEN })),
"pretend" => Some(Box::new(Pretend)),
_ => None,
};
match next {
Some(r) => {
replier = r;
println!("bot: replies now come from {}", replier.name());
}
None => {
println!("bot: I don't know a model called {choice}. Try echo or pretend")
}
}
}
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;
history.push(Message::new("you", &text));
// the history holds its own copy, so `text` can move into `last`
last = Some(text);
let answer = replier.reply(&history);
println!("bot: {answer}");
history.push(Message::new("bot", &answer));
}
}
}
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 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.rs
//! 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),
Model(String),
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")?)?),
"/model" => Command::Model(need(line, "model", "pretend")?),
"/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.rs
//! 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/replier.rsnew file
//! Who answers: anything that can reply, behind one trait.
use crate::message::Message;
 
/// A promise with a name: any type that implements Replier can answer a
/// conversation. The chat loop only ever talks to this promise.
pub trait Replier {
/// A short name, for "/model" and the banner.
fn name(&self) -> &str;
/// The answer to the conversation so far (the last message is the newest).
fn reply(&self, history: &[Message]) -> String;
}
 
/// The echo Chatterbox has had since chapter 3, now as one kind of Replier.
pub struct Echo {
pub max_len: usize,
}
 
impl Replier for Echo {
fn name(&self) -> &str {
"echo"
}
 
fn reply(&self, history: &[Message]) -> String {
let message = match history.last() {
Some(m) => &m.text,
None => return String::from("(nothing to reply to)"),
};
if message.len() > self.max_len {
format!("that is too long (the limit is {} bytes)", self.max_len)
} 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}")
}
}
}
 
/// A stand-in for a real model: it shows that it was handed the WHOLE
/// conversation, which is exactly what the real one will need.
pub struct Pretend;
 
impl Replier for Pretend {
fn name(&self) -> &str {
"pretend"
}
 
fn reply(&self, history: &[Message]) -> String {
match history.last() {
Some(m) => format!(
"(pretend model) I read all {} message{}. You last said: {}",
history.len(),
if history.len() == 1 { "" } else { "s" },
m.text
),
None => String::from("(pretend model) nothing to read yet"),
}
}
}
 
#[cfg(test)]
mod tests {
use super::*;
 
fn said(text: &str) -> Vec<Message> {
vec![Message::new("you", text)]
}
 
#[test]
fn echo_repeats_you() {
assert_eq!(Echo { max_len: 200 }.reply(&said("hi")), "you said: hi");
}
 
#[test]
fn echo_refuses_long_messages() {
assert!(
Echo { max_len: 5 }
.reply(&said("far too long"))
.starts_with("that is too long")
);
}
 
#[test]
fn pretend_reads_the_whole_history() {
let history = vec![
Message::new("you", "a"),
Message::new("bot", "b"),
Message::new("you", "c"),
];
assert_eq!(
Pretend.reply(&history),
"(pretend model) I read all 3 messages. You last said: c"
);
}
}
src/store.rs
//! 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.

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

Questions people ask

What are lifetimes in Rust?

A lifetime is the part of the program during which a reference is valid. The compiler checks that no reference is used after the value it points at is gone. Most lifetimes are worked out for you; you write one, like the a in longer, when a function returns a reference and the compiler cannot tell which input it comes from.

What does missing lifetime specifier mean?

It is error E0106. A function returns a reference, but its signature does not say which input that reference borrows from, usually because there are two reference inputs, or none. Name a lifetime and put it on the output and the input it comes from, or return an owned value such as a String.

Do lifetime annotations make a value live longer?

No. They only describe how long references are valid relative to each other so the compiler can check every call. How long a value lives is still decided by its owner and its scope.

What is the static lifetime?

It means the reference is valid for the entire run of the program. Text written in quotes in your code has the static lifetime because it is stored in the program file itself.

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