Skip to content
Learn Rust
Theme

Functions

Basics · Lesson 13

Parameters always have types. The return type comes after ->. The last line, written without a semicolon, is the return value.

Coming from GoTypes come after names like Go, with -> i32 for the return type. The last expression without a semicolon is the return value, and return is still there for leaving early.

Coming from KotlinThe shape is Kotlin's fun add(a: Int, b: Int): Int, with -> before the return type. Braces are always required (there is no = a + b body), and the last expression without a semicolon is the result, so return is only for leaving early.

Coming from PythonLike a fully type-hinted def, except the compiler enforces the hints and you must write them for every parameter. The last expression without a semicolon is returned, so leaving out return doesn't give you None.

Coming from TypeScriptSame shape as function add(a: number, b: number): number, with -> before the return type. Parameter types must always be written, and so must any return type other than nothing. The last expression without a semicolon is the return value.

Coming from JavaThe return type goes after ->, and the last expression without a semicolon is the return value, so return is only needed for early exits. Rust has no overloading, so two functions need two different names.

Coming from C#The return type comes after ->, and a block's last expression (no semicolon) is its result. Unlike an expression-bodied => member, the block can hold statements first. Rust has no method overloading and no default or named arguments.

Coming from Cfn add(a: i32, b: i32) -> i32 is int add(int a, int b) with the return type moved to the end. The last expression without a semicolon is returned, and falling off the end of a function that should return a value is a compile error, not junk.

Coming from C++The trailing -> i32 is like C++'s trailing return type, and the last expression without ; is the return value. Forgetting to return a value is a compile error rather than undefined behaviour, and Rust has no overloading or default arguments.

The quick comparison

