Vibe Engines
YouTube

Lesson 6.4 · 9 min

Small functions on the spot

You have been reading |m| m.text.len() as "given m, produce this". That is a closure: a small function written exactly where you need it. With one, Chatterbox can answer "show me the long messages".

Adds to ChatterboxA closure-powered /long command.

A function with no name

A closure is a small function you write in the middle of other code, without a name. The inputs go between two bars, then the result: |n| n + 1 means "given n, give back n plus one". If it is handy to reuse, put it in a let like any value and call it with brackets.

A closure in a let, called like a function
fn main() {
let double = |n: u32| n * 2;
println!("{}", double(21));
}
$ cargo run
42

The part that makes closures worth learning: they can use variables from the code around them. Rust calls that capturing. A regular fn cannot see the variables of the function that calls it; a closure can.

PredictUsing a variable from outside
fn main() {
let bonus = 3;
let add_bonus = |n: u32| n + bonus;
println!("{}", add_bonus(4));
}

What does this program print?

  • 4
  • 7
  • 3
Reveal the answer

The program prints: 7 The closure never declared bonus. It reached out to the one in main and used it. Capturing is by borrow when it can be: the closure only looks at bonus, so bonus is still yours.

Closures meet lists

The usual job for a closure is to say what to do with each item to a method that does the walking. Two you need now:

  • filter(|m| …) keeps only the items for which the closure says true. Like map, it is lazy, so add .collect() to get a Vec.
  • sort_by_key(|m| …) sorts a Vec by whatever number or text the closure returns for each item. Equal items keep their original order.

Here is a "long messages" filter. The variable limit lives outside the closure; the closure captures it, so changing the limit is a one-place edit. Two small details in the program below: the list holds &str items and iter() borrows them, so each closure argument is really a reference to a reference (&&str), and Rust lets you call .len() on it anyway. And .copied() turns those borrowed references into plain &str values so collect() can build a Vec<&str>.

filter keeps, sort_by_key orders, limit is captured
fn main() {
let texts = vec!["ok", "tell me about closures", "thanks", "and what about iterators then"];
let limit = 10;
let mut long: Vec<&str> = texts.iter().filter(|t| t.len() > limit).copied().collect();
long.sort_by_key(|t| t.len());
println!("{:?}", long);
}
$ cargo run
["tell me about closures", "and what about iterators then"]

Chatterbox v0.6: /long

Add a /long command. It filters the history for messages longer than limit characters, sorts them shortest first, and prints them. The filter closure uses limit from main, so the threshold lives in one named place. The history is only borrowed, so Chatterbox goes on taking messages afterwards.

Chatterbox now keeps a history, counts messages per user, and answers /history, /stats and /long.

src/main.rs: Chatterbox v0.6
use std::collections::HashMap;
 
struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: author.to_string(), text: text.to_string() }
}
 
fn shout(&self) -> String {
self.text.to_uppercase()
}
 
fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
 
enum Command {
Help,
Quit,
History,
Stats,
Long,
Say(String),
Unknown(String),
}
 
fn parse(line: &str) -> Command {
match line {
"/help" => Command::Help,
"/quit" => Command::Quit,
"/history" => Command::History,
"/stats" => Command::Stats,
"/long" => Command::Long,
_ => match line.strip_prefix("/say ") {
Some(rest) => Command::Say(rest.to_string()),
None => Command::Unknown(line.to_string()),
},
}
}
 
fn main() {
let mut history: Vec<Message> = Vec::new();
let mut counts: HashMap<String, u32> = HashMap::new();
let limit = 20;
let lines = [
("sam", "/say hello there"),
("ada", "/say has anyone read the new model release notes?"),
("sam", "/say yes, the context window got a lot bigger"),
("ada", "/stats"),
("sam", "/long"),
("sam", "/quit"),
];
for (user, line) in lines {
match parse(line) {
Command::Help => println!("commands: /help /history /stats /long /say /quit"),
Command::Quit => break,
Command::History => {
for (i, m) in history.iter().enumerate() {
println!("{}. {}: {}", i + 1, m.author, m.text);
}
}
Command::Stats => {
let mut names: Vec<&String> = counts.keys().collect();
names.sort();
for name in names {
println!("{name}: {}", counts[name]);
}
}
Command::Long => {
let mut long: Vec<&Message> =
history.iter().filter(|m| m.text.len() > limit).collect();
long.sort_by_key(|m| m.text.len());
for m in long {
println!("long ({} chars): {}", m.text.len(), m.text);
}
}
Command::Say(text) => {
history.push(Message::new(user, &text));
*counts.entry(user.to_string()).or_insert(0) += 1;
}
Command::Unknown(other) => println!("unknown command: {other}"),
}
}
}
$ cargo run
ada: 1
sam: 2
long (40 chars): yes, the context window got a lot bigger
long (44 chars): has anyone read the new model release notes?
Try itYour turn: the long ones

Write long_messages. Return the text of every message longer than limit characters (strictly longer), shortest first. Use a filter closure and sort_by_key.

Write long_messages. Return the text of every message longer than limit characters (strictly longer), shortest first. Use a filter closure and sort_by_key.

struct Message {
author: String,
text: String,
}
 
impl Message {
fn new(author: &str, text: &str) -> Message {
Message { author: author.to_string(), text: text.to_string() }
}
}
 
fn long_messages(history: &Vec<Message>, limit: usize) -> Vec<String> {
// keep messages longer than limit, shortest first
Vec::new()
}
 
fn main() {
let history = vec![Message::new("sam", "hi"), Message::new("ada", "this one is quite long")];
println!("{:?}", long_messages(&history, 5));
}

Hidden tests (4): empty_history drops_short_messages exactly_the_limit_is_not_long shortest_first

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

What you have now

A closure is a function you write where you use it, and it can read the variables around it. Paired with filter, map and sort_by_key it replaces most hand-written loops over a list. Chapter 6 is done: Chatterbox has a history, counts, and a way to ask questions of both.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does |n| n + 1 mean?Reveal
A closure: given n, produce n plus one. The inputs sit between the bars and the result follows.
What does it mean for a closure to capture a variable?Reveal
It uses a variable from the surrounding code without it being passed in. Where possible it just borrows it.
What does filter(|m| m.text.len() > limit) keep?Reveal
Only the messages whose text is longer than limit characters.

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

Questions people ask

What is a closure in Rust?

A function without a name, written inline with |inputs| result, that can use variables from the code around it.

Is a closure the same as a Python lambda?

It is the nearest idea. A Rust closure can also have a full block body in braces, and it is typed: the compiler knows its inputs and result.

Does sort_by_key keep equal items in order?

Yes. It is a stable sort, so items with equal keys stay in their original order.

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