Functions#

A function in Rust is declared with fn, has typed parameters, an optional return type (-> T), and a body that is itself an expression. The implicit return value is the final expression (no trailing ;). Functions can be generic over types and lifetimes, async, const, and unsafe.

/types`. For methods (functions on a type), see OOP. For async runtime mechanics, see Concurrency.

Declaration#

fn add(a: i32, b: i32) -> i32 {
    a + b                            // implicit return; no `;`
}

fn shout(s: &str) -> String {
    s.to_uppercase()
}

fn print_args(args: &[String]) {     // -> () implied
    for a in args { println!("{a}"); }
}

Annotate every parameter and the return type; inference does not reach across function boundaries.

Parameters#

Pass by value (move or copy), shared reference (&T), or exclusive reference (&mut T). Pick based on ownership intent.

fn consume(s: String)      { /* takes ownership */ }
fn read(s: &String)        { /* borrows; better as &str */ }
fn read_str(s: &str)       { /* prefer this */ }
fn modify(v: &mut Vec<i32>){ v.push(0); }

Default values are not built in; use Option<T> or an Options struct with Default.

fn fetch(url: &str, timeout: Option<Duration>) { /* … */ }
fetch("https://x", None);

Returns#

The final expression of the body is the return value. return expr; also works, mostly for early returns.

fn classify(n: i32) -> &'static str {
    if n < 0 { return "negative"; }
    if n == 0 { return "zero"; }
    "positive"
}

Multiple returns: tuples or structs.

fn divmod(a: i32, b: i32) -> (i32, i32) { (a / b, a % b) }
let (q, r) = divmod(17, 5);

Closures#

|params| body. Closures capture surrounding bindings; the compiler picks how (by reference, by mutable reference, by move) based on what the body needs.

let n = 10;
let add_n = |x: i32| x + n;
println!("{}", add_n(5));            // 15

let mut total = 0;
let mut add = |x| total += x;
add(1); add(2); add(3);
println!("{total}");                  // 6

let owned = String::from("hi");
let print = move || println!("{owned}"); // forces capture by move

Closures implement one of three traits.

Trait

When

Fn

reads captured bindings; can be called many times.

FnMut

mutates captured bindings; can be called many times.

FnOnce

moves captured bindings; can be called once.

Take closures by trait bound or trait object.

fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn apply_mut<F: FnMut(&str)>(mut f: F, items: &[&str]) {
    for x in items { f(x); }
}

let boxed: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + 1);

Generics#

Type parameters in <>; constraints with T: Trait or in a where clause.

fn head<T>(xs: &[T]) -> Option<&T> {
    xs.first()
}

fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn sort_strings<T>(xs: &mut [T])
where T: AsRef<str> + Ord {
    xs.sort();
}

impl Trait#

Argument position: f(x: impl Trait) is sugar for f<T: Trait>(x: T). Return position: -> impl Trait lets the operator return an unnamed type that implements the trait (common for iterators and futures).

fn double_all(xs: &[i32]) -> impl Iterator<Item = i32> + '_ {
    xs.iter().map(|x| x * 2)
}

The caller cannot name the return type, but can call any method on it that the trait provides.

Lifetimes in signatures#

When the compiler cannot tell which input lifetime an output reference comes from, the operator annotates explicitly.

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

struct Wrap<'a> { name: &'a str }

impl<'a> Wrap<'a> {
    fn name(&self) -> &'a str { self.name }
}

async functions#

async fn returns a Future; calling the function does not run it. The future is driven by an executor (tokio, async-std, smol). See Concurrency.

async fn fetch(url: &str) -> reqwest::Result<String> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

#[tokio::main]
async fn main() -> reqwest::Result<()> {
    let body = fetch("https://example.com").await?;
    println!("{body}");
    Ok(())
}

const fn#

const fn can be called at compile time as well as runtime. Used for table lookups, sized constants, and static / const initialisers.

const fn buffer_size(level: u32) -> usize {
    1 << (level + 10)
}

const BUF: usize = buffer_size(3);

unsafe fn#

unsafe fn marks a function whose caller must uphold invariants the compiler cannot check. Pointer dereferences, FFI, manual aliasing, calls to other unsafe functions live inside unsafe blocks or unsafe fn.

unsafe fn deref(p: *const i32) -> i32 { *p }

let n = 10_i32;
let p: *const i32 = &n;
unsafe { println!("{}", deref(p)); }

Wrap minimal unsafe blocks in safe APIs; nothing outside the wrapper needs the unsafe keyword.

References#