Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close