4.xChapter 4 summary and quiz
This was the chapter where Rust's type system stopped being scenery. The review, then two programs that need most of it.
Quick review
Memory is bits in 8-bit, addressable bytes; a data type fixes how many bytes a value uses and what the bits mean, and n bits afford 2ⁿ values. Sizes are exact and in the names: ten integer types from i8/u8 to i128/u128 (signed i for possibly-negative, unsigned u for never-negative), plus pointer-sized usize/isize, which exist to measure memory and arrive on their own with collections. Default to i32; deviate with a reason; constants like i32::MAX know the edges. Different numeric types never mix implicitly, which converts the classic signed/unsigned disasters into compile errors.
Integer literals take suffixes (42u8), underscores, and bases (0xFF, 0o377, 0b1010); out-of-range literals are rejected at compile time, range quoted. Runtime integer overflow is defined: dev builds panic at the guilty operation, release builds wrap like an odometer, and the checked_/wrapping_/saturating_ families make the policy explicit when the edge is part of the job.
Floating-point f32/f64 (default and recommendation: f64) hold approximations: ~7 versus ~15-16 trustworthy digits, binary fractions that can't quite say 0.1, rounding that accumulates (ten dimes ≠ one dollar), so never compare floats with == (tolerance technique in 6.4). Division by zero yields inf/-inf/NaN rather than a panic, and NaN equals nothing, itself included.
bool is true/false, one byte, flipped by !, born mostly from comparisons (==, !=, <, >, <=, >=), and there is no truthiness: integers don't pass for bools, and if count != 0 says what if (count) only implied. if demands a bool condition, skips the parentheses, insists on braces, chains with else if, and is an expression: let label = if c { a } else { b }, arms agreeing on type, else mandatory in value position. The ternary operator sends its regrets. And if x = 5 is a compile error here, paying off lesson 1.4's oldest warning.
char is four bytes, one Unicode scalar value, single-quoted, emoji included; '5' is catalog entry 53, not the number 5. Type inference types your locals from values and usage, whole function body at a time, falling back to i32/f64, asking ("type annotations needed") when evidence runs out; signatures stay explicit by design. Conversions are explicit with as: widening exact, integer-narrowing truncates bits, float-to-int truncates then saturates, int-to-float can round, and none of it warns, because the keyword is your signature. Tuples group fixed, possibly-mixed values ((i32, f64)), unpack by .0/.1 or destructuring (let (x, y) = ...), give functions multiple return values, and at length zero turn out to be the unit type () you've known since lesson 1.11.
Quiz time
Question #1
Pick the most appropriate type, one phrase of justification each:
a) the number of students in a school
b) whether a checkbox is ticked
c) a bank account balance in cents, possibly overdrawn, possibly enormous
d) the percentage of a download completed, shown as 73.4%
e) the key a player pressed, like 'w'
Show solution
a) i32: the default; school sizes don't strain it.
b) bool: it is a yes/no.
c) i64: signed because overdrawn happens, 64-bit because enormous happens, and cents-as-integers because lesson 4.5 told you what floats do to money.
d) f64: fractional by nature, and the default float.
e) char: one character is what it is.
Question #2
Write the chapter's calculator. It reads two numbers (allow decimals), then an operator (+, -, *, or /), and prints the result in the format shown; for anything else it admits defeat politely. A sample run:
Enter a number:
8
Enter another number:
2
Enter an operator (+, -, *, or /):
*
8 * 2 is 16
Hints: the lesson 1.12 recipe reads an f64 if you change one annotation; read the operator as text and compare it (after trim) against "+" and friends with ==; an else chain dispatches; lesson 2.6's decomposition advice applies.
Show solution
fn main() {
let a = read_number();
let b = read_number();
println!("Enter an operator (+, -, *, or /):");
let mut op_input = String::new();
std::io::stdin()
.read_line(&mut op_input)
.expect("failed to read input");
let op = op_input.trim();
if op == "+" {
println!("{} + {} is {}", a, b, a + b);
} else if op == "-" {
println!("{} - {} is {}", a, b, a - b);
} else if op == "*" {
println!("{} * {} is {}", a, b, a * b);
} else if op == "/" {
println!("{} / {} is {}", a, b, a / b);
} else {
println!("'{op}' isn't an operator I know.");
}
}
fn read_number() -> f64 {
println!("Enter a number:");
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("failed to read input");
input.trim().parse().expect("that wasn't a number")
}
Things worth checking against your version: the recipe's annotation became f64 (via the return type, lesson 4.9's evidence rules at work); the operator is compared as trimmed text; the final else handles the unexpected, per lesson 3.6's defensive habit. Slightly different prompts or formats: fine. (Two honest limitations you now know enough to name: dividing by zero produces inf since these are floats, and typing potato as a number still panics via expect. Chapter 12 civilizes both.)
Question #3 (extra credit, the famous one)
A ball drops from a tower. The user enters the tower's height in meters; gravity is 9.8 m/s², and after t seconds the ball has fallen 9.8 · t² / 2 meters. Print the ball's height at t = 0 through 5, clamping at the ground:
Enter the height of the tower in meters:
100
At 0 seconds, the ball is at height: 100 meters
At 1 seconds, the ball is at height: 95.1 meters
At 2 seconds, the ball is at height: 80.4 meters
At 3 seconds, the ball is at height: 55.9 meters
At 4 seconds, the ball is at height: 21.599999999999994 meters
At 5 seconds, the ball is on the ground.
(Yes, the t = 4 line really looks like that, and you should reproduce it as-is. Three lines print tidily and one erupts in digits: that's lesson 4.5's rounding error making a personal appearance, since 78.4 has no exact binary spelling. Chapter 5's formatting tools will let you print it to one decimal like a civilized program.)
Show solution
fn ball_height(start: f64, seconds: f64) -> f64 {
let fallen = 9.8 * seconds * seconds / 2.0;
let height = start - fallen;
if height < 0.0 { 0.0 } else { height }
}
fn report(start: f64, seconds: f64) {
let height = ball_height(start, seconds);
if height > 0.0 {
println!("At {seconds} seconds, the ball is at height: {height} meters");
} else {
println!("At {seconds} seconds, the ball is on the ground.");
}
}
fn main() {
println!("Enter the height of the tower in meters:");
let start = read_number();
report(start, 0.0);
report(start, 1.0);
report(start, 2.0);
report(start, 3.0);
report(start, 4.0);
report(start, 5.0);
}
fn read_number() -> f64 {
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("failed to read input");
input.trim().parse().expect("that wasn't a number")
}
Design notes: ball_height computes and clamps (an if expression as the tail), report decides how to say it (compute and print kept separate per lesson 2.5, then introduced by a function that owns the phrasing). If your t = 4 line surprised you with its digit eruption, reread the question's parenthetical and feel the chapter close its own loop. And grammar pedants are invited to enjoy "At 1 seconds" until chapter 5's formatting tools offer a fix for that, too.
And those six nearly identical report lines in main? You're supposed to find them irritating. The tool that collapses them into two lines is the for loop, chapter 7, and you've just motivated it personally.
That's milestone territory: types, decisions, and real little programs. Chapter 5 starts the data story proper, with constants, shadowing, and the long-promised truth about strings.