Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

Learn RustChapter 2: Values

Lesson 2.6 · 11 min

Pairs and constants

Chatterbox has facts that never change while it runs: its name, its version, its length limit. Right now they are made inside main and handed to every function that needs them, one parameter at a time. A constant gives a fixed fact one home that the whole program can see, and a tuple lets the version be what it really is: a pair of numbers.

Adds to ChatterboxFixed facts become constants; the version becomes a pair of numbers.

Facts that never change

Look at Chatterbox as lesson 2.5 left it. bot and version are made with let in main, then passed into banner(bot, version) and greeting(name, bot). That is a lot of handing over for two facts that are the same from the first line of the program to the last. Why not let banner just use bot directly?

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 banner() -> String {
format!("{} v0.2", bot)
}
 
fn main() {
let bot = "Chatterbox";
println!("{}", banner());
}
error[E0425]: cannot find value `bot` in this scope
 --> src/main.rs:2:24
  |
2 |     format!("{} v0.2", bot)
  |                        ^^^ not found in this scope

E0425 in the compiler error atlas →

  1. cannot find value `bot` in this scope: inside banner, nothing is called bot.
  2. But line 6 makes one! It does, inside main. A name made with let belongs to the function it is made in. Its scope, the stretch of code where the name can be seen, ends at that function's closing brace, and banner is outside it.
  3. That is why the compiler also warns that bot in main is never used: from where it stands, no line in main reads it.
  4. This is a protection, not a nuisance: a function can only use what it is given, so you can understand one by reading it alone.

Fixes that were compiled for real

  • Hand it over as a parameter — compiles. What Chatterbox has done since lesson 2.1. It works, and it is the right tool for values that change from call to call. For a fact that never changes, every function that needs it gains one more parameter.
  • Trap: pass it in, but leave banner's first line alone — still E0425. Two errors now. E0061, because <code>banner()</code> still takes nothing and the call gives it one argument; and the E0425 stays, because the body still cannot see <code>main</code>'s <code>bot</code>. A value can only be passed in once the function has a parameter to receive it.
  • Make it a constant — compiles. Outside every function, visible to all of them, and no parameter anywhere. This is what the rest of the lesson is about.

const: one home for a fixed fact

A constant is a name for a value that is fixed for the whole program. You write it outside every function, so every function can see it:

const BOT: &str = "Chatterbox";

Three things are different from a let, and each has a reason.

  • The name is in capitals, with underscores between words: BOT, MAX_LEN. That is a convention, and the compiler warns if you break it, because it lets a reader tell at a glance that a name is a constant and not something made a few lines up.
  • The type is required. With let, Rust can often work the type out from how the name is used. A constant can be used anywhere in the program, so Rust insists you state its type once, where it is made, instead of guessing from all those places.
  • It can never change. There is no const mut. And the value must be something the compiler can work out while it builds the program, such as quoted text, a number or a sum of numbers, because it does not live in one place: the compiler copies the value straight into every spot that uses it.

So which does a fixed fact deserve? A let is for values the running program produces or changes: a name someone typed, a count. A const is for facts you knew before the program started: what the bot is called, the longest message it accepts. Making those constants means no function has to be handed them, nobody can change one by accident, and the top of the file reads like a list of the program's settings.

PredictA constant without a type
const BOT = "Chatterbox";
 
fn main() {
println!("I am {}.", BOT);
}

Does this compile — and if it does, does it run?

  • It compiles and runs
  • It does not compile
  • It compiles, then panics
Reveal the answer

It does not compile. It does not compile: missing type for `const` item. Even though "Chatterbox" is plainly text, a constant must say its type. The help line offers the fix, : &str, between the name and the =. (No E-number this time: like a few other messages, this one has only its words, and they say everything.)

error: missing type for `const` item
 --> src/main.rs:1:10
  |
1 | const BOT = "Chatterbox";
  |          ^ help: provide a type for the constant: `: &str`

Tuples: several values as one

Now the version. It has been the text "0.2", which prints nicely but is not really text: it is two numbers, a major version and a minor one. As text, you cannot do sums with it, such as working out the next version (the second exercise below does exactly that). What it needs is a way to keep two numbers together as one value.

