Vibe Engines
YouTube

Lesson 3.4 · 9 min

Giving code a name

The chat loop is getting crowded: banner text, reply rules and the loop itself all sit in main. A function lets you give a piece of code a name, so main can say what it does instead of how.

Adds to Chatterboxreply() is its own function.

The shape of a function

You have been writing one since lesson 1.1: fn main(). A function is fn, a name, a list of inputs in brackets, an arrow and the type of what comes out, then the body in braces.

Each input is written name: Type, and the type is required: Rust never guesses at a function's edges. That is how a reader, and the compiler, know how to call it without reading the body.

One input, one output
fn shout(text: &str) -> String {
format!("{text}!")
}
 
fn main() {
println!("{}", shout("hello"));
println!("{}", shout("bye"));
}
$ cargo run
hello!
bye!

Two things to notice. &str is the plain-text type from lesson 2.4, and the String coming out is text the function made itself. And the body's last line, format!("{text}!"), has no semicolon. That is how a function hands back a value: whatever the last line produces is the answer. It is the same rule as the if blocks in lesson 3.1.

The classic slip

Add a semicolon and the last line turns into a statement that produces nothing:

A semicolon throws the answer away
fn shout(text: &str) -> String {
format!("{text}!");
}
 
fn main() {
println!("{}", shout("hello"));
}
error[E0308]: mismatched types
 --> src/main.rs:1:25
  |
1 | fn shout(text: &str) -> String {
  |    -----                ^^^^^^ expected `String`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     format!("{text}!");
  |                       - help: remove this semicolon to return this value

The message says the function was promised to return a String but its body gives (), which is Rust's word for "nothing". The compiler even points at the semicolon and suggests removing it. Remember this one: you will see it again.

PredictA function is reusable
fn banner() -> String {
String::from("Chatterbox v0.3")
}
 
fn main() {
println!("{}", banner());
println!("{}", banner());
}

What does this print?

  • Chatterbox v0.3 (once)
  • Chatterbox v0.3 Chatterbox v0.3
  • banner()
Reveal the answer

The program prints: Chatterbox v0.3 Chatterbox v0.3 Every call runs the body again. Writing the code once and calling it from several places is the whole point: change the banner in one spot and every caller gets the change.

Chatterbox v0.3, tidied

The program behaves the same, but main is now short enough to read in one glance. banner() builds the first line, and reply() holds the rules for what to say back. The loop only decides when to stop. Every line that reaches reply() now counts as a message, the commands included, which is a small change we will clean up when messages get a proper shape in chapter 5.

src/main.rs: Chatterbox v0.3
fn banner() -> String {
let bot = "Chatterbox";
let version = "0.3";
format!("{bot} v{version}")
}
 
fn reply(msg: &str) -> String {
match msg {
"" => String::from("(nothing to say)"),
"/help" => String::from("commands: /help, /quit"),
other => format!("you: {other}"),
}
}
 
fn main() {
let name = "Sam";
let mut message_count = 0;
let messages = ["hello", "", "/help", "how are you", "/quit", "never seen"];
 
println!("{}", banner());
println!("Hi {name}! I'm Chatterbox.");
 
for message in messages {
if message == "/quit" {
println!("bye, {name}!");
break;
}
println!("{}", reply(message));
message_count += 1;
}
println!("messages so far: {message_count}");
}
$ cargo run
Chatterbox v0.3
Hi Sam! I'm Chatterbox.
you: hello
(nothing to say)
commands: /help, /quit
you: how are you
bye, Sam!
messages so far: 4
Try itYour turn: write reply

Write the body of reply. An empty message returns (nothing to say), /help returns commands: /help, /quit, and anything else returns you: plus the text. Mind the semicolon.

Write the body of reply. An empty message returns (nothing to say), /help returns commands: /help, /quit, and anything else returns you: plus the text. Mind the semicolon.

fn reply(msg: &str) -> String {
// match on msg; the last expression is the answer
String::new()
}
 
fn main() {
println!("{}", reply("/help"));
}

Hidden tests (3): empty_message echoes_the_rest help_message

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
Why does the last line of a function body have no semicolon?Reveal
The value of the last expression is what the function returns. A semicolon turns it into a statement that gives nothing back.
What do the parts of fn reply(msg: &str) -> String mean?Reveal
It is named reply, takes one input called msg that is text, and gives back a String.
What does E0308 say when a function body ends in a semicolon?Reveal
Mismatched types: the function promised a value, but its body produced nothing.

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

Next: 3.5 Reading a line →
Learn Rust by building a chat app · Lesson 3.4 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026