Polyglot
Languages Rust functions
Rust § functions

Functions and closures

Functions are the principal Rust abstraction. The fn keyword introduces a function; parameters require explicit types; return types follow ->; the body is a block expression whose last expression (without a trailing semicolon) is the return value. Functions are first-class values with type fn(...) -> .... Closures are anonymous functions that admit capturing variables from the enclosing scope; they have one of three traits — Fn, FnMut, FnOnce — depending on how they capture. The combination — explicit-type functions, closures, traits expressing call-shape, monomorphisation for generic functions — covers the function surface.

This page covers function declarations, calling conventions, closures, and conventional patterns.

Function declarations

The principal form:

fn name(param1: Type1, param2: Type2) -> ReturnType {
    body
}

Examples:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn greet(name: &str) {                         // implicit -> ()
    println!("Hello, {}", name);
}

fn area(width: f64, height: f64) -> f64 {
    width * height
}

Each parameter requires a type annotation. The return type is () (unit, the empty tuple) if omitted. The function body is a block expression; the last expression (without a trailing semicolon) is the return value.

Return values

Three principal forms:

fn implicit() -> i32 {
    42                                          // last expression; implicit return
}

fn explicit() -> i32 {
    return 42;                                  // explicit return
}

fn early(n: i32) -> i32 {
    if n < 0 {
        return 0;                               // early return
    }
    n * 2                                       // implicit return
}

The conventional Rust style favours the implicit form (last expression without semicolon) for the principal return; explicit return is reserved for early returns.

For “no useful return value”, use ():

fn perform_action() {                           // implicit -> ()
    println!("doing something");
}

Multiple returns via tuples

Rust does not have multiple-return values; tuples are the conventional substitute:

fn min_max(v: &[i32]) -> (i32, i32) {
    let min = *v.iter().min().unwrap();
    let max = *v.iter().max().unwrap();
    (min, max)
}

let (lo, hi) = min_max(&[3, 1, 4, 1, 5, 9, 2, 6]);

For named fields, define a struct:

struct Bounds { min: i32, max: i32 }

fn min_max(v: &[i32]) -> Bounds {
    Bounds {
        min: *v.iter().min().unwrap(),
        max: *v.iter().max().unwrap(),
    }
}

The struct form is conventionally clearer for non-trivial returns.

Function pointers

A function’s type is fn(...) -> ...:

fn add(a: i32, b: i32) -> i32 { a + b }

let f: fn(i32, i32) -> i32 = add;
let result = f(3, 4);                          // 7

Function pointers admit dynamic dispatch:

fn apply(op: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
    op(a, b)
}

let result = apply(add, 3, 4);                 // 7

For most uses, closures (treated below) are conventional.

Generic functions

Functions may be generic over types:

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

let x = max(3, 4);                              // T = i32
let y = max(3.0, 4.0);                          // T = f64
let z = max("a", "b");                          // T = &str

The <T> introduces the type parameter; T: PartialOrd is a trait bound requiring T to support <. The function works for any type satisfying the bound; the compiler generates a specialised version for each concrete type used (monomorphisation).

Treated in Generics.

Default arguments

Rust does not have default arguments. The conventional substitutes:

  • Multiple functionsnew(), with_capacity(cap).
  • Option<T> parametersfn build(name: &str, version: Option<&str>).
  • Builder patterns — see Idioms.
  • Default traitfn make() -> T where T: Default; T::default() provides the default.
struct Config {
    timeout: u64,
    retries: u32,
}

impl Default for Config {
    fn default() -> Self {
        Config { timeout: 30, retries: 3 }
    }
}

fn run_with_config(config: Config) {
    // ...
}

run_with_config(Config::default());

run_with_config(Config { timeout: 60, ..Default::default() });

The ..Default::default() admits “use these specified fields, default the rest” — the conventional Rust form for partial overrides.

Variadic arguments

