Vibe Engines
YouTube

Lesson 8.2 · 10 min

One name for anything that replies

Chatterbox has a fake reply today and will have a real model tomorrow. The chat loop should not care which. A trait is how Rust lets you say "anything that can reply", and swap the thing behind it without touching the loop.

Adds to ChatterboxA Replier trait with an echo and a pretend model.

A promise with a name

Right now, something has to produce the bot's reply. Today that is a function that echoes your words. Soon it will be a call to a real model over the internet. A chat loop that says "call the echo function" would have to be rewritten for the model. A chat loop that says "ask whatever replies" would not.

A trait is a list of things a type can do, with a name. It is a promise: any type that implements this trait has these methods. Ours is called Replier and promises one method, reply, which takes the user's text and gives back text.

src/main.rs: Chatterbox v0.8, the Replier trait
trait Replier {
fn reply(&self, msg: &str) -> String;
}
 
struct EchoReplier;
 
impl Replier for EchoReplier {
fn reply(&self, msg: &str) -> String {
format!("you said: {msg}")
}
}
 
struct ModelReplier {
model: String,
}
 
impl Replier for ModelReplier {
fn reply(&self, msg: &str) -> String {
format!("[{}] pretend answer to: {msg}", self.model)
}
}
 
fn main() {
let echo = EchoReplier;
let model = ModelReplier { model: "big-model".to_string() };
println!("{}", echo.reply("hi"));
println!("{}", model.reply("hi"));
}
$ cargo run
you said: hi
[big-model] pretend answer to: hi

Read impl Replier for EchoReplier as "here is how EchoReplier keeps the promise". The two structs have nothing else in common: one holds nothing, the other holds a model name. Both can reply. For now ModelReplier only pretends. In the next lesson it does the real call, and nothing else in the program changes.

When a type breaks the promise

A function can say "I accept anything that implements Replier" by writing &dyn Replier in its argument. If you hand it something that never made the promise, the compiler stops you.

PredictA type with no promise
trait Replier {
fn reply(&self, msg: &str) -> String;
}
 
struct Mute;
 
fn talk(r: &dyn Replier, msg: &str) {
println!("{}", r.reply(msg));
}
 
fn main() {
talk(&Mute, "hello");
}

What happens when this program is compiled?

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

It does not compile: E0277. E0277 is "the trait bound is not satisfied". Mute never wrote impl Replier for Mute, so it cannot be used where a Replier is expected. This is checked before the program ever runs.

error[E0277]: the trait bound `Mute: Replier` is not satisfied
  --> src/main.rs:12:10
   |
12 |     talk(&Mute, "hello");
   |          ^^^^^ unsatisfied trait bound
   |
help: the trait `Replier` is not implemented for `Mute`
  --> src/main.rs:5:1
   |
 5 | struct Mute;
   | ^^^^^^^^^^^
help: this trait has no implementations, consider adding one
  --> src/main.rs:1:1
   |
 1 | trait Replier {
   | ^^^^^^^^^^^^^
   = note: required for the cast from `&Mute` to `&dyn Replier`

Many kinds behind one box

Sometimes you want to choose the implementor while the program runs: if there is an API key, use the model; if not, echo. The two structs have different sizes, so a variable cannot hold "either" directly. The answer is Box<dyn Replier>. A Box is a pointer to a value on the heap, and dyn means "some type that implements this trait, decided at run time". The box has one fixed size whatever is inside it.

That gives the chat loop one type to work with, and the code that builds the box is the only place that knows which kind it is. Next lesson, that choice is made from an environment variable.

Try itYour turn: reply through the trait

Finish the two impl blocks (echo says you said: <msg>, the model says [<model>] pretend answer to: <msg>), then write all_replies so it asks every boxed replier in order.

Finish the two impl blocks (echo says you said: <msg>, the model says [<model>] pretend answer to: <msg>), then write all_replies so it asks every boxed replier in order.

trait Replier {
fn reply(&self, msg: &str) -> String;
}
 
struct EchoReplier;
 
struct ModelReplier {
model: String,
}
 
impl Replier for EchoReplier {
fn reply(&self, msg: &str) -> String {
// you said: <msg>
String::new()
}
}
 
impl Replier for ModelReplier {
fn reply(&self, msg: &str) -> String {
// [<model>] pretend answer to: <msg>
String::new()
}
}
 
fn all_replies(repliers: &[Box<dyn Replier>], msg: &str) -> Vec<String> {
// ask each replier, keep the order
Vec::new()
}
 
fn main() {
let repliers: Vec<Box<dyn Replier>> = vec![
Box::new(EchoReplier),
Box::new(ModelReplier { model: "big-model".to_string() }),
];
for line in all_replies(&repliers, "hi") {
println!("{line}");
}
}

Hidden tests (4): all_replies_keeps_order echo_repeats no_repliers_no_replies model_names_itself

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

What you have now

Chatterbox v0.8 has a Replier trait, a fake EchoReplier, and a ModelReplier that pretends. The chat loop only has to hold a Box<dyn Replier>. Next lesson, ModelReplier stops pretending.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What is a trait?Reveal
A named list of methods a type can promise to have. Any type that writes impl Trait for Type keeps the promise.
What does <code>Box&lt;dyn Replier&gt;</code> mean?Reveal
A pointer to some value that implements Replier, with the exact type decided at run time. The box itself always has the same size.
What error appears when you pass a type that does not implement the trait?Reveal
E0277, the trait bound is not satisfied.

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

Next: 8.3 One HTTP call →

Questions people ask

What is a trait in Rust?

A set of method signatures that different types can implement. It is Rust's way of describing shared behaviour, similar to an interface in other languages.

What is the difference between dyn Trait and impl Trait?

impl Trait in an argument fixes one concrete type at compile time. dyn Trait, usually behind a Box or a reference, lets the concrete type be chosen while the program runs.

Why do I need Box for a trait object?

Different implementors have different sizes and a variable needs one size. A Box is a pointer, which always has the same size, to the value on the heap.

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