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 functions —
new(),with_capacity(cap). Option<T>parameters —fn build(name: &str, version: Option<&str>).- Builder patterns — see Idioms.
- Default trait —
fn 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.- Macros —
vec![1, 2, 3],println!("{}, {}", a, b). - FFI —
unsafe extern "C" fnadmits 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:
| Trait | Capture mode | Reusable? |
|---|---|---|
FnOnce | Moves captured values | Once |
FnMut | Borrows mutably | Many times (sequentially) |
Fn | Borrows immutably | Many times (concurrently) |
Fn: FnMut: FnOnce — Fn 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:
| Form | Description |
|---|---|
self | Consumes the value (move) |
&self | Borrows immutably |
&mut self | Borrows 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
&strinstead of&Stringfor string parameters;&[T]instead of&Vec<T>for slice parameters. - Return
impl Traitrather 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
Fntrait that suffices in trait bounds. - Document with
///and include# Examplessections.
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.