Rustsrc/main.rsRun ↗
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon: this is what the function returns
}
fn greet(name: &str) { // no -> means it returns nothing
println!("hi {name}");
}
fn main() {
println!("{}", add(2, 3));
greet("kotlin");
}
$ cargo run
5
hi kotlin
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func greet(name string) {
fmt.Println("hi", name)
}
func main() {
fmt.Println(add(2, 3))
greet("kotlin")
}
$ go run main.go
5
hi kotlin
fun add(a: Int, b: Int): Int {
return a + b
}
fun greet(name: String) {
println("hi $name")
}
fun main() {
println(add(2, 3))
greet("kotlin")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
5
hi kotlin
def add(a: int, b: int) -> int:
return a + b # return gives the value back
def greet(name: str) -> None:
print(f"hi {name}")
print(add(2, 3))
greet("kotlin")
$ python3 main.py
5
hi kotlin
function add(a: number, b: number): number {
return a + b;
}
function greet(name: string): void { // void: returns nothing
console.log(`hi ${name}`);
}
console.log(add(2, 3));
greet("kotlin");
$ tsc --strict --module nodenext --types node main.ts && node main.js
5
hi kotlin
public class Main {
static int add(int a, int b) {
return a + b;
}
static void greet(String name) { // void means it returns nothing
System.out.println("hi " + name);
}
public static void main(String[] args) {
System.out.println(add(2, 3));
greet("kotlin");
}
}
$ javac Main.java && java Main
5
hi kotlin
Console.WriteLine(Add(2, 3));
Greet("kotlin");
static int Add(int a, int b) => a + b;
static void Greet(string name) // void: returns nothing
{
Console.WriteLine($"hi {name}");
}
$ dotnet run
5
hi kotlin
#include <stdio.h>
int add(int a, int b) {
return a + b; // return gives the value back
}
void greet(const char *name) { // void means it returns nothing
printf("hi %s\n", name);
}
int main(void) {
printf("%d\n", add(2, 3));
greet("kotlin");
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
5
hi kotlin
#include <iostream>
#include <string_view>
int add(int a, int b) {
return a + b;
}
void greet(std::string_view name) { // void: returns nothing
std::cout << "hi " << name << "\n";
}
int main() {
std::cout << add(2, 3) << "\n";
greet("kotlin");
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
5
hi kotlin

What's different

  • return a + b; also works. The no-semicolon style is just shorter, and it is what most Rust code uses.
  • &str is the type for text you only read; a string literal such as "kotlin" has it. Strings comes later.
  • Functions can be declared after they are used. Their order in the file doesn't matter.
  • Arguments are passed by value, and a String passed in is moved, not copied (Ownership). There are no default or named arguments and no overloading (What Rust lacks).
Gotcha

A semicolon on the last line

Looks fine: in Python and 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.Broken in Python. Rust catches it.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(a: i32, b: i32) -> i32 {
a + b;
}
fn main() {
println!("{}", add(2, 3));
}
Go✗ refuses too

Go refuses this too: a + b is "not used" and the function has a missing return. Go always needs an explicit return, so a stray semicolon can't change what a function returns.

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

Kotlin refuses this too: a block body needs return, so a body whose last line is just a + b gives missing return statement.

fun add(a: Int, b: Int): Int {
a + b
}
fun main() {
println(add(2, 3))
}
Main.kt:3:1: error: missing return statement.
}
^
def add(a: int, b: int) -> int:
a + b
print(add(2, 3))
$ python3 main.py! runs · wrong answer
None
TypeScript✗ refuses too

TypeScript refuses this too: add promises a number but has no return. Without the : number it would compile and quietly return undefined.

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

Java refuses this too: a + b; is "not a statement". Java always needs an explicit return, so a stray semicolon can't change what a method returns.

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

C# refuses this too: a + b; is not a valid statement on its own, and the method never returns a value (CS0201, CS0161). C# always needs an explicit return.

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

Go refuses this too: a + b is "not used" and the function has a missing return. Go always needs an explicit return, so a stray semicolon can't change what a function returns.

What happens in Kotlin

Kotlin refuses this too: a block body needs return, so a body whose last line is just a + b gives missing return statement.

What happens in Python

Python prints None. Without return, the last line is worked out and thrown away, and the function gives back None.

What happens in TypeScript

TypeScript refuses this too: add promises a number but has no return. Without the : number it would compile and quietly return undefined.

What happens in Java

Java refuses this too: a + b; is "not a statement". Java always needs an explicit return, so a stray semicolon can't change what a method returns.

What happens in C#

C# refuses this too: a + b; is not a valid statement on its own, and the method never returns a value (CS0201, CS0161). C# always needs an explicit return.

What happens in C

gcc only warns that a + b does nothing and that add never returns. The program prints 2 instead of 5: whatever value happened to be left behind. Another compiler or machine can print something else.

What happens in C++

g++ warns that a + b; does nothing and that add never returns, then builds it anyway. Running off the end is undefined behaviour, and here g++ plants a trap so the program dies before printing. On ARM, this site's machine, that is a trap signal (SIGTRAP); on x86 it is an illegal-instruction signal (SIGILL).

What Rust sees

In Rust a block's value is its last expression. Adding ; turns a + b into a statement that produces nothing, written () and called the unit type.

Why Rust says no

So the function now promises an i32 and gives back (). The compiler's help: line points at the exact semicolon to remove.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(2, 3));
}
$ cargo run
5

The toolkit

A Rust function states its inputs and its output in its signature. How the body ends (the tail expression: the last line, with no ;) is in Statements and expressions, and reading a signature piece by piece is in Reading signatures. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.

The signature is the contract

Each parameter is name: Type and the result follows -> (the spelling). No -> means the function returns (), the value for “nothing” (unit), and -> ! means it never returns (never type).

Rustsrc/main.rsRun ↗
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn greet(name: &str) {
println!("hello, {name}");
}
fn main() {
println!("add(2, 3): {}", add(2, 3));
greet("Ferris");
}
$ cargo run
add(2, 3): 5
hello, Ferris

Inside a body Rust works out types for you (Variables); in a signature the type is required for every parameter. Leave one out and the compiler complains at the comma. Grouping, as in a, b: i32, leaves a without one:

Rustsrc/main.rs✗ won't compile
fn add(a, b: i32) -> i32 {
a + b
}
fn main() {
println!("add(2, 3): {}", add(2, 3));
}
$ cargo build
error: expected one of `:`, `@`, or `|`, found `,`
 --> src/main.rs:1:9
  |
1 | fn add(a, b: i32) -> i32 {
  |         ^ expected one of `:`, `@`, or `|`
  |
help: if this is a parameter name, give it a type
  |
1 | fn add(a: TypeName, b: i32) -> i32 {
  |         ++++++++++
help: if this is a type, explicitly ignore the parameter name
  |
1 | fn add(_: a, b: i32) -> i32 {
  |        ++

Leave the return type out and it is (), so a body that produces a number is a mismatch. The compiler’s help line suggests the type:

Rustsrc/main.rs✗ won't compile
fn add(a: i32, b: i32) {
a + b
}
fn main() {
add(2, 3);
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:2:5
  |
1 | fn add(a: i32, b: i32) {
  |                       - help: try adding a return type: `-> i32`
2 |     a + b
  |     ^^^^^ expected `()`, found `i32`

Leaving early

A function normally ends on its tail expression; return is for leaving before the end, from inside an if or a loop:

Rustsrc/main.rsRun ↗
fn sign(n: i32) -> i32 {
if n < 0 {
return -1;
}
if n == 0 {
return 0;
}
1
}
fn next_multiple(of: u32, above: u32) -> u32 {
let mut n = above + 1;
loop {
if n % of == 0 {
return n;
}
n += 1;
}
}
fn main() {
println!("sign(-7): {}", sign(-7));
println!("sign(0): {}", sign(0));
println!("sign(12): {}", sign(12));
println!("next_multiple(7, 20): {}", next_multiple(7, 20));
}
$ cargo run
sign(-7): -1
sign(0): 0
sign(12): 1
next_multiple(7, 20): 21

A function that panics or ends the program is written -> !, and that type fits anywhere a value is expected, which is why half can still promise an i32. The panic message names the line, and the exit status is non-zero (Errors covers panic! and its alternatives):

Rustsrc/main.rs! panics at runtimeRun ↗
fn fail(msg: &str) -> ! {
panic!("{msg}")
}
fn half(n: i32) -> i32 {
if n % 2 == 0 {
n / 2
} else {
fail("odd number")
}
}
fn main() {
println!("half(10): {}", half(10));
println!("half(7): {}", half(7));
}
$ cargo run
half(10): 5

thread 'main' (1) panicked at src/main.rs:2:5:
odd number
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Functions inside functions

A function can be written inside another function’s body:

Rustsrc/main.rsRun ↗
fn describe(n: i32) -> String {
fn is_even(n: i32) -> bool {
n % 2 == 0
}
let kind = if is_even(n) { "even" } else { "odd" };
format!("{n} is {kind}")
}
fn main() {
println!("{}", describe(7));
println!("{}", describe(10));
}
$ cargo run
7 is odd
10 is even

Because a nested function is an ordinary function, it cannot see the outer function’s variables. A closure can (Closures):

Rustsrc/main.rs✗ won't compile
fn total_with_tax(price: u32) -> u32 {
let rate = 20;
fn tax(p: u32) -> u32 {
p * rate / 100
}
println!("rate: {rate}");
price + tax(price)
}
fn main() {
println!("total: {}", total_with_tax(50));
}
$ cargo buildWhat is E0434?
error[E0434]: can't capture dynamic environment in a fn item
 --> src/main.rs:4:13
  |
4 |         p * rate / 100
  |             ^^^^
  |
  = help: use the `|| { ... }` closure form instead
Rustsrc/main.rsRun ↗
fn total_with_tax(price: u32) -> u32 {
let rate = 20;
let tax = |p: u32| p * rate / 100;
println!("rate: {rate}");
price + tax(price)
}
fn main() {
println!("total: {}", total_with_tax(50));
}
$ cargo run
rate: 20
total: 60

Arguments are passed by value

Every argument is handed to the parameter: copied for simple values such as numbers, moved for others. The parameter is a new variable: changing it needs mut, and does not change the caller’s. A let with the same name hides it (shadowing):

Rustsrc/main.rsRun ↗
fn bump(mut n: i32) -> i32 {
n += 1;
n
}
fn cap(qty: u32) -> u32 {
let qty = qty.min(10); // a new variable that hides the parameter
qty * 3
}
fn main() {
let a = 5;
let b = bump(a);
println!("a: {a}, b: {b}");
println!("cap(4): {}", cap(4));
println!("cap(50): {}", cap(50));
}
$ cargo run
a: 5, b: 6
cap(4): 12
cap(50): 30

A String is not copied for you. Passing it gives it away, and the caller cannot use it afterwards:

Rustsrc/main.rs✗ won't compile
fn length(s: String) -> usize {
s.len()
}
fn main() {
let name = String::from("Ferris");
let n = length(name);
println!("{n} {name}");
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `name`
 --> src/main.rs:8:20
  |
6 |     let name = String::from("Ferris");
  |         ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 |     let n = length(name);
  |                    ---- value moved here
8 |     println!("{n} {name}");
  |                    ^^^^ value borrowed here after move
  |
note: consider changing this parameter type in function `length` to borrow instead if owning the value isn't necessary
 --> src/main.rs:1:14
  |
1 | fn length(s: String) -> usize {
  |    ------    ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     let n = length(name.clone());
  |                        ++++++++

That is the ownership rule, taught in Ownership (which values are copied is in Copy or reference?). The usual fix is to pass &name, a borrow (Borrowing), so the caller keeps the value.

Functions are values

A function’s name converts to a function pointer, whose type is written fn(i32) -> i32. A closure that captures nothing converts to one too:

Rustsrc/main.rsRun ↗
fn double(n: i32) -> i32 {
n * 2
}
fn negate(n: i32) -> i32 {
-n
}
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
println!("apply(double, 4): {}", apply(double, 4));
println!("apply(negate, 4): {}", apply(negate, 4));
// A closure that captures nothing converts to a fn pointer.
println!("apply(|n| n + 100, 4): {}", apply(|n| n + 100, 4));
let ops: [fn(i32) -> i32; 2] = [double, negate];
for f in ops {
println!("f(10): {}", f(10));
}
}
$ cargo run
apply(double, 4): 8
apply(negate, 4): -4
apply(|n| n + 100, 4): 104
f(10): 20
f(10): -10

A closure that uses a variable from outside does not:

Rustsrc/main.rs✗ won't compile
fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let extra = 100;
println!("{}", apply(|n| n + extra, 4));
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:7:26
  |
7 |     println!("{}", apply(|n| n + extra, 4));
  |                    ----- ^^^^^^^^^^^^^ expected fn pointer, found closure
  |                    |
  |                    arguments to this function are incorrect
  |
  = note: expected fn pointer `fn(i32) -> i32`
                found closure `{closure@src/main.rs:7:26: 7:29}`
note: closures can only be coerced to `fn` types if they do not capture any variables
 --> src/main.rs:7:34
  |
7 |     println!("{}", apply(|n| n + extra, 4));
  |                                  ^^^^^ `extra` captured here
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn apply(f: fn(i32) -> i32, x: i32) -> i32 {
  |    ^^^^^ -----------------

To accept those too, take impl Fn(i32) -> i32 (below). Lowercase fn(...) is a type, capitalised Fn a bound: Reading signatures lists both.

Your habits, in Rust

Going deeper

impl Trait as a parameter type

impl Fn(i32) -> i32 or impl Display means “any type that can do this” (how to read it). The Reference calls it an anonymous type parameter: the caller picks the type, the body may use only what the trait provides. A closure that captures works here, and so does a number or a string. See Traits and Generics.

Rustsrc/main.rsRun ↗
use std::fmt::Display;
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn show(x: impl Display) -> String {
format!("<{x}>")
}
fn main() {
let extra = 100;
println!("apply(|n| n + extra, 4): {}", apply(|n| n + extra, 4));
println!("show(5): {}", show(5));
println!("show(\"hi\"): {}", show("hi"));
}
$ cargo run
apply(|n| n + extra, 4): 104
show(5): <5>
show("hi"): <hi>

const fn

A const fn can be run by the compiler while compiling, when it is called in a const, a static or an array length (Declarations and types). It can still be called at run time. A const fn may call only other const fns (and tuple-struct constructors):

Rustsrc/main.rsRun ↗
const fn square(n: u32) -> u32 {
n * n
}
const AREA: u32 = square(12);
static GRID: [u8; square(3) as usize] = [0; square(3) as usize];
fn main() {
println!("AREA: {AREA}");
println!("GRID.len(): {}", GRID.len());
println!("square(7) at run time: {}", square(7));
}
$ cargo run
AREA: 144
GRID.len(): 9
square(7) at run time: 49
Rustsrc/main.rs✗ won't compile
fn helper() -> u32 {
1
}
const fn f() -> u32 {
helper()
}
fn main() {
println!("{}", f());
}
$ cargo buildWhat is E0015?
error[E0015]: cannot call non-const function `helper` in constant functions
 --> src/main.rs:6:5
  |
6 |     helper()
  |     ^^^^^^^^
  |
note: function `helper` is not const
 --> src/main.rs:1:1
  |
1 | fn helper() -> u32 {
  | ^^^^^^^^^^^^^^^^^^
  = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants

The C++ cousin is constexpr. The Reference lists what constant evaluation allows.

Deep recursion

A function can call itself. Each call takes space on the thread’s stack (Stack and heap), which is fixed in size, so a call chain that is too deep runs out:

Rustsrc/main.rs! exits with code -6Run ↗
fn depth(n: u64) -> u64 {
if n == 0 { 0 } else { 1 + depth(n - 1) }
}
fn main() {
println!("depth(1_000): {}", depth(1_000));
println!("depth(100_000_000): {}", depth(100_000_000));
}
$ cargo run
depth(1_000): 1000

thread 'main' (1) has overflowed its stack
fatal runtime error: stack overflow, aborting

The depth that fails depends on the machine and the stack size, and these are debug builds: an optimised build can change the outcome for a function this simple. Rust prints a message and aborts the process. Some languages turn the same bug into an exception, and others die as Rust does:

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.

Small pure functions

Write three functions. Use an early return in two and a plain tail expression in the other, make gcd call itself, and mind the semicolons (the gotcha above is the trap). Leap years (1900 and 2000) and a zero on either side of gcd are what people get wrong.

Starterexercises/small-functions.rs
/// `n` limited to the range `lo..=hi`.
fn clamp(n: i32, lo: i32, hi: i32) -> i32 {
todo!("return lo if n < lo, hi if n > hi, otherwise n: an early return, then a tail expression")
}
/// Gregorian leap year: divisible by 4, except centuries, except every 400th year.
fn is_leap_year(year: u32) -> bool {
todo!("one boolean expression with && and ||, and no semicolon at the end")
}
/// The greatest common divisor, by Euclid's rule: gcd(a, 0) is a, otherwise gcd(b, a % b).
fn gcd(a: u32, b: u32) -> u32 {
todo!("the base case is an early return; otherwise the function calls itself")
}
fn main() {
assert_eq!(clamp(5, 0, 10), 5);
assert_eq!(clamp(-3, 0, 10), 0);
assert_eq!(clamp(99, 0, 10), 10);
assert_eq!(clamp(0, 0, 10), 0);
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
// Centuries are not leap years, unless divisible by 400.
assert!(!is_leap_year(1900));
assert!(is_leap_year(2000));
assert_eq!(gcd(12, 18), 6);
assert_eq!(gcd(17, 5), 1);
// The base case, whichever side is zero.
assert_eq!(gcd(7, 0), 7);
assert_eq!(gcd(0, 5), 5);
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/small-functions.solution.rsRun ↗
/// `n` limited to the range `lo..=hi`.
fn clamp(n: i32, lo: i32, hi: i32) -> i32 {
if n < lo {
return lo;
}
if n > hi {
return hi;
}
n
}
/// Gregorian leap year: divisible by 4, except centuries, except every 400th year.
fn is_leap_year(year: u32) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
/// The greatest common divisor, by Euclid's rule: gcd(a, 0) is a, otherwise gcd(b, a % b).
fn gcd(a: u32, b: u32) -> u32 {
if b == 0 {
return a;
}
gcd(b, a % b)
}
fn main() {
assert_eq!(clamp(5, 0, 10), 5);
assert_eq!(clamp(-3, 0, 10), 0);
assert_eq!(clamp(99, 0, 10), 10);
assert_eq!(clamp(0, 0, 10), 0);
assert!(is_leap_year(2024));
assert!(!is_leap_year(2023));
// Centuries are not leap years, unless divisible by 400.
assert!(!is_leap_year(1900));
assert!(is_leap_year(2000));
assert_eq!(gcd(12, 18), 6);
assert_eq!(gcd(17, 5), 1);
// The base case, whichever side is zero.
assert_eq!(gcd(7, 0), 7);
assert_eq!(gcd(0, 5), 5);
println!("all checks passed");
}
$ cargo run
all checks passed

Functions as values

by_name returns one of three functions, chosen by a match. apply_n takes impl Fn(i32) -> i32 and applies it repeatedly, so a closure that captures a variable works. Applying it zero times must give back the input.

Starterexercises/pick-and-apply.rs
fn double(n: i32) -> i32 {
n * 2
}
fn negate(n: i32) -> i32 {
-n
}
fn identity(n: i32) -> i32 {
n
}
/// The function called `name`: "double", "negate", or `identity` for anything else.
fn by_name(name: &str) -> fn(i32) -> i32 {
todo!("a match on name whose arms are function names, with no parentheses")
}
/// Apply `f` to `x`, then to the result, `times` times in all. `f` may capture variables.
fn apply_n(f: impl Fn(i32) -> i32, times: u32, x: i32) -> i32 {
todo!("a for loop over 0..times that updates a mutable copy of x")
}
fn main() {
assert_eq!(by_name("double")(4), 8);
assert_eq!(by_name("negate")(4), -4);
assert_eq!(by_name("nope")(4), 4);
assert_eq!(apply_n(double, 3, 1), 8);
assert_eq!(apply_n(negate, 2, 7), 7);
// Zero times means the input comes back unchanged.
assert_eq!(apply_n(double, 0, 5), 5);
// A closure that captures `step` works because the parameter is `impl Fn`.
let step = 7;
assert_eq!(apply_n(|n| n + step, 3, 0), 21);
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/pick-and-apply.solution.rsRun ↗
fn double(n: i32) -> i32 {
n * 2
}
fn negate(n: i32) -> i32 {
-n
}
fn identity(n: i32) -> i32 {
n
}
/// The function called `name`: "double", "negate", or `identity` for anything else.
fn by_name(name: &str) -> fn(i32) -> i32 {
match name {
"double" => double,
"negate" => negate,
_ => identity,
}
}
/// Apply `f` to `x`, then to the result, `times` times in all. `f` may capture variables.
fn apply_n(f: impl Fn(i32) -> i32, times: u32, x: i32) -> i32 {
let mut value = x;
for _ in 0..times {
value = f(value);
}
value
}
fn main() {
assert_eq!(by_name("double")(4), 8);
assert_eq!(by_name("negate")(4), -4);
assert_eq!(by_name("nope")(4), 4);
assert_eq!(apply_n(double, 3, 1), 8);
assert_eq!(apply_n(negate, 2, 7), 7);
// Zero times means the input comes back unchanged.
assert_eq!(apply_n(double, 0, 5), 5);
// A closure that captures `step` works because the parameter is `impl Fn`.
let step = 7;
assert_eq!(apply_n(|n| n + step, 3, 0), 21);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more