Rust does not admit variadic functions in safe Rust. The principal substitutes:

  • Vec<T> or array parameter — pass a collection.
  • Macrosvec![1, 2, 3], println!("{}, {}", a, b).
  • FFIunsafe extern "C" fn admits C-style varargs.
fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

let total = sum(&[1, 2, 3, 4]);

// Macros for variadic-like:
let v = vec![1, 2, 3, 4];
println!("a={}, b={}, c={}", a, b, c);

Closures

Closures are anonymous functions that admit capturing variables from the enclosing scope:

let add_one = |x| x + 1;
println!("{}", add_one(5));                    // 6

let multiplier = 3;
let times = |x| x * multiplier;                 // captures multiplier
println!("{}", times(5));                       // 15

The principal forms:

let f = |x| x + 1;                              // single param, type inferred
let f = |x: i32| -> i32 { x + 1 };              // explicit types
let f = |a, b| a + b;                           // multiple params
let f = || 42;                                  // no params
let f = |x| {                                   // multi-statement body
    let y = x * 2;
    y + 1
};

Closures are first-class values; they can be stored, passed, and returned.

The closure traits

Closures have one of three traits, depending on how they capture:

TraitCapture modeReusable?
FnOnceMoves captured valuesOnce
FnMutBorrows mutablyMany times (sequentially)
FnBorrows immutablyMany times (concurrently)

Fn: FnMut: FnOnceFn implements FnMut and FnOnce; FnMut implements FnOnce. The hierarchy admits using a more-restrictive closure where a less-restrictive one is required.

The compiler infers the trait based on what the closure does:

let s = String::from("hello");

let only_once = move || drop(s);                // FnOnce; consumes s
only_once();
// only_once();                                 // ERROR: s already consumed

let mut count = 0;
let mut incr = || count += 1;                    // FnMut; mutates count
incr();
incr();

let multiplier = 3;
let times = |x| x * multiplier;                 // Fn; only reads multiplier
times(5);
times(7);                                        // can call repeatedly

The conventional choice in API design is to take the most-permissive trait bound that suffices.

Capture modes

Closures capture by reference by default; the move keyword admits capturing by value:

let v = vec![1, 2, 3];

let print = || println!("{:?}", v);             // captures by reference
print();
println!("{:?}", v);                             // v still accessible

let v = vec![1, 2, 3];
let move_print = move || println!("{:?}", v);    // captures by value
move_print();
// println!("{:?}", v);                          // ERROR: v moved into closure

The move is conventionally needed when a closure outlives the enclosing scope (e.g., when spawned to a thread):

use std::thread;

let v = vec![1, 2, 3];
thread::spawn(move || {                         // move required
    println!("{:?}", v);
});

Closures as parameters

The impl Fn(...) syntax (or impl FnMut(...), impl FnOnce(...)) admits passing closures:

fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(x)
}

// Or with impl Trait:
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

let result = apply(|n| n * 2, 5);              // 10

Both forms admit static dispatch (monomorphisation). For dynamic dispatch, use Box<dyn Fn(...)>:

fn apply(f: Box<dyn Fn(i32) -> i32>, x: i32) -> i32 {
    f(x)
}

apply(Box::new(|n| n * 2), 5);

The static form is conventionally preferred; dynamic only when the closure type cannot be known at compile time.

Closures as returns

Returning a closure requires impl Fn(...) or Box<dyn Fn(...)>:

fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

let add5 = make_adder(5);
println!("{}", add5(3));                        // 8

The move ensures n is captured by value (otherwise the returned closure would reference a local that no longer exists). The impl Fn admits returning a closure with an unnameable type.

Method functions

Functions defined inside impl blocks are methods:

struct Point { x: f64, y: f64 }

impl Point {
    fn new(x: f64, y: f64) -> Self {            // associated function (no self)
        Point { x, y }
    }

    fn distance(&self, other: &Point) -> f64 {  // method (takes &self)
        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
    }

    fn translate(&mut self, dx: f64, dy: f64) {  // method (takes &mut self)
        self.x += dx;
        self.y += dy;
    }
}

