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
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));}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))}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)}")}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 = 7label = "even" if n % 2 == 0 else "odd"w = 3h = 4area = w * hprint(f"{n} is {label}")print(f"area: {area}")print("clamp:", clamp(-5), clamp(50), clamp(250))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));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)); }}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)}");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;}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";}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,loopand{ }can all sit on the right ofletor be the last line of a function. Every branch must agree on a type (a branch that leaves, likecontinue, fits any type).returnis for leaving early. At the end of a function, write the value withoutreturnand without;.- Assignment (
x = 5) is an expression, but it is worth(), soa = b = cdoes not chain andif x = 1is a type error. Conditions must bebooland need no parentheses.
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.fn add_one(x: i32) -> i32 { x + 1;}
fn main() { println!("{}", add_one(41));}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 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))None
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: 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 errorC# 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;}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";}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.
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
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.
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.
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.
TypeScript refuses (TS2355): a function declared to return number must return a value. The fix is return x + 1;.
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;.
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;.
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.
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).
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.
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.
fn add_one(x: i32) -> i32 { x + 1}
fn main() { println!("{}", add_one(41));}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.)
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}");}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.)
fn double(x: i32) -> i32 { x * 2}
fn main() { let a = { double(4) }; let b = { double(4); }; println!("a: {a:?}"); println!("b: {b:?}");}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:
fn five() -> i32 { 5}
fn main() { if true { five() }}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”.)
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::<()>());}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:
fn main() { let mut a: i32 = 0; let mut b: i32 = 0; a = b = 5; println!("{a} {b}");}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 ().
fn main() { let mut x = 0; if x = 1 { println!("x is one"); }}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.)
fn main() { let x = let y = 6; println!("{x} {y}");}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:
fn total(n: i32) -> i32 { if n > 0 { 10 } else { 20 } + 1}
fn main() { println!("{}", total(5));}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:
fn main() { let n = 7; let size = if n > 5 { "big" }; println!("{size}");}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):
fn main() { let mut i = 0; let found = while i < 10 { i += 1; if i == 4 { break i; } }; println!("{found:?}");}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:
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}");}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 + 1and C#’s=> x + 1have 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.
// 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 valuefn 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 5fn 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 linefn 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
// 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 valuefn 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 5fn c_when_n_is_7() -> &'static str { "c = 7"}
/// Hint: a block can hold a `let` first; its value is still the last linefn 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");}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.
/// 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
/// 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");}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.
/// 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
/// 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");}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.
/// 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
/// 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");}all checks passed
Read more
- The Rust Reference: Statements and expressions, with Statements, Expressions and Block expressions: the exact rules, including the
;and}ones. - The Rust Reference: Functions (the grammar of a function body).
- The Rust Reference: Loop expressions (
breakwith a value) and Never type. - The Rust Book, chapter 3.3: Functions, the section “Statements and Expressions”, and chapter 3.5: Control Flow.
- Rust by Example: Expressions and if/else.
- The primitive type
unitin the standard library docs.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.