That is a tuple: a few values grouped in round brackets, like (0, 2). Its type lists each part's type in the same brackets, so a pair of u32s is (u32, u32). The parts can be different types, such as ("Sam", 3), and a tuple always has exactly as many parts as it was made with. You read a part by its position, counting from zero: version.0 is the first part, version.1 the second. Or you can pull all the parts out into names in one go, which is called destructuring:

Make a pair, read its parts, pull it apart
fn main() {
let version: (u32, u32) = (0, 2);
println!("major {}, minor {}", version.0, version.1);
 
let (major, minor) = version;
println!("v{}.{}", major, minor);
}
$ cargo run
major 0, minor 2
v0.2
PredictWhich part goes where?
fn main() {
let version = (0, 2);
let (major, minor) = version;
println!("v{}.{}", minor, major);
}

The names are listed in a different order from the parts. What does this print?

  • v0.2
  • v2.0
  • v(0, 2)
Reveal the answer

The program prints: v2.0 Destructuring matches by position: major takes the first part (0) and minor the second (2). The println! then fills its holes in the order the names are listed, minor first, so it prints v2.0. The names mean nothing to Rust; only the positions do.

What does a tuple cost in memory? Usually just its parts: a pair of four-byte numbers is eight bytes, the same as two separate names. But not always exactly the sum. Rust places a four-byte number at a position in memory that is a multiple of four, because that is where the processor expects to find it. So when parts of different sizes are mixed, the compiler may leave a few unused bytes, called padding, to line the bigger parts up. A u8 and a u32 are five bytes of numbers, but together they make an eight-byte tuple.

Open the tabs: every size here is measured by the compiler, and the order of the parts is the one this compiler chose. (Rust is allowed to arrange a tuple's parts in memory however it likes, to keep padding small; .0 still always means the first part you wrote.)

Type InspectorA tuple is its parts, plus any padding

sizes measured with size_of on this compiler

TypeBytesLayout
u324the number (4 B)
(u32, u32)8.0 major (4 B) · .1 minor (4 B)
(u8, u8, u8)3.0 (1 B) · .1 (1 B) · .2 (1 B)
(u8, u32)8.0 (1 B) · padding (3 B) · .1 (4 B)
(&str, i32)24.0 the &str (16 B) · .1 (4 B) · padding (4 B)

Chatterbox v0.2: settings at the top

The top of the file now holds Chatterbox's settings, as three constants, each with a /// comment saying what it is for:

  • const BOT: &str = "Chatterbox"; is the bot's name.
  • const VERSION: (u32, u32) = (0, 2); is the version as a pair: major 0, minor 2.
  • const MAX_LEN: usize = 200; is the length limit from lesson 2.3, which used to be a let in main.

Because every function can see them, the functions shrink. banner() takes no parameters and builds Chatterbox v0.2 with format!("{} v{}.{}", BOT, VERSION.0, VERSION.1): two holes for the version, with a dot typed between them. greeting(name) keeps only the parameter that really does change, and reads the bot's name from BOT. In main, the bot, version and max_len lets are gone, and the settings line prints MAX_LEN. What is left in main is exactly what can vary: the person's name, the messages_seen counter and the quiet switch.

Chatterbox v0.2 after lesson 2.6: the whole program (green lines are new in this lesson)
Chatterbox prints (a real run)
Chatterbox v0.2
Hi Sam! I'm Chatterbox. Ask me anything.
(limit: 200 bytes a message, quiet: false)
messages so far: 1
Show the whole program (open it after you have tried the exercise below: it contains the answer)
src/main.rs14 new lines
// Chatterbox: a small chat program, built one lesson at a time.
 
/// 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, 2);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
 
/// 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 {}! I'm {}.", name, BOT);
text.push_str(" Ask me anything.");
text
}
 
fn main() {
let name = "Sam";
// u32: a whole number that is never negative and fits in 4 bytes.
let mut messages_seen: u32 = 0;
let quiet = false; // true would skip the greeting
 
println!("{}", banner());
println!("{}", greeting(name));
println!("(limit: {} bytes a message, quiet: {})", MAX_LEN, quiet);
 
messages_seen += 1;
println!("messages so far: {}", messages_seen);
}

