Foreword
Written for the Rust Auckland meetup 2019-12-02.
- Rust Auckland meetup: https://www.meetup.com/rust-akl/
- Slack: https://rust-akl.slack.com/messages/CCC7KUXMY/
- Repository: https://github.com/azriel91/rust_closures
Feel free to use and improve on these.
What Are Closures
(azriel's) Definition: A stored anonymous function, that captures its scoped environment.
#![allow(unused_variables)] fn main() { let closure_0 = || println!("hello"); let closure_1 = || println!("world"); closure_0(); }
π¦ Stored
Since a closure can be stored, it can be passed around:
#![allow(unused_variables)] fn main() { struct JoblessWorker { tasks: Vec<fn()>, } let task_0 = || println!("create rust"); let task_1 = || println!("create talk"); let task_2 = || println!("talk create rust"); let azriel = JoblessWorker { tasks: vec![ task_0, task_1, task_2, ], }; azriel.tasks.into_iter().for_each(|f| f()); }
π¦ Anonymous
A closure is a type that cannot be named.
use std::any::Any; struct A; fn hello() { println!("hello"); } fn main() { let a_0: A = A; let a_1: A = A; let f_0: fn() = hello; let f_1: fn() = hello; let c_0 = || {}; let c_1 = || {}; println!("{}", std::any::type_name::<A>()); println!("{}", std::any::type_name::<fn()>()); println!(""); println!("a_0: {:?}", a_0.type_id()); println!("a_1: {:?}", a_1.type_id()); println!("f_0: {:?}", f_0.type_id()); println!("f_1: {:?}", f_1.type_id()); println!("c_0: {:?}", c_0.type_id()); println!("c_1: {:?}", c_1.type_id()); }
πΈοΈ Captures Environment
#![allow(unused_variables)] fn main() { let nine = 9; let closure = |x| x * nine; // nine is captured println!("{:?}", closure(1234567890)) }
Why Closures
- πΊ Development ergonomics
- π Lazy evaluation
πΊ Development Ergonomics
"Tell me what I need to know, and nothing else."
#![allow(unused_variables)] fn main() { let mut numbers = vec![0, 1, 2, 3, 4]; numbers.iter_mut().for_each(|n| *n <<= 1); println!("{:?}", numbers); }
Closure:
|n| *n <<= 1
If we did not have closures:
trait Mutation { type Item; fn mutate(&self, i: &mut Self::Item); } struct Doubler; impl Mutation for Doubler { type Item = i32; fn mutate(&self, i: &mut Self::Item) { *i <<= 1; } } fn do_work(numbers: &mut [i32], mutation: &dyn Mutation<Item = i32>) { for n in numbers { mutation.mutate(n); } } fn main() { let mut numbers = vec![0, 1, 2, 3, 4]; let doubler = Doubler; do_work(&mut numbers, &doubler); println!("{:?}", numbers); }
π Lazy evaluation
Instead of applying an operation to every item in a collection, using a closure allows one to:
- Store the logic.
- Apply the logic when the value is requested.
#![allow(unused_variables)] fn main() { use std::{ thread, time::{Duration, Instant}, }; let start = Instant::now(); let numbers = vec![1, 2, 3]; let mut numbers_double_iter = numbers.iter().map(|n| { // Expensive calculation. thread::sleep(Duration::from_millis(500)); *n << 1 }); // Retrieve one value from the iterator let n = numbers_double_iter.next().unwrap(); println!("n: {}", n); let duration = start.elapsed().as_secs_f32(); println!("Duration: {:.2} s", duration); }
Type Inference
Rust can infer the type of each closure parameters and its return type, provided its usage is unambiguous.
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
If the usage is ambiguous, you may explicitly specify the parameter types and return type that the closure must adhere to.
Shared Syntax Does Not Compile
Since each closure is typed, the following will fail to compile, as Rust needs to choose whether x
's type is u32
or i32
:
#![allow(unused_variables)] fn main() { let add_one = |x| x + 1; add_one(1u32); add_one(1i32); println!("π¦ Ok!") }
Snippet from The Rust Book - Chapter 13.1 Closures
Closure Environment
Borrow or Move
Rust determines whether a variable is borrowed or moved from the closure environment when capturing the variables.
-
Inferred borrow:
#[derive(Clone, Debug)] struct A { value: i32, } fn borrow(_a: &A) { println!("borrow"); } fn consume(_a: A) { println!("consume"); } fn main() { let a = A { value: 0 }; let closure = || { // `a` still refers to `A` instead of `&A`. // So we have to add `&` in front of it. // borrow(a); // ^ // | // expected &A, found struct `A` // help: consider borrowing here: `&a` borrow(&a); }; closure(); let closure_move = || consume(a); closure_move(); }
-
Inferred
move
:#[derive(Clone, Debug)] struct A { value: i32, } fn borrow(_a: &A) { println!("borrow"); } fn consume(_a: A) { println!("consume"); } fn main() { let a = A { value: 0 }; let closure = || { // -- value moved into closure here borrow(&a); consume(a); }; closure(); // let closure_move = || consume(a); // ^^ - use occurs due to use in closure // | // value used here after move // closure_move(); }
-
Explicit
move
:You can use the
move
keyword to indicate the variable must be moved instead of borrowed.#![allow(dead_code)] #[derive(Clone, Debug)] struct A { value: i32, } fn borrow(_a: &A) {} fn consume(_a: A) {} fn main() { let a = A { value: 0 }; let closure_move = move || borrow(&a); // ------- - variable moved due to use in closure // | // value moved into closure here closure_move(); // let closure_move = move || consume(a); // ^^^^^^^ - use occurs due to use in closure // | // value used here after move // closure_move(); }
Borrow Lifetime
The usual borrowing rules apply -- note that the borrow lasts for the lifetime of the closure:
#[derive(Debug)] struct A { value: i32, } fn main() { let mut a = A { value: 0 }; let mut closure = || a.value = 123; // -- - borrow occurs due to use in closure // | // borrow of `a.value` occurs here // a.value = 456; // error[E0506]: cannot assign to `a.value` because it is borrowed closure(); // ------- borrow later used here }
Fn Traits
There are three traits that Rust automatically implements for closures when possible. In increasing superset order:
FnOnce
: Consumesself
. The closure is run once, and may consume variables.FnMut
: Uses&mut self
.The closure may be executed multiple times, and may borrow variables mutably.Fn
: Uses&self
. The closure may be executed multiple times, and only borrow variables immutably.
struct A { value: i32 } fn call_fn_once<F>(f: F) where F: FnOnce() { f(); } fn call_fn_mut <F>(f: &mut F) where F: FnMut() { f(); } fn call_fn <F>(f: &F) where F: Fn() { f(); } fn main() { let a = A { value: 0 }; let closure_fn_once = || { println!("closure_fn_once"); drop(a); }; // closure_fn_once(); // --------------- value moved here // closure_fn_once(); // ^^^^^^^^^^^^^^^ value used here after move let mut a = A { value: 0 }; let mut closure_fn_mut = || { println!("closure_fn_mut. a.value: `{}`", a.value); a.value += 1; }; // closure_fn_mut(); // closure_fn_mut(); // closure_fn_mut(); let a = A { value: 0 }; let mut closure_fn = || { println!("closure_fn. a.value: `{}`", a.value); }; // closure_fn(); // closure_fn(); // closure_fn(); call_fn(&closure_fn); call_fn_mut(&mut closure_fn_mut); call_fn_mut(&mut closure_fn); call_fn_once(closure_fn_once); call_fn_once(closure_fn_mut); call_fn_once(closure_fn); }
Why Not Closures
- Closures don't hold much contextual information (for humans).
- Similar to stringly-typed instead of strongly typed.
Closure:
// Trait:
// Fn(&Block, &Block) -> &Block;
// Implementation
let cipher_chain: Box<dyn Fn(&Block, &Block) -> &Block> =
Box::new(|block_current, block_previous| -> Block { /* .. */ });
Trait:
#![allow(unused_variables)] fn main() { /// Chain of encrypted block data. pub trait CipherChain { /// The type of the block. type Block; /// Returns encrypted block data. /// /// # Parameters /// /// * `block_current`: Block to encrypt. /// * `block_previous`: Previous encrypted block. fn compute(block_current: &Self::Block, block_previous: &Self::Block) -> Self::Block; } }