Lesson 01 / 12
Rust's Design Goals
A measurement core of eighteen writings runs from inside the program and places each writing into one of three classes — expression, statement, item; the result is fixed at eight expressions, five statements, five items, and the rest of the course carries this trio forward as a single question: does what is written in the source produce a value?
Published Updated
In this lesson
- Defines the expression, statement and item classes by the value-producing criterion and distinguishes them from one another
- Verifies the class distribution of eighteen writings by running the measurement core
- Explains why two methods that do the same work fall into separate classes for a reason read from their signature
Contents
The Go curriculum closed with five courses, and its last course measured what can be known about a program itself: whether a piece of information comes from the source, from a tool, or from the run. This curriculum takes one more step in the same direction and looks directly at the source. When a line is written in Rust, that line’s fate splits three ways: the line is evaluated and produces a value, is evaluated but produces nothing, or is never evaluated at all, only declares a name. The one question asked throughout the course is this: does what you write in the source produce a value?
This question has been asked in two different forms before. The Java Fundamentals course measured what the compiler adds to the source — how something not written in the source gets completed during compilation. The Go Fundamentals course, by contrast, measured the counterpart of something not being written — whether the language leaves that defined, rejects it, or leaves it open. This course departs from both: here nothing is missing, and the matter is what is produced when what is written in the source gets evaluated.
This threefold classification should be read as a design goal, not as a list of three
categories to recite from memory: which of the three classes you write the same piece of work
in determines where that line can be used. You can bind an expression to a variable, pass it
to a function, or embed it inside another expression; a statement fits none of these, because
all you have in hand is (). The course’s later lessons will show, one by one, where this
boundary is useful — and where it turns into a writing mistake.
Expression, Statement, Item
There are three answers, and these three names do not change throughout the course.
| Class | Definition |
|---|---|
| expression | Evaluates and produces a value other than () |
| statement | Evaluates but produces () — or is never evaluated |
| item | Only declares; it is not itself evaluated, and its value arises when used |
The () that appears in the middle of this trio is not a gap — the Primitive Types lesson
takes it up separately, but what needs saying already is this: () is itself a type, and it
has exactly one value. A statement does not “produce nothing”; it produces the value of this
single-valued type. if, match, loop, and a block are each a value in Rust; a semicolon
discards a value; fn, struct, enum, mod, and const are never evaluated at all —
they only introduce a name to the compiler. Because these three paths exist, this question
can carry an entire course.
The Programming Fundamentals course built the if construct with Python; there, if was a
statement and produced no value. This concept is not rebuilt here — this one-sentence
reference exists only to show why the three classes form a separate axis in Rust: the same
construct does not fall into the same class in every language.
The Measurement Core
The core used throughout the rest of the course is expression.rs. It runs eighteen separate
writings inside the same frame, writes each one to an observation log, and prints the total
per class at the end. The observation log itself is an array of tuples — each row holds a
name, a class, and a value together as three separate pieces of text. A tuple is the most
direct way to carry several values of different types as a single unit; the fact that all
three fields here are text is not a requirement, but a choice made so the table can be printed
in one uniform format. The observation log is not part of the language — this should be kept
in mind while reading the code.
LB1. The observe function and the log tuple (a String, String, String triple) are
the core’s own design decision, not a mechanism of Rust. What decides which class a writing
falls into is each line’s form in the source; the observe call only records this decision
into a table so the eighteen lines can be compared within a single run.
// expression.rs — everything written in the source falls into one of three classes:
// one that produces a value (expression), one that produces () or is never
// evaluated (statement), one that only declares (item).
use std::collections::BTreeMap;
use std::collections::HashSet;
const LIMIT: i32 = 100;
struct Point {
x: i32,
y: i32,
}
enum Color {
Red,
Green,
}
mod measure {
pub fn unit() -> i32 {
3
}
}
fn double(x: i32) -> i32 {
x * 2
}
fn observe(log: &mut Vec<(String, String, String)>, name: &str, class: &str, value: String) {
log.push((name.to_string(), class.to_string(), value));
}
fn main() {
let mut log: Vec<(String, String, String)> = Vec::new();
// --- expression: writings that produce a value ---
let a = 7;
let b = if a > 5 { "big" } else { "small" };
observe(&mut log, "if as a value", "expression", b.to_string());
let color = if a > 5 { Color::Green } else { Color::Red };
let name = match color {
Color::Red => "red",
Color::Green => "green",
};
observe(&mut log, "match as a value", "expression", name.to_string());
let block = {
let inner = double(a);
inner + 1
};
observe(&mut log, "block as a value", "expression", block.to_string());
let mut counter = 0;
let loop_result = loop {
counter += 1;
if counter == 4 {
break counter * 10;
}
};
observe(&mut log, "loop returns a value via break", "expression", loop_result.to_string());
let n = Point { x: 1, y: 2 };
observe(&mut log, "struct syntax produces a value", "expression", format!("x={} y={}", n.x, n.y));
let v = vec![1, 2, 3, 4];
observe(&mut log, "the vec macro produces a value", "expression", format!("length={}", v.len()));
let total: i32 = v.iter().map(|x| double(*x)).sum();
observe(&mut log, "an iterator chain produces a value", "expression", total.to_string());
let mut set: HashSet<i32> = HashSet::new();
let first = set.insert(5);
let second = set.insert(5);
observe(&mut log, "set insertion returns a value", "expression", format!("{} then {}", first, second));
// --- statement: writings that produce () or are never evaluated ---
let with_semicolon = {
double(2);
};
observe(&mut log, "a block ending in a semicolon", "statement", format!("{:?}", with_semicolon));
let mut d = 1;
let assignment = { d = d + 8; };
observe(&mut log, "the assignment itself", "statement", format!("{:?} (d={})", assignment, d));
let mut list = Vec::new();
let push_result = list.push(d);
observe(&mut log, "pushing onto a vector", "statement", format!("{:?} (length={})", push_result, list.len()));
let mut iterations = 0;
let for_result = for _ in 0..3 {
iterations += 1;
};
observe(&mut log, "the for loop", "statement", format!("{:?} (count={})", for_result, iterations));
let print_result = print!("");
observe(&mut log, "the value of printing", "statement", format!("{:?}", print_result));
// --- item: writings that only declare; their values arise when used ---
observe(&mut log, "fn declaration", "item", format!("declaration; call gave {}", double(3)));
observe(&mut log, "struct declaration", "item", format!("declaration; instance gave x={}", n.x));
observe(&mut log, "enum declaration", "item", "declaration; variant gave green".to_string());
observe(&mut log, "mod declaration", "item", format!("declaration; call inside it gave {}", measure::unit()));
observe(&mut log, "const declaration", "item", format!("declaration; reading it gave {}", LIMIT));
// --- report ---
println!("{:<40}{:<12}{}", "observation", "class", "value");
for (name, class, value) in &log {
println!("{:<40}{:<12}{}", name, class, value);
}
let mut tally: BTreeMap<&str, i32> = BTreeMap::new();
for (_, class, _) in &log {
*tally.entry(class.as_str()).or_insert(0) += 1;
}
println!();
for (class, count) in &tally {
println!("{:<12}{}", class, count);
}
println!("total {}", log.len());
}
observation class value if as a value expression big match as a value expression green block as a value expression 15 loop returns a value via break expression 40 struct syntax produces a value expression x=1 y=2 the vec macro produces a value expression length=4 an iterator chain produces a value expression 20 set insertion returns a value expression true then false a block ending in a semicolon statement () the assignment itself statement () (d=9) pushing onto a vector statement () (length=1) the for loop statement () (count=3) the value of printing statement () fn declaration item declaration; call gave 6 struct declaration item declaration; instance gave x=1 enum declaration item declaration; variant gave green mod declaration item declaration; call inside it gave 3 const declaration item declaration; reading it gave 100 expression 8 item 5 statement 5 total 18
LB2. The run is deterministic: the same source produces the same eighteen lines, in the same order, with the same class distribution, on every run. No line reads from a file system, a clock, or a random generator — everything is computed from a fixed source. Eight of the eighteen writings are expressions, five are statements, and five are items; this count does not change for the rest of the course — only each lesson takes its own slice of it.
The final block, which computes the total per class, uses an ordered map (BTreeMap), not a
hash map (HashMap). This choice is not arbitrary: the order in which a hash map’s keys are
traversed can vary from run to run, so even if the total for all three classes comes out
correct, the order in which the rows print is not guaranteed. An ordered map’s keys always
traverse in the same order — here, alphabetically, expression, item, statement — so the
printed text also stays deterministic. This is the most concrete example of the course’s rule
about environment-dependent data: if a collection’s order is going to be used as a
presentation device, that order must come from a structure that carries a guarantee. This
distinction will be its own subject in the Ordered and Set Structures lesson; here it only
explains why the core’s own output can be safely compared run after run.
Reading the Results
What the eight expression lines have in common is that all of them stand on the right side of
a let binding and produce a value: if/match picks a branch and gives that branch’s
value, a block gives its own last line, loop gives only the value handed to break, struct
syntax and the vec macro construct a new value, an iterator chain accumulates a total, and
set insertion returns a bool. The five statement lines show the exact opposite: a block
closed with a semicolon, an assignment, Vec::push, a for loop, and an empty print — all
five print (), because all five say “do a piece of work,” not “give a value.” The five item
lines stand on a separate axis: the fn, struct, enum, mod, and const declarations
themselves never enter the table at all; what enters the table is the value that arises from
using these declarations — the 6 that double(3) returns, a field of the Point
instance, the call to measure::unit(), the reading of LIMIT. A declaration produces
nothing the moment it is written; it only introduces a name, and that name produces a value
once it is used in a later line.
LB3. The double item is used in three separate places in the core: once inside block
(to produce the value that feeds the inner + 1 computation), once in the iterator chain to
apply to each element, and once in the “fn declaration” line, called directly so its result is
written to the table. In all three, double itself does not change — what changes is the
argument given to each call and the value that call returns. This makes concrete what it means
for an item to be “valueless”: the declaration is single, but the number of uses and the value
each use produces are free.
The count is not equal among these three groups, and it is not expected to be: most of the
source is written to produce a value (8), and the rest is written either for a side effect (5)
or to introduce a name (5). The rest of the course will deepen each of these three groups in
turn — if, match, loop, and the full set of loops in the Control Flow lesson, assignment
and binding in the Variables and Immutability lesson, and the share declarations hold in the
source in the Setup and the Package Manager lesson. The only question this lesson leaves open
is where the class is read from.
Limiting Measurement: Class Is Read From the Signature
Two rows that sit side by side in the table carry the answer to this question: “set insertion
returns a value” (expression, true then false) and “pushing onto a vector” (statement,
() (length=1)). The two look like they do the same work — adding an element to a collection.
But one produced a value during the run, the other did not. The difference lies not in the
nature of the work, but in the signature of the method being called.
HashSet::insert both changes the set and returns a boolean saying whether the insertion
actually added a new element. In the core, 5 was inserted twice: the first call returns
true because it adds a new element to the set, the second call returns false because it
tries to insert the already-present 5 again — the true then false in the table is these
two calls placed side by side. Vec::push never gives this kind of information: because
adding an element to a vector always succeeds, there is no boolean to return, and the
signature gives only ().
This shows that a single piece of work called “insertion” gives two separate Rust methods two
separate classes. What determines the class is not the reader’s intuition about what the work
is, but the return type the compiler sees: statement if the return type is (), expression if
it is any other type. Because this criterion is formal and mechanical, which class a method
falls into can be told in advance without looking at its call, just its signature — a mutator
that returns Option or bool is always an expression, a mutator that returns only () is
always a statement. This distinction will come up once more in the Collections lesson, with
Vec::pop returning Option.
This also explains why the course stands under the heading of a design goal: class is not a
runtime observation but a signature decision, and a signature can be read from the source
without running anything. A reader can look at HashSet::insert‘s return type and say, without
running a single instance, that it is an expression. The standard library’s own design
determines this classification in advance; what the lesson measures throughout is what
difference this determination makes when reading the source.
Summary
- Everything written in the source falls into one of three classes: an expression produces a
value, a statement produces
()or is never evaluated, and an item only declares a name whose value arises from use. ()is not a gap; it is a single-valued type, and it is what a statement produces.- The measurement core classifies eighteen writings in a single run: 8 expressions, 5 statements, 5 items — this count does not change for the rest of the course.
- The same piece of work (insertion) can give two separate methods two separate classes; class is read not from the nature of the work but from the method’s return type — its signature.
Next Step
The three classes are now defined, but for a source to actually run, there is a much smaller, much more mandatory set than these: the least a program must write. The next lesson asks about this smallest set — it shows how, when a piece kept in a separate file that does not live in the source itself changes, the output changes while the source itself never does.
To keep your progress and take notes, Log in
My notes
Log in to take notes.