Skip to content
Learn Rust
Theme

Statements and expressions

Syntax · Lesson 2

In Rust the pieces that steer a program, if, match, loops and { } blocks, are expressions: they produce a value. The last line of a block, with no ;, is the block's value, and a function's last line is what it returns.

Coming from Goif and switch are statements in Go, so you declare label := "odd" first and assign inside a branch. In Rust if and match give a value, and a function ends with its result, written without return (return x; still works). a = b = c and if x = 1 fail in both languages, for different reasons: Go's assignment is a statement, Rust's is an expression worth ().

Coming from KotlinYou already have if, when and a lambda's last expression as values, and Rust works the same way. The new parts: a Rust function body in braces also returns its last expression (Kotlin needs return, or the = expr form, which Rust does not have), a bare { } is a block, not a lambda, and assignment is an expression worth () where in Kotlin it is a statement. a = b = c fails in both.

Coming from PythonPython ends a statement at the newline; Rust ends it at ;. A bare x + 1 line in Python is worked out and thrown away, and so is x + 1; in Rust. A line with no ; at the end of a block is the block's value. A Rust function declares its result type, so a body that ends x + 1; instead of x + 1 is a compile error, not a quiet None.

Coming from TypeScriptc ? a : b becomes if c { a } else { b }, and a switch statement becomes a match that gives a value. An arrow function with braces needs return; a Rust function or closure in braces returns its last expression, like x => x + 1 does without braces. a = b = c and if (x = 1) compile in TypeScript; in Rust they are type errors.

Coming from JavaRust has no ?:; if is an expression, so it is not needed. match is like the Java 14+ switch expression. A method returns its last expression with no return. Java lets assignment sit inside an expression (a = b = c, if (flag = true)); Rust does not.

Coming from C#?: becomes if c { a } else { b }, and n switch { ... } becomes match n { ... }. Rust has no => expression body: a function uses braces and returns its last expression. C# lets assignment sit inside an expression (a = b = c); Rust does not.

Coming from CRust has no ?: and no comma operator: if is an expression, and { f(); 2 } is a block that gives 2. if (x = 1) compiles in C (gcc warns only with -Wall); in Rust the condition must be a bool and an assignment is worth (), so it is an error. A non-void C function that runs off its end is a problem only if the caller uses the result; in Rust it is a compile error.

