Functional Language Features
Fun Fact: The implementations of closures and iterators are such that runtime performance is not affected. It means as if you've written to an optimized low level code, like in Assembly Language. This is part of Rust’s goal to strive to provide zero-cost abstractions.
- Programming in a functional style often includes using functions as values by passing them in arguments, returning them from other functions, assigning them to variables for later execution, and so forth.
- Specifically, functional programming includes:
- Closures: A function-like construct you can store in a variable.
- Iterators: A way of processing a series of elements.
Closures
-
They are "Anonymous Functions that Can Capture Their Environment".
-
An example of closure:
#![allow(unused)] fn main() { let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; }
-
Why closures don't require type annotations, but functions (
fn
) do?- Type annotations are required on functions because they’re part of an explicit interface exposed to your users. Defining this interface rigidly is important for ensuring that everyone agrees on what types of values a function uses and returns.
- But closures aren’t used in an exposed interface like this: they’re stored in variables and used without naming them and exposing them to users of our library.
-
In case, we still want to explicitly define type annotations, we can do it by:
#![allow(unused)] fn main() { let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; }
-
Comparisons for Functions and closures syntax:
#![allow(unused)] fn main() { 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 ; }
-
Closures will always have only one concrete type:
#![allow(unused)] fn main() { // FAIL: Closure inferred two different types of x, which is against the rules let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); }
-
Performing memoization or lazy evaluation:
-
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.
-
All closures implement at least one of the traits:
Fn
,FnMut
, orFnOnce
. -
Using this information, we can create a
Cacher
#![allow(unused)] fn main() { // Cacher will store a closure inside calculation // and the computed value inside value struct Cacher<T> where T: Fn(u32) -> u32, { calculation: T, value: Option<u32>, } }
-
The use of the memoization is that, we can store the closure, that contains computation which takes very long time to finish. Then we can save it's computed value inside the struct, so that we can reuse to computation (thereby preventing expensive computation again and again), as well as update the computed value whenever necessary.
-
We'll need to write an implementation block to make the
Cacher
easier to use:#![allow(unused)] fn main() { impl<T> Cacher<T> where T: Fn(u32) -> u32, { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = v; v } } } } }
-
The only limitation of this
Cacher
is that it assumes that, it'll only receive one value, that means even if we call thevalue()
function multiple with different arguments, it'll still return the same value every time and that value will be the computed value when the closure was called for the first time. Here's the explanation:#![allow(unused)] fn main() { let mut c = Cacher::new(|a| a); let v1 = c.value(1); // v1 = 1 let v2 = c.value(2); // v2 = 1 let v3 = c.value(3); // v3 = 1 }
-
So, here is a better version of the above cacher that can memorize all the arguments and their computed value inside a HashMap, which is also generic. You may refer it's implementation over here.
-
-
Capturing the Environmet with closures:
-
You can't don the following thing using simple functions:
// FAIL: Functions can't capture their environment, hence x shouldn't live inside the function fn main() { let x = 4; fn equal_to_x(z: i32) -> bool { z == x } let y = 4; assert!(equal_to_x(y)); }
-
But, you can easliy do this using closure:
fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); }
-
Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: taking ownership, borrowing mutably, and borrowing immutably. These are encoded in the three Fn traits as follows:
FnOnce
consumes the variables it captures from its enclosing scope, known as the closure’s environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.FnMut
can change the environment because it mutably borrows values.Fn
borrows values from the environment immutably.
-
When you create a closure, Rust infers which trait to use based on how the closure uses the values from the environment. All closures implement
FnOnce
because they can all be called at least once. Closures that don’t move the captured variables also implementFnMut
, and closures that don’t need mutable access to the captured variables also implementFn
. In Listing 13-12, the equal_to_x closure borrows x immutably (so equal_to_x has the Fn trait) because the body of the closure only needs to read the value in x. -
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. Themove
closures may still implement Fn or FnMut, even though they capture variables by move. This is because the traits implemented by a closure type are determined by what the closure does with captured values, not how it captures them. The move keyword only specifies the latter. -
An example of
move
:// FAIL: We tried to print x even though it is moved inside closure fn main() { let x = vec![1, 2, 3]; let equal_to_x = move |z| z == x; println!("can't use x here: {:?}", x); let y = vec![1, 2, 3]; assert!(equal_to_x(y)); }
-
Most of the time when specifying one of the
Fn
trait bounds, you can start withFn
and the compiler will tell you if you needFnMut
orFnOnce
based on what happens in the closure body.
-
Iterators
-
Iterators are lazy in rust, meaning they have no effect until you call methods that consume the iterator to use it up.
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); // It won't do anything useful until called upon for val in v1_iter { // Now, the iterator is called upon and used println!("Got: {}", val); } }
-
To just get the next element stored in iterator:
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; // Calling the next() method changes the state of iterator, // hence we'll need to use mut in this case let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); }
-
Why is it required to use
mut
when usingnext()
, but not when usingfor
loop?next()
- Each call tonext
eats up an item from the iterator. Hence, we need to define it asmut
to be able to do that.for
- The loop takes ownership of the iterator and made it mutable behind the scenes. Hence, we don't need to usemut
there.
-
Difference between
iter
,into_iter
, anditer_mut
-
They all return iterator, except the way they return differs. Here are the differences:
into_iter
: It yields any ofT
,&T
or&mut T
, depending on the context.iter
: It yields&T
.iter_mut
: It yields&mut T
.
-
For more details refer to this stackoverflow answer.
-
-
Consuming Adaptors
: Some methods insideIterator
trait usesnext()
. It means those functions will also eat away the iterator, just like hownext()
does. Here's an example:#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); // sum() uses the next() and hence will eat away the iterator }
-
Iterator Adaptors
: Some methods insideIterator
allows you to change iterators into different kinds of iterators. It is also possible to useIterator
,Enumerator
,Map
, andFilter
together. Rust has these functions inside the Iterator Trait.#![allow(unused)] fn main() { let v1: Vec<u32> = vec![0, 1, 2, 3, 4, 5]; let iterator = v1.iter() .enumerate() .filter(|(i, val)| i % 2 == 0) .map(|(i, val)| val); // On it's own it won't do anything, because iterators are lazy // You can either print them one by one using for loop (remember it'll consume the iterator) for val in iterator { println!("{}", val); } // Or you can collect them inside a vector, make sure you explicitly specify the type (`Vec<_>`) too. let new_vector: Vec<_> = iterator.collect(); println!("New Vector: {:?}") }
-
Creating your own iterator:
-
You'll need to implement
Iterator
trait on your type. -
You'll only need to define one function, that is
next()
, it'll be sufficient.struct Counter { count: u32, } impl Counter { fn new() -> Self { Self { count: 0, } } } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } } fn main() { let counter = Counter::new(); for val in counter { println!("{:?}", val); } // Since we have next() method we can use any default implementation of the Iterator trait let sum: u32 = Counter::new() .zip(Counter::new().skip(1)) // Skips first element only and generate pairs { (1,2) (2,3) (3,4) (4,5) } because (5,None) is ignored .map(|(a, b)| a * b) // [2, 6, 12, 20] .filter(|x| x % 3 == 0) // [6, 12] .sum(); // 18 }
-
-
Which is faster,
for
loop oriterator adapters
?-
Here are the benchmarks:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
-
Iterators, although a high-level abstraction, get compiled down to roughly the same code as if you’d written the lower-level code yourself. Iterators are one of Rust’s
zero-cost abstractions
, which means that using the abstraction imposes no additional runtime overhead.
-