Download this version (zip)Unzip, then cargo run inside the folder.

Try itYour turn: build the banner from the constants

This is Chatterbox v0.2 with the body of banner replaced by todo!(). Write it so the banner reads Chatterbox v0.2, built from BOT and the two parts of VERSION, not typed out. The hidden tests also check that the greeting still works.

This is Chatterbox v0.2 with the body of banner replaced by todo!(). Write it so the banner reads Chatterbox v0.2, built from BOT and the two parts of VERSION, not typed out. The hidden tests also check that the greeting still works.

// Chatterbox: a small chat program, built one lesson at a time.
 
/// 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, 2);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
 
/// The first line Chatterbox prints.
fn banner() -> String {
todo!()
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str) -> String {
let mut text = format!("Hi {}! I'm {}.", name, BOT);
text.push_str(" Ask me anything.");
text
}
 
fn main() {
let name = "Sam";
// u32: a whole number that is never negative and fits in 4 bytes.
let mut messages_seen: u32 = 0;
let quiet = false; // true would skip the greeting
 
println!("{}", banner());
println!("{}", greeting(name));
println!("(limit: {} bytes a message, quiet: {})", MAX_LEN, quiet);
 
messages_seen += 1;
println!("messages so far: {}", messages_seen);
}

Hidden tests (4): the_banner the_banner_starts_with_the_bot_name major_comes_before_minor the_greeting_reads_the_constant

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

Try itYour turn: the next version

When Chatterbox gains a feature, its minor version goes up by one and its major version stays the same. Make next_minor take a version pair and hand back the next one: (0, 2) becomes (0, 3). Pull the pair apart first, then build the new one.

When Chatterbox gains a feature, its minor version goes up by one and its major version stays the same. Make next_minor take a version pair and hand back the next one: (0, 2) becomes (0, 3). Pull the pair apart first, then build the new one.

fn next_minor(version: (u32, u32)) -> (u32, u32) {
// pull version apart into two names, then build the next pair
version
}
 
fn main() {
let (major, minor) = next_minor((0, 2));
println!("v{}.{}", major, minor);
}

Hidden tests (3): bumps_the_minor keeps_the_major starts_from_nothing

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

What you have now

Chapter 2 is done. Chatterbox v0.2 has settings at the top of the file, a version that is a real pair of numbers, a counter with a size, and a greeting that grows. It still does exactly the same thing every time you run it. In chapter 3 it starts to decide.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
Why can <code>banner</code> not use a name made with <code>let</code> in <code>main</code>?Reveal
A let name only exists inside the function it is made in. Its scope ends at that function's closing brace.
Write a constant for a length limit of 200 bytes.Reveal
const MAX_LEN: usize = 200; Capitals for the name, and the type is required.
When should a value be a <code>const</code> rather than a <code>let</code>?Reveal
When it is a fixed fact known before the program runs and never changes, such as a name or a limit. Values the program works out or changes use let.
Given <code>let version = (0, 2);</code>, what are <code>version.0</code> and <code>version.1</code>?Reveal
0 and 2: tuple parts are read by position, counting from zero.

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

Questions people ask

What is the difference between const and let in Rust?

A let makes a name inside one function, and with mut it can change. A const is written outside every function, can be used by all of them, must state its type, and never changes. Its value is worked out when the program is built.

Why does a Rust const need a type?

A constant can be used anywhere in the program, so Rust asks you to state its type once where it is made instead of inferring it from every place it is used. Leaving it out is a compile error, and the help line suggests the type.

What is a tuple in Rust?

A fixed-size group of values in round brackets, such as (0, 2). The parts can have different types, and you read them by position with .0, .1 and so on, counting from zero.

How do I get the values out of a tuple in Rust?

Read one part with .0 or .1, or destructure the whole tuple with a let, as in let (major, minor) = version. Destructuring matches by position, not by name.

Why are Rust constants written in capital letters?

It is the convention, and the compiler warns if a constant name is not in capitals. Capitals tell a reader at a glance that the name is a fixed fact of the program.

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