Coming from C++Rust has no ?: and no comma operator: if is an expression, and { f(); 2 } is a block that gives 2 (standard C++'s closest is an immediately called lambda). if (x = 1) compiles in C++ (gcc warns only with -Wall); in Rust it is an error. Running off the end of a function that returns a value is undefined behaviour in C++ even if the caller ignores the result; in Rust it is a compile error.

The quick comparison

Rustsrc/main.rsRun ↗
fn clamp(x: i32) -> i32 {
if x < 0 {
return 0; // leaving early needs `return`
}
if x > 100 { 100 } else { x } // the last expression is the result
}
fn main() {
let n = 7;
let label = if n % 2 == 0 { "even" } else { "odd" };
let area = {
let w = 3;
let h = 4;
w * h // no semicolon: the block's value
};
println!("{n} is {label}");
println!("area: {area}");
println!("clamp: {} {} {}", clamp(-5), clamp(50), clamp(250));
}
$ cargo run
7 is odd
area: 12
clamp: 0 50 100
package main
import "fmt"
func clamp(x int) int {
if x < 0 {
return 0
}
if x > 100 {
return 100
}
return x
}
func main() {
n := 7
label := "odd"
if n%2 == 0 {
label = "even"
}
w := 3
h := 4
area := w * h
fmt.Println(n, "is", label)
fmt.Println("area:", area)
fmt.Println("clamp:", clamp(-5), clamp(50), clamp(250))
}
$ go run main.go
7 is odd
area: 12
clamp: 0 50 100
fun clamp(x: Int): Int {
if (x < 0) {
return 0
}
return if (x > 100) 100 else x
}
fun main() {
val n = 7
val label = if (n % 2 == 0) "even" else "odd"
val area = run {
val w = 3
val h = 4
w * h
}
println("$n is $label")
println("area: $area")
println("clamp: ${clamp(-5)} ${clamp(50)} ${clamp(250)}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
7 is odd
area: 12
clamp: 0 50 100
def clamp(x):
if x < 0:
return 0
return 100 if x > 100 else x
n = 7
label = "even" if n % 2 == 0 else "odd"
w = 3
h = 4
area = w * h
print(f"{n} is {label}")
print(f"area: {area}")
print("clamp:", clamp(-5), clamp(50), clamp(250))
$ python3 main.py
7 is odd
area: 12
clamp: 0 50 100
function clamp(x: number): number {
if (x < 0) {
return 0;
}
return x > 100 ? 100 : x;
}
const n = 7;
const label = n % 2 === 0 ? "even" : "odd";
const area = (() => {
const w = 3;
const h = 4;
return w * h;
})();
console.log(`${n} is ${label}`);
console.log(`area: ${area}`);
console.log("clamp:", clamp(-5), clamp(50), clamp(250));
$ tsc --strict --module nodenext --types node main.ts && node main.js
7 is odd
area: 12
clamp: 0 50 100
public class Main {
static int clamp(int x) {
if (x < 0) {
return 0;
}
return x > 100 ? 100 : x;
}
public static void main(String[] args) {
int n = 7;
String label = n % 2 == 0 ? "even" : "odd";
int w = 3;
int h = 4;
int area = w * h;
System.out.println(n + " is " + label);
System.out.println("area: " + area);
System.out.println("clamp: " + clamp(-5) + " " + clamp(50) + " " + clamp(250));
}
}
$ javac Main.java && java Main
7 is odd
area: 12
clamp: 0 50 100
static int Clamp(int x)
{
if (x < 0)
{
return 0;
}
return x > 100 ? 100 : x;
}
int n = 7;
string label = n % 2 == 0 ? "even" : "odd";
int w = 3;
int h = 4;
int area = w * h;
Console.WriteLine($"{n} is {label}");
Console.WriteLine($"area: {area}");
Console.WriteLine($"clamp: {Clamp(-5)} {Clamp(50)} {Clamp(250)}");
$ dotnet run
7 is odd
area: 12
clamp: 0 50 100
#include <stdio.h>
int clamp(int x) {
if (x < 0) {
return 0;
}
return x > 100 ? 100 : x;
}
int main(void) {
int n = 7;
const char *label = n % 2 == 0 ? "even" : "odd";
int w = 3;
int h = 4;
int area = w * h;
printf("%d is %s\n", n, label);
printf("area: %d\n", area);
printf("clamp: %d %d %d\n", clamp(-5), clamp(50), clamp(250));
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
7 is odd
area: 12
clamp: 0 50 100
#include <iostream>
int clamp(int x) {
if (x < 0) {
return 0;
}
return x > 100 ? 100 : x;
}
int main() {
int n = 7;
const char *label = n % 2 == 0 ? "even" : "odd";
int area = [] {
int w = 3;
int h = 4;
return w * h;
}();
std::cout << n << " is " << label << "\n";
std::cout << "area: " << area << "\n";
std::cout << "clamp: " << clamp(-5) << " " << clamp(50) << " " << clamp(250) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
7 is odd
area: 12
clamp: 0 50 100

What's different

  • The last expression in a block, with no ;, is the block's value. It is called the tail expression. Put a ; after it and the value is thrown away: the block is then worth (), Rust's "nothing" value.
  • if, match, loop and { } can all sit on the right of let or be the last line of a function. Every branch must agree on a type (a branch that leaves, like continue, fits any type).
  • return is for leaving early. At the end of a function, write the value without return and without ;.
  • Assignment (x = 5) is an expression, but it is worth (), so a = b = c does not chain and if x = 1 is a type error. Conditions must be bool and need no parentheses.
Gotcha

A semicolon on the last line

Looks fine: in Python it runs fine; in C it runs but gives a wrong answer; in C++ it crashes at runtime. Rust won't compile it.

Go refuses this too. Same idea, different words.Kotlin refuses this too. Same idea, different words.Works in Python. In Rust it's just a rule.TypeScript refuses this too. Same idea, different words.Java refuses this too. Same idea, different words.C# refuses this too. Same idea, different words.Broken in C. Rust catches it.Broken in C++. Rust catches it.
Rustsrc/main.rs✗ won't compile
fn add_one(x: i32) -> i32 {
x + 1;
}
fn main() {
println!("{}", add_one(41));
}
Go✗ refuses too

Go refuses twice: x + 1 is "not used", and there is a "missing return". The fix is return x + 1. Go has no last-expression rule.

package main
import "fmt"
func addOne(x int) int {
x + 1
}
func main() {
fmt.Println(addOne(41))
}
# command-line-arguments
./main.go:6:2: x + 1 (value of type int) is not used
./main.go:7:1: missing return
Kotlin✗ refuses too

Kotlin refuses with "missing return statement". A block body needs return x + 1; the other fix is the expression form fun addOne(x: Int) = x + 1.

fun addOne(x: Int): Int {
x + 1
}
fun main() {
println(addOne(41))
}
Main.kt:3:1: error: missing return statement.
}
^
def add_one(x):
x + 1
print(add_one(41))
$ python3 main.py✓ runs
None
TypeScript✗ refuses too

TypeScript refuses (TS2355): a function declared to return number must return a value. The fix is return x + 1;.

function addOne(x: number): number {
x + 1;
}
console.log(addOne(41));
main.ts(1,29): error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
Java✗ refuses too

Java refuses: x + 1; is "not a statement", because Java only allows calls, assignments and a few others as statements. The fix is return x + 1;.

public class Main {
static int addOne(int x) {
x + 1;
}
public static void main(String[] args) {
System.out.println(addOne(41));
}
}
Main.java:3: error: not a statement
        x + 1;
          ^
1 error
C#✗ refuses too

C# refuses twice (CS0201 and CS0161): x + 1; cannot be a statement, and not all code paths return a value. The fix is return x + 1;.

static int AddOne(int x)
{
x + 1;
}
Console.WriteLine(AddOne(41));
Program.cs(3,5): error CS0201: Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement
Program.cs(1,12): error CS0161: 'AddOne(int)': not all code paths return a value
#include <stdio.h>
int add_one(int x) {
x + 1;
}
int main(void) {
printf("%d\n", add_one(41));
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main! runs · wrong answer
main.c: In function ‘add_one’:
main.c:4:7: warning: statement with no effect [-Wunused-value]
    4 |     x + 1;
      |     ~~^~~
main.c:5:1: warning: control reaches end of non-void function [-Wreturn-type]
    5 | }
      | ^
41
#include <iostream>
int add_one(int x) {
x + 1;
}
int main() {
std::cout << add_one(41) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main! crashes at runtime
main.cpp: In function ‘int add_one(int)’:
main.cpp:4:7: warning: statement has no effect [-Wunused-value]
    4 |     x + 1;
      |     ~~^~~
main.cpp:5:1: warning: no return statement in function returning non-void [-Wreturn-type]
    5 | }
      | ^
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:1:23
  |
1 | fn add_one(x: i32) -> i32 {
  |    -------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     x + 1;
  |          - help: remove this semicolon to return this value
What happens in Go

Go refuses twice: x + 1 is "not used", and there is a "missing return". The fix is return x + 1. Go has no last-expression rule.

What happens in Kotlin

Kotlin refuses with "missing return statement". A block body needs return x + 1; the other fix is the expression form fun addOne(x: Int) = x + 1.

What happens in Python

Python runs this and prints None. The body computes x + 1 and discards it, and a function with no return gives back None. Nothing complains.

What happens in TypeScript

TypeScript refuses (TS2355): a function declared to return number must return a value. The fix is return x + 1;.

What happens in Java

Java refuses: x + 1; is "not a statement", because Java only allows calls, assignments and a few others as statements. The fix is return x + 1;.

What happens in C#

C# refuses twice (CS0201 and CS0161): x + 1; cannot be a statement, and not all code paths return a value. The fix is return x + 1;.

What happens in C

gcc compiles this. With the runner's -Wall -Wextra it warns twice (plain gcc says nothing) and the program prints 41. x + 1; is legal C. The problem is the missing return: using the result of a call that ran off its end is undefined behaviour. On this runner (ARM64, no -O) the argument 41 is still in the register the caller reads the result from. With -O1 or higher the same program printed 0. If the caller ignores the result, C runs fine.

What happens in C++

g++ compiles this with warnings, then the program crashes before printing. In C++, reaching the end of a function that returns a value is undefined behaviour even if the caller ignores the result. Here g++ put a trap instruction at the missing return (SIGTRAP, exit status 133; with -O2 the same program segfaults instead).

What Rust sees

x + 1; is a statement: the sum is worked out and thrown away. The block then has no final expression, so its value is (), and the function promised an i32.

Why Rust says no

One character changes what a line is. With the ; it is a statement. Without it, it is the block's value. Rust cannot guess which you meant, so it points at the ; and says to remove it. Look at the other languages' results: most refuse, Python quietly returns None, and C and C++ compile with warnings and give an undefined result.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
println!("{}", add_one(41));
}
$ cargo run
42

The toolkit

Statement or expression?

An expression is code that produces a value: 7, x + 1, f(x). A statement is an instruction that does something and produces no value, such as let x = 7;. Many languages keep the two apart. In Rust, if, match, loop and { } blocks are expressions too, so they can sit on the right of a let or be the last line of a function. The Reference: “Rust is primarily an expression language.”

The last expression of a block, with no ;, is the block’s value. It is called the tail expression, a name you will see in compiler errors.

Code What it is Its value Learn more
x + 1, f(x) expression a number, a string, and so on Types
{ let w = 3; w * 4 } block expression its tail expression below
if c { a } else { b } expression a or b, which must agree on a type If / else
match n { 1 => "one", _ => "many" } expression the chosen arm (_ means “anything else”) Match
loop { ... break v; } expression the v after break Loops
while c { }, for x in a..b { } expression () (a..b runs from a up to, not including, b) Loops
x = 5, x += 1 expression () Variables
return v, break, continue, panic!("...") expression none: they leave, so their type is ! (“never”, not the macro !) the never type
let x = 5; statement none Variables
expr; statement the value is dropped next section

Four of these side by side. Each let takes the value of a block, an if, a match and a loop. ({block} in a string prints the variable. let mut makes a variable that can change. The ! in println! marks a macro: see Rust at a glance.)

Rustsrc/main.rsRun ↗
fn main() {
let n = 7;
let block = {
let doubled = n * 2;
doubled + 1
};
let branch = if n > 5 { "big" } else { "small" };
let name = match n {
1 => "one",
2 => "two",
_ => "many",
};
let mut count = 0;
let found = loop {
count += 3;
if count % 7 == 0 {
break count;
}
};
println!("block: {block}");
println!("branch: {branch}");
println!("match: {name}");
println!("loop: {found}");
}
$ cargo run
block: 15
branch: big
match: many
loop: 21

The semicolon decides

A ; turns an expression into a statement and drops its value. A block whose last line has no ; is worth that last line. A block that ends in a ; is worth (). ({a:?} prints a value in its debug form.)

Rustsrc/main.rsRun ↗
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
let a = { double(4) };
let b = {
double(4);
};
println!("a: {a:?}");
println!("b: {b:?}");
}
$ cargo run
a: 8
b: ()

That is the rule behind “no return at the end of a function”: the body is a block, and its tail expression is the result. return is for leaving early. return x; as the last line is legal, but clippy, Rust’s standard linter, warns about it by default (needless_return).

The gotcha above is this rule in a function. Its code, E0308, means “mismatched types” (all codes). It appears the other way round too: an if with no else must be worth (), so a value inside it needs a ;. main returns (), so this if is its last expression:

Rustsrc/main.rs✗ won't compile
fn five() -> i32 {
5
}
fn main() {
if true {
five()
}
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:7:9
  |
6 | /     if true {
7 | |         five()
  | |         ^^^^^^- help: consider using a semicolon here: `;`
  | |         |
  | |         expected `()`, found `i32`
8 | |     }
  | |_____- `if` expressions without `else` arms expect their inner expression to be `()`

(), the value for “nothing”

() is called unit: a type with exactly one value, also written () (the empty tuple). A function with no -> returns it, and so do println!, assignment, while and for. size_of::<()>() is the size in bytes of the type (). (_ means “I don’t need the value”.)

Rustsrc/main.rsRun ↗
use std::mem::size_of;
fn log(msg: &str) {
println!("log: {msg}");
}
fn main() {
let mut x = 1;
println!("x starts as {x}");
let from_fn = log("hello");
let from_macro = println!("printing");
let from_assign = x = 5;
let from_add = x += 1;
let from_while = while x < 10 {
x += 1;
};
let from_for = for _ in 0..2 {};
println!("fn: {from_fn:?}");
println!("println!: {from_macro:?}");
println!("assignment: {from_assign:?}");
println!("+=: {from_add:?}");
println!("while: {from_while:?}");
println!("for: {from_for:?}");
println!("size_of::<()>(): {}", size_of::<()>());
}
$ cargo run
x starts as 1
log: hello
printing
fn: ()
println!: ()
assignment: ()
+=: ()
while: ()
for: ()
size_of::<()>(): 0

It is not a null: a variable of another type cannot hold it. It is a real value you can store, which a void result is not in C, C++, Java, C# or Go (an error in each). Python’s None, Kotlin’s Unit and TypeScript’s undefined are the closest relatives.

Your habits, in Rust

Your own table. Rows that claim a result were run in both languages.

Assignment is worth ()

In C, Java, C#, C++ and TypeScript, a = b = 5 works because b = 5 is worth 5. In Rust b = 5 is worth (), so the chain is a type error:

Rustsrc/main.rs✗ won't compile
fn main() {
let mut a: i32 = 0;
let mut b: i32 = 0;
a = b = 5;
println!("{a} {b}");
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:4:9
  |
2 |     let mut a: i32 = 0;
  |                --- expected due to this type
3 |     let mut b: i32 = 0;
4 |     a = b = 5;
  |         ^^^^^ expected `i32`, found `()`

The same fact catches the typo if x = 1: the condition must be a bool, and an assignment is ().

Rustsrc/main.rs✗ won't compile
fn main() {
let mut x = 0;
if x = 1 {
println!("x is one");
}
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:3:8
  |
3 |     if x = 1 {
  |        ^^^^^ expected `bool`, found `()`
  |
help: you might have meant to compare for equality
  |
3 |     if x == 1 {
  |           +

let is a statement, so it cannot be used as a value. (It may appear inside the condition of if and while: that is if let, see Option.)

Rustsrc/main.rs✗ won't compile
fn main() {
let x = let y = 6;
println!("{x} {y}");
}
$ cargo build
error: expected expression, found `let` statement
 --> src/main.rs:2:13
  |
2 |     let x = let y = 6;
  |             ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

What works: (a, b) = (b, a); swaps two variables, and x += 1 adds one.

Smaller slips

Each was compiled; the messages are the compiler’s.

You write The compiler says Fix
if n { } with a number E0308 “expected bool, found integer” if n != 0 { }
if (n > 5) { } warning “unnecessary parentheses around if condition” (an error under deny(unused_parens)) if n > 5 { }
if n > 5 println!("big"); “expected {”: braces are required if n > 5 { ... }
struct S { }; “expected item, found ;” struct S { }
let x = if c { 1 }; E0317 “if may be missing an else clause” add else
x++ “Rust has no postfix increment operator” x += 1;

Going deeper

Why the language is built this way

The Reference: “statements serve mostly to contain and explicitly sequence expression evaluation”. The Book: statements perform an action and return no value; expressions evaluate to one. Two things follow. With if giving a value, Rust has no need for ?:. And a function’s result needs no separate mechanism: the body is a block, and a block has a value. The cost is the gotcha’s trap: in C or Java the final ; is punctuation, and in Rust it changes the type.

A } can end a statement

Where a statement is expected, a block-like expression (if, match, loop, { }) ends at its closing brace. When that is ambiguous, the Reference says, “it is parsed as a statement”. So a last line of if ... { } else { } + 1 does not mean what it looks like. Read the first message; the return suggestions after it are side effects:

Rustsrc/main.rs✗ won't compile
fn total(n: i32) -> i32 {
if n > 0 { 10 } else { 20 } + 1
}
fn main() {
println!("{}", total(5));
}
$ cargo build
error: leading `+` is not supported
 --> src/main.rs:2:33
  |
2 |     if n > 0 { 10 } else { 20 } + 1
  |                                 ^ unexpected `+`
  |
help: parentheses are required to parse this as an expression
  |
2 |     (if n > 0 { 10 } else { 20 }) + 1
  |     +                           +

error[E0308]: mismatched types
 --> src/main.rs:2:16
  |
2 |     if n > 0 { 10 } else { 20 } + 1
  |     -----------^^--------------
  |     |          |
  |     |          expected `()`, found integer
  |     expected this to be `()`
  |
help: you might have meant to return this value
  |
2 |     if n > 0 { return 10; } else { 20 } + 1
  |                ++++++   +

error[E0308]: mismatched types
 --> src/main.rs:2:28
  |
2 |     if n > 0 { 10 } else { 20 } + 1
  |     -----------------------^^--
  |     |                      |
  |     |                      expected `()`, found integer
  |     expected this to be `()`
  |
help: you might have meant to return this value
  |
2 |     if n > 0 { 10 } else { return 20; } + 1
  |                            ++++++   +

After let t = there is no ambiguity, so let t = if n > 0 { 10 } else { 20 } + 1; works (it gives 11). Elsewhere, wrap the if in parentheses, as the compiler suggests, or name it first.

if without else, and loops that give a value

An if with no else has nothing to give when the condition is false, so its value is (). That is why it cannot be used for a value:

Rustsrc/main.rs✗ won't compile
fn main() {
let n = 7;
let size = if n > 5 {
"big"
};
println!("{size}");
}
$ cargo buildWhat is E0317?
error[E0317]: `if` may be missing an `else` clause
 --> src/main.rs:3:16
  |
3 |       let size = if n > 5 {
  |  ________________^
4 | |         "big"
  | |         ----- found here
5 | |     };
  | |_____^ expected `&str`, found `()`
  |
  = note: `if` expressions without `else` evaluate to `()`
  = help: consider adding an `else` block that evaluates to the expected type

Of the three loops, only loop can hand back a value with break value. The Reference says a while expression evaluates to (), and the compiler allows break with a value only in a loop (or a labeled block, which this page does not cover):

Rustsrc/main.rs✗ won't compile
fn main() {
let mut i = 0;
let found = while i < 10 {
i += 1;
if i == 4 {
break i;
}
};
println!("{found:?}");
}
$ cargo buildWhat is E0571?
error[E0571]: `break` with value from a `while` loop
 --> src/main.rs:6:13
  |
3 |     let found = while i < 10 {
  |                 ------------ you can't `break` with a value in a `while` loop
...
6 |             break i;
  |             ^^^^^^^ can only break with a value inside `loop` or breakable block
  |
help: use `break` on its own without a value inside this `while` loop
  |
6 -             break i;
6 +             break;

When nothing comes back: the never type

return, break, continue and panic! leave the current spot and never produce a value. Their type is !, read “never” (a different ! from the macro mark). Because they cannot produce anything, the compiler accepts them where any type is expected, and that is what makes these lines work:

Rustsrc/main.rsRun ↗
fn main() {
let mut sum = 0;
for i in 0..6 {
let even = if i % 2 == 0 { i } else { continue };
sum += even;
}
println!("sum of evens: {sum}");
let ok = true;
let x: i32 = if ok { 5 } else { panic!("not ok") };
println!("x: {x}");
}
$ cargo run
sum of evens: 6
x: 5

In let even = if i % 2 == 0 { i } else { continue }; the first branch gives an i32 and the else branch never gives anything, so the whole if is an i32.

Where this is awkward

  • The ; decides the type, and a last line without one is easy to miss when reading.
  • A function body is always a block, so Kotlin’s = x + 1 and C#’s => x + 1 have no counterpart.

Practice

Try it

Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed. The first one trains reading: it shows a program and asks what its blocks are worth.

Read this: what is each block worth?

Work out the value of three blocks and an if, for two inputs. One block ends in a ;, and one if takes a different branch for each input.

Starterexercises/read-blocks.rs
// Read this program without running it. Then replace each `todo!()` below with the line
// that `show` makes for that variable, written as a string, in the form "x = 1".
fn double(x: i32) -> i32 {
x * 2
}
fn show(n: i32) -> Vec<String> {
let a = { double(n) };
let b = {
double(n);
};
let c = if n > 5 { n } else { n * 10 };
let d = {
let m = double(n);
m * 10 + 1
};
vec![
format!("a = {a:?}"),
format!("b = {b:?}"),
format!("c = {c:?}"),
format!("d = {d:?}"),
]
}
// What `show` makes:
/// Hint: the last line of the block has no `;`, so it is the block's value
fn a_when_n_is_3() -> &'static str {
todo!("the last line of the block has no `;`, so it is the block's value")
}
/// Hint: look at the end of the last line in the block: what does a `;` make of the value?
fn b_when_n_is_3() -> &'static str {
todo!("look at the end of the last line in the block: what does a `;` make of the value?")
}
/// Hint: which branch of the `if` runs when n is 3?
fn c_when_n_is_3() -> &'static str {
todo!("which branch of the `if` runs when n is 3?")
}
/// Hint: same `if`, the other branch: 7 is not below 5
fn c_when_n_is_7() -> &'static str {
todo!("same `if`, the other branch: 7 is not below 5")
}
/// Hint: a block can hold a `let` first; its value is still the last line
fn d_when_n_is_3() -> &'static str {
todo!("a block can hold a `let` first; its value is still the last line")
}
fn check(what: &str, said: &str, truth: &str) {
assert!(said == truth, "misread `{what}`: you said {said:?}");
println!("ok: {what} is {said:?}");
}
fn main() {
let at3 = show(3);
let at7 = show(7);
check("a with n = 3", a_when_n_is_3(), &at3[0]);
check("b with n = 3", b_when_n_is_3(), &at3[1]);
check("c with n = 3", c_when_n_is_3(), &at3[2]);
check("c with n = 7", c_when_n_is_7(), &at7[2]);
check("d with n = 3", d_when_n_is_3(), &at3[3]);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/read-blocks.solution.rsRun ↗
// Read this program without running it. Then replace each `todo!()` below with the line
// that `show` makes for that variable, written as a string, in the form "x = 1".
fn double(x: i32) -> i32 {
x * 2
}
fn show(n: i32) -> Vec<String> {
let a = { double(n) };
let b = {
double(n);
};
let c = if n > 5 { n } else { n * 10 };
let d = {
let m = double(n);
m * 10 + 1
};
vec![
format!("a = {a:?}"),
format!("b = {b:?}"),
format!("c = {c:?}"),
format!("d = {d:?}"),
]
}
// What `show` makes:
/// Hint: the last line of the block has no `;`, so it is the block's value
fn a_when_n_is_3() -> &'static str {
"a = 6"
}
/// Hint: look at the end of the last line in the block: what does a `;` make of the value?
fn b_when_n_is_3() -> &'static str {
"b = ()"
}
/// Hint: which branch of the `if` runs when n is 3?
fn c_when_n_is_3() -> &'static str {
"c = 30"
}
/// Hint: same `if`, the other branch: 7 is not below 5
fn c_when_n_is_7() -> &'static str {
"c = 7"
}
/// Hint: a block can hold a `let` first; its value is still the last line
fn d_when_n_is_3() -> &'static str {
"d = 61"
}
fn check(what: &str, said: &str, truth: &str) {
assert!(said == truth, "misread `{what}`: you said {said:?}");
println!("ok: {what} is {said:?}");
}
fn main() {
let at3 = show(3);
let at7 = show(7);
check("a with n = 3", a_when_n_is_3(), &at3[0]);
check("b with n = 3", b_when_n_is_3(), &at3[1]);
check("c with n = 3", c_when_n_is_3(), &at3[2]);
check("c with n = 7", c_when_n_is_7(), &at7[2]);
check("d with n = 3", d_when_n_is_3(), &at3[3]);
println!("all checks passed");
}
$ cargo run
ok: a with n = 3 is "a = 6"
ok: b with n = 3 is "b = ()"
ok: c with n = 3 is "c = 30"
ok: c with n = 7 is "c = 7"
ok: d with n = 3 is "d = 61"
all checks passed

