2.6How to design your first programs
You can define functions, pass values in, and get values out. This lesson is about the step before the typing: deciding what functions a program should be made of. It's the least code-heavy lesson in the chapter and the one whose advice survives longest; the syntax you're learning this month will be muscle memory by spring, while "design a little before you build" stays load-bearing for your whole programming life.
Lesson 0.4 made the case that design is the habitually skipped step. Here's the doing of it, scaled to the programs you can write now.
Step 1: State the goal
One or two sentences, in user terms, not programmer terms. "A program that asks for two numbers and an operation, then shows the result." If you can't state it, you can't build it, and every fuzzy edge in the statement will resurface later as a bug with interest.
Step 2: List the requirements
The constraints and behaviors the solution must satisfy, still no how: which inputs? what outputs, exactly? what happens on bad input? (For now, honestly: "the program stops, rudely," courtesy of expect. Chapter 12 civilizes this, and a design list that says so is a design list you can upgrade.)
Step 3: Break the job into pieces
The technique is top-down decomposition: start with the whole task, split it into a few subtasks, and split any subtask that still feels big. The calculator:
- Get the first number
- Get the operation
- Get the second number
- Compute the result
- Show the result
Each line is function-sized: one job, clear inputs and outputs. Notice the list is the design, and notice something else: it's also nearly the body of main. That's the move this whole chapter has been arming you for. When the decomposition is right, main reads as a to-do list, and each item is a function call.
(Decomposition also works bottom-up, spotting small capabilities you'll obviously need and building toward the goal. Most people mix both. The destination is the same: a tree of small jobs.)
Step 4: Outline, then fill in, one function at a time
Here's where Rust gives the method mechanical support. Write main as the to-do list, and stub the functions with the todo!() macro:
fn main() {
let first = get_number();
let second = get_number();
let sum = compute_sum(first, second);
show_result(sum);
}
fn get_number() -> i32 {
todo!()
}
fn compute_sum(a: i32, b: i32) -> i32 {
todo!()
}
fn show_result(total: i32) {
todo!()
}What you're taking on credit
todo!() is a macro that means exactly what it says: this part isn't written yet. The program compiles with it in place (it satisfies any return type, by a type-system trick chapter 12's panic machinery will explain), and if execution actually reaches one, the program stops with a clear message:
thread 'main' (1125977) panicked at src/main.rs:9:5:
not yet implemented
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(The number in parentheses is a thread ID; yours will differ.)
It's the load-bearing version of a "under construction" sign, and real Rust codebases use it daily.
This skeleton is a complete, compiling program whose design is finished and whose parts are individually missing. (Compiling, with honest grumbling: the stubs don't use their parameters yet, so the compiler issues a few unused-variable warnings. Stubs never use their inputs; the warnings retire as the implementations arrive, a nice little progress meter.) Now implement one function at a time, and after each, run the program or poke the function with a temporary call to see it work. get_number first (it's lesson 2.2's, ready to paste), then compute_sum (one line), then show_result (one line), testing as you go. At every moment between steps you have a program that builds cleanly and fails only at honest, labeled todo!() boundaries. Compare that against writing all four parts blind and debugging the pile-up at the end.
Best practice
The first version of any program should do embarrassingly little, completely. The calculator's version one adds two numbers, the end. Subtraction, division, input validation: each is a later lap around a working track. Programs grown one working feature at a time stay working; programs assembled all at once get to experience chapter 3 early and often.
A few field notes to round out the method, all learned the expensive way by generations before you. Keep your focus on the one function you're implementing; the skeleton holds the rest. Don't polish prematurely (clear beats clever, and working beats both). And when you finish, take the five-minute cleanup pass from lesson 1.12: better names, dead code out, comments where the why isn't obvious. Design, build, tidy: that loop is the actual job, at every scale from these exercises to operating systems.
Quiz time
Question #1
What does todo!() do for a stubbed-out function, at compile time and at runtime?
Show solution
At compile time it satisfies the function's contract (any return type), so the whole skeleton builds. At runtime, reaching it stops the program with a "not yet implemented" message pointing at the file and line. Compiles now, refuses honestly later.
Question #2
Decompose this goal into function-sized pieces (names and signatures, no bodies): "ask the user for a temperature in Celsius, convert it to Fahrenheit, and display both."
Show solution
One reasonable design:
fn get_celsius() -> i32
fn to_fahrenheit(celsius: i32) -> i32
fn show_temperatures(celsius: i32, fahrenheit: i32)
with a main that calls them in order. Variations are fine (maybe show takes only what it prints); the marks are for one-job functions and a main that reads as the plan. (Real temperature math wants fractional types; chapter 4 brings them.)
That's the chapter's toolkit complete: mechanics, judgment, and method. The summary and the classic chapter quiz are next.