Functional Language Features: Iterators and Closures

Closures

Rust’s closures are anonymous functions you can save in a variable or pass as arguments to other functions, and unlike functions, closures can capture values from the scope in which they are defined.

let some_closure = |param1: u32, param2: u32| -> Result<(), Box<dyn std::error::Error>> {
	do_something_with_params(param1, param2)?;
	Ok(())
};

// Next, we cal call some_closure as a normal function.
if let Ok(_) = some_closure(123, 456) {
	// do something...
}

Closures does not require one to annotate the types of the parameters or the return value like a normal function does. Type annotations are required on functions because they are part of an explicit interface exposed to users, while closures are local objects storing in variables and used without naming them and exposing them to users of the library. Closures are usually short and relevant only within a narrow context rather than in any arbitrary scenario. Within these limited contexts, the compiler is reliably able to infer the types of the parameters and the return type, similar to how it is able to infer the types of most variables. The following definitions are quasi-equivalent:

// Function
fn  add_one_v1   (x: u32) -> u32 { x + 1 }
// Closures
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Note that closures are statically types, which means if the compiler infers the type of the closure, we cannot change it anymore.

let foo = |x| x;
let s = foo(String::from("bar")); // OK.
let n = foo(5);                   // Error!
let n = foo(5.to_string())        // OK.

Storing Closures Using Generic Parameters and the Fn Traits

We can create a struct that will hold the closure and the resulting value of calling the closure. The struct will execute the closure only if we need the resulting value, and it will cache the resulting value so the rest of our code doesn’t have to be responsible for saving and reusing the result. You may know this pattern as memoization or lazy evaluation.

struct Cacher<T>
where T: Fn(u32) -> u32
{
	calculation: T, // Trait bound on closure type: Fn (u32) -> u32
	value: Option<u32>,
}

impl<T> Cacher<T> {
	// Constructor.
	fn new(calculation: T) -> Cacher<T> {
		Cacher {
			calculation,
			value: None,
		}
	}

	// Calculates the value and forbids the user to directly access value.
	fn value(&mut self, arg: u32) -> u32 {
		match self.value {
			Some(val) => val,
			None => {
				let val = (self.calculation)(arg);
				self.value = Some(val);
				self.value
			}
		}
	}
}

Note that closures will capture environment, which means they would incur memory overhead and the issue of ownership. There are three different Fn traits as follows:

These traits can be inferred by the compiler when the closure is created. If you want to force the closure to take ownership of the values it uses in the environment, you can use the move keyword before the parameter list. This technique is mostly useful when passing a closure to a new thread to move the data so it’s owned by the new thread.

let vec1 = vec![1, 2, 3];
let eq = move |x| x == vec1;
// vec is moved and cannot be used anymore.
assert!(eq(vec![1, 2, 3]));

Iterators