Shipping price

Turn price rules into one if / else if / else expression bound with let, then double it for express delivery. The boundary weights (1 and 5) are what people get wrong.

Starterexercises/shipping.rs
/// Shipping in cents. Up to 1 kg costs 500, over 1 kg and up to 5 kg costs 900,
/// over 5 kg costs 1500. Express delivery doubles the price.
fn shipping_cents(weight_kg: u32, express: bool) -> u32 {
todo!("an if / else if / else expression gives the base price; then double it if express")
}
fn main() {
assert_eq!(shipping_cents(0, false), 500);
assert_eq!(shipping_cents(1, false), 500);
assert_eq!(shipping_cents(2, false), 900);
assert_eq!(shipping_cents(5, false), 900);
assert_eq!(shipping_cents(6, false), 1500);
assert_eq!(shipping_cents(5, true), 1800);
assert_eq!(shipping_cents(1, true), 1000);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/shipping.solution.rsRun ↗
/// Shipping in cents. Up to 1 kg costs 500, over 1 kg and up to 5 kg costs 900,
/// over 5 kg costs 1500. Express delivery doubles the price.
fn shipping_cents(weight_kg: u32, express: bool) -> u32 {
let base = if weight_kg <= 1 {
500
} else if weight_kg <= 5 {
900
} else {
1500
};
if express { base * 2 } else { base }
}
fn main() {
assert_eq!(shipping_cents(0, false), 500);
assert_eq!(shipping_cents(1, false), 500);
assert_eq!(shipping_cents(2, false), 900);
assert_eq!(shipping_cents(5, false), 900);
assert_eq!(shipping_cents(6, false), 1500);
assert_eq!(shipping_cents(5, true), 1800);
assert_eq!(shipping_cents(1, true), 1000);
println!("all checks passed");
}
$ cargo run
all checks passed

Steps to one

Count the steps until a number reaches 1: halve it when even, otherwise triple it and add one. Use a loop that ends in break steps, with the next number an if expression. No one-line formula exists.

Starterexercises/collatz.rs
/// How many steps until `start` reaches 1? One step: if the number is even, halve it;
/// if it is odd, triple it and add one. `start` is at least 1.
fn collatz_steps(start: u64) -> u32 {
todo!("a `loop` that ends with `break steps`; the next number is an `if` expression")
}
fn main() {
assert_eq!(collatz_steps(1), 0); // already there: zero steps
assert_eq!(collatz_steps(2), 1);
assert_eq!(collatz_steps(6), 8);
assert_eq!(collatz_steps(7), 16);
assert_eq!(collatz_steps(27), 111);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/collatz.solution.rsRun ↗
/// How many steps until `start` reaches 1? One step: if the number is even, halve it;
/// if it is odd, triple it and add one. `start` is at least 1.
fn collatz_steps(start: u64) -> u32 {
let mut n = start;
let mut steps = 0;
loop {
if n == 1 {
break steps;
}
n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
steps += 1;
}
}
fn main() {
assert_eq!(collatz_steps(1), 0); // already there: zero steps
assert_eq!(collatz_steps(2), 1);
assert_eq!(collatz_steps(6), 8);
assert_eq!(collatz_steps(7), 16);
assert_eq!(collatz_steps(27), 111);
println!("all checks passed");
}
$ cargo run
all checks passed

Early return, then a value

Return 0 early when whole is 0, then finish with a plain expression, with no return and no ;. Integer / already rounds down, and // starts a comment in Rust.

Starterexercises/percent.rs
/// What percent `part` is of `whole`, rounded down. When `whole` is 0 the answer is 0.
fn percent(part: u32, whole: u32) -> u32 {
todo!(
"return early when whole is 0, then finish with an expression and no `;`. Integer `/` rounds down; `//` starts a comment in Rust"
)
}
fn main() {
assert_eq!(
percent(1, 4),
25,
"1 of 4 is 25 percent; `/` already rounds down"
);
assert_eq!(percent(1, 3), 33); // rounded down
assert_eq!(percent(150, 100), 150);
assert_eq!(percent(0, 9), 0);
assert_eq!(percent(3, 0), 0); // must not divide by zero
assert_eq!(percent(0, 0), 0);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/percent.solution.rsRun ↗
/// What percent `part` is of `whole`, rounded down. When `whole` is 0 the answer is 0.
fn percent(part: u32, whole: u32) -> u32 {
if whole == 0 {
return 0;
}
part * 100 / whole
}
fn main() {
assert_eq!(
percent(1, 4),
25,
"1 of 4 is 25 percent; `/` already rounds down"
);
assert_eq!(percent(1, 3), 33); // rounded down
assert_eq!(percent(150, 100), 150);
assert_eq!(percent(0, 9), 0);
assert_eq!(percent(3, 0), 0); // must not divide by zero
assert_eq!(percent(0, 0), 0);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more