let p = Point::new(3.0, 4.0);
let q = Point::new(0.0, 0.0);
println!("{}", p.distance(&q));                  // 5.0

Treated in Data structures.

The self parameter

A method’s first parameter is one of:

FormDescription
selfConsumes the value (move)
&selfBorrows immutably
&mut selfBorrows mutably
Box<Self>, Rc<Self>, etc.”Object-style” methods on smart pointers
impl Point {
    fn into_tuple(self) -> (f64, f64) {         // consumes self
        (self.x, self.y)
    }

    fn x(&self) -> f64 {                        // borrows immutably
        self.x
    }

    fn set_x(&mut self, x: f64) {               // borrows mutably
        self.x = x;
    }
}

The conventional discipline: &self for getters, &mut self for setters, self for transformations that consume the value.

Generic methods and trait bounds

Methods may be generic and constrained by trait bounds:

struct Container<T> { items: Vec<T> }

impl<T: Clone> Container<T> {
    fn duplicate(&self) -> Vec<T> {
        self.items.iter().cloned().collect()
    }
}

Treated in Generics and Traits.

Common patterns

Iterator with closure

let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|&x| x * 2).collect();
let positive: Vec<i32> = v.iter().filter(|&&x| x > 0).copied().collect();
let sum: i32 = v.iter().sum();

Closures with iterators are the conventional Rust functional-style.

Higher-order function

fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}

let result = apply_twice(|n| n + 1, 5);        // 7

Function composition

fn compose<F, G, T, U, V>(f: F, g: G) -> impl Fn(T) -> V
where
    F: Fn(T) -> U,
    G: Fn(U) -> V,
{
    move |x| g(f(x))
}

let f = compose(|n: i32| n + 1, |n| n * 2);
println!("{}", f(5));                          // (5+1)*2 = 12

The form is verbose; explicit chain-call (g(f(x))) is conventionally clearer in Rust.

Builder pattern

struct RequestBuilder {
    url: String,
    method: String,
    headers: Vec<(String, String)>,
}

impl RequestBuilder {
    fn new(url: &str) -> Self {
        RequestBuilder {
            url: url.to_string(),
            method: "GET".to_string(),
            headers: vec![],
        }
    }

    fn method(mut self, m: &str) -> Self {
        self.method = m.to_string();
        self
    }

    fn header(mut self, k: &str, v: &str) -> Self {
        self.headers.push((k.to_string(), v.to_string()));
        self
    }

    fn build(self) -> Request {
        Request { /* ... */ }
    }
}

let req = RequestBuilder::new("https://example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .build();

The pattern admits configurable construction; treated in Idioms.

Callback pattern

fn with_resource<F: FnOnce(&Resource) -> R, R>(f: F) -> R {
    let resource = acquire_resource();
    let result = f(&resource);
    // resource dropped automatically
    result
}

let value = with_resource(|r| r.read_value());

The pattern admits “open, use, close” with the user supplying only the use.

A note on const fn

Some functions admit being called at compile time:

const fn square(n: i32) -> i32 {
    n * n
}

const FOUR: i32 = square(2);                   // computed at compile time

const fn admits a subset of Rust (no allocation, no trait calls in older versions, etc.); the surface has expanded substantially over recent versions. The conventional use is for compile-time constants and static-data generation.

A note on the conventional discipline

The contemporary Rust function advice:

  • Take parameters by reference (&T) when borrowing suffices; by value (T) only when consuming.
  • Use &str instead of &String for string parameters; &[T] instead of &Vec<T> for slice parameters.
  • Return impl Trait rather than naming concrete types when the return type is implementation detail.
  • Use closures for short callbacks; named functions for reusable logic.
  • Use the most-permissive Fn trait that suffices in trait bounds.
  • Document with /// and include # Examples sections.

The combination — explicit types, ownership-aware parameters, closure-based callbacks, generics with trait bounds — is the substance of Rust function design. The mechanism admits substantial expressiveness and zero-cost abstractions.