Vibe Engines
YouTube

Lesson 5.2 · 8 min

Giving a struct behaviour

A Message can hold an author and a text, but everything you do with one still lives in loose functions. An impl block puts the actions next to the data they act on.

Adds to ChatterboxMessage::new, plus shout and is_empty methods.

Functions that live with the struct

You can already write fn describe(msg: &Message). An impl block lets you attach that same function to the type, so it is called with a dot: msg.shout(). Same borrowing rules as chapter 4; just a tidier address.

Two kinds of thing go in an impl. A function without self, like new, is called on the type: Message::new(..). A function whose first parameter is self is a method, called on a value.

new builds one, shout and is_empty only look
struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: String::from(author), text: String::from(text) }
}
 
fn shout(&self) -> String {
self.text.to_uppercase()
}
 
fn is_empty(&self) -> bool {
self.text.trim().is_empty()
}
}
 
fn main() {
let m = Message::new("you", "hello there");
println!("{}", m.shout());
println!("{}", m.is_empty());
}
$ cargo run
HELLO THERE
false

&self is shorthand for "borrow this message, read only". Inside the method, self.text is the field. The caller keeps the key, which is why you can call shout and then is_empty on the same m.

A method that needs to change the message says &mut self, the exclusive borrow from lesson 4.4.

A &mut self method on a binding that is not mut
struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: String::from(author), text: String::from(text) }
}
 
fn clear(&mut self) {
self.text.clear();
}
}
 
fn main() {
let m = Message::new("you", "oops");
m.clear();
println!("{}", m.author);
}
error[E0596]: cannot borrow `m` as mutable, as it is not declared as mutable
  --> src/main.rs:18:5
   |
18 |     m.clear();
   |     ^ cannot borrow as mutable
   |
help: consider changing this to be mutable
   |
17 |     let mut m = Message::new("you", "oops");
   |         +++

E0596: "cannot borrow m as mutable, as it is not declared as mutable". Calling clear quietly takes a &mut borrow of m, and you never said m could change. The fix is the same word as in lesson 2.2: let mut m.

So the signature is a promise you can read from outside. &self: looks only. &mut self: may change it. self: takes the key and gives it away.

PredictCall a method twice
struct Message {
text: String,
}
 
impl Message {
fn shout(&self) -> String {
self.text.to_uppercase()
}
}
 
fn main() {
let m = Message { text: String::from("hi") };
let a = m.shout();
let b = m.shout();
println!("{a}{b}{}", m.text);
}

What does this print?

  • HIHIhi
  • HIHI
  • it does not compile
Reveal the answer

The program prints: HIHIhi shout borrows with &self and returns a brand-new String. The original m.text is untouched and m is still usable, so you can call it as often as you like.

src/main.rs: Chatterbox v0.5, Message grows methods
struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: String::from(author), text: String::from(text) }
}
 
fn shout(&self) -> String {
self.text.to_uppercase()
}
 
fn is_empty(&self) -> bool {
self.text.trim().is_empty()
}
}
 
fn main() {
let m = Message::new("you", "hello");
let blank = Message::new("you", " ");
println!("{} {}", m.shout(), blank.is_empty());
}
$ cargo run
HELLO true
Try itYour turn: word_count and append

Fill in both methods. word_count only looks and returns how many words are in the text: split_whitespace() splits text into words and .count() counts what it gives you. The count is a usize, a whole number used for sizes and positions. append changes the message: it adds a space and then the extra text to the end.

Fill in both methods. word_count only looks and returns how many words are in the text: split_whitespace() splits text into words and .count() counts what it gives you. The count is a usize, a whole number used for sizes and positions. append changes the message: it adds a space and then the extra text to the end.

struct Message {
text: String,
}
 
impl Message {
fn word_count(&self) -> usize {
// count the words in self.text
0
}
 
fn append(&mut self, extra: &str) {
// add a space, then extra, to the end of self.text
}
}
 
fn main() {
let mut m = Message { text: String::from("hello big") };
m.append("world");
println!("{} words", m.word_count());
}

Hidden tests (4): append_joins_with_a_space append_then_count counts_words empty_has_no_words

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 is the difference between <code>Message::new(..)</code> and <code>m.shout()</code>?Reveal
new has no self, so it is called on the type and builds a value. shout takes &self, so it is called with a dot on a value that exists.
Why did <code>m.clear()</code> fail with E0596?Reveal
clear needs &mut self, an exclusive borrow, and m was not declared mut.
What does <code>&amp;self</code> promise the caller?Reveal
The method only reads the value. The caller keeps ownership and can use the value again afterwards.

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

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