Skip to content

← Ground Rules: Values, Types, Control Flow step 12 of 24

Easy Primitives

Statements vs expressions, and the semicolon that changes the type

Turn a score into a letter grade.

pub fn grade(score: u32) -> char

90 and above is 'A', 80–89 is 'B', 70–79 is 'C', 60–69 is 'D', anything below is 'F'. Write it with tail expressions only — no return anywhere in the function.

The starter is correct and fails the gate twice over.

The deepest syntactic difference from C

In C, Java, Python and their descendants, most of the language is statements: things that happen. Rust is the other way round. Almost everything is an expression: a thing that has a value.

let x = if flag { 1 } else { 2 };            // if is an expression
let y = match n { 0 => "zero", _ => "some" }; // match is an expression
let z = loop { break 42; };                   // loop is an expression
let w = { let a = 1; a + 1 };                 // a block is an expression

Once “the last expression of a block is the value of that block” lands, all four of those stop being separate facts and become one fact seen from four angles. It is worth internalising here, in the smallest possible setting, because if, match and loop all show up in the next few items.

The semicolon changes the type

This is where it bites:

fn f() -> i32 { 5 }     // block value is 5      : i32
fn g() -> i32 { 5; }    // block value is ()     : E0308

A semicolon turns an expression into a statement, and a statement’s value is () — the empty tuple, pronounced “unit”, Rust’s “no meaningful value” type. So 5; computes 5 and throws it away, and the block ends up worth ().

The error you get is E0308: mismatched types — expected i32, found (), and rustc usually adds help: remove this semicolon to return this value. When you see found () in a type error, look for a stray semicolon before you look at anything else. This is one of the most common beginner failures in the language, and once you know the signature of the message it costs you five seconds instead of five minutes.

The two lints in the starter

needless_late_init. The starter declares let letter; and then assigns to it from every branch. That is the C idiom, and it is unnecessary here because if/else is an expression: bind the result directly.

let letter = if score >= 90 { 'A' } else { … };

The if version is better than the declare-then-assign version for a reason that goes beyond taste: the compiler can see that letter is initialised exactly once, on every path, and so can the reader.

needless_return. You met this in item 0.1. return x; as the last thing in a function is x.

Apply both and the function collapses into a single if/else if chain that is the return value. No binding, no return, no semicolon at the end.

A third default-on lint, let_and_return, catches the halfway house — binding a value to a name and then immediately returning that name. Worth knowing so you do not accidentally trade one lint for another.

char literals

'A' with single quotes is a char. "A" with double quotes is a &str of length one. They are different types and swapping them is E0308. The harness prints your char as a one-character JSON string.