30 days of Rust - Day Thirteen - Closures
Today, I decided to take it easy and just learn a new concept. Closures.
Day 13 - Closures
Sometimes when programming, we need to pass functions around as arguments to other functions, return them as well as assign them to a variable to be called at a later stage.
If, like me, you have experience in a language such as Javascript, you may think of them as arrow functions or lambda functions in Python.
Defining a Closure
In order to define a closure, we simply have its arguments between two pipes like |x|
. Thereafter follows the closure body which can be between brackets {}
if it is multi-line or the inline expression followed by a ;
.
fn main() {
let y = 10;
let my_closure = |x| x+2;
let result = my_closure(y);
println!("{}", result)
}
Calling a closure is the exact same as calling a function. Please keep in mind that my_closure
does not store the result of calling the closure, but stores the definition
Where's the type?
This is the first thing I asked myself. Since Rust is strongly typed, the above example has no types for both the argument x
as well as the output x+2
.
Well, that's the cool thing about closures. Rust can infer the type from when you call the closure. In our case, Rust already knows that y
is of type i32
, therefore it can tell that my_closure
takes an i32
one we call it.
The only limitation is that we cannot call the same closure with different data types. The code below would not run.
let y = 10;
let name ="masai";
let my_closure = |x| x+2;
let result = my_closure(y);
let boss = my_closure(name);
Generic Parameters
A cool example I saw was of a Cacher, which stores the result of calling an expensive function call. The Struct below takes in any closure which has one argument of type u32 and returns a number of type u32.
struct Cacher<T>
where T : Fn(u32) -> u32 {
calculation: T,
value: Option<u32>
}
So the idea is that the result is only calculated once and any subsequent calls to the function will get the same response immediately without the need to run the function again.
struct Cacher<T>
where T : Fn(u32) -> u32 {
calculation: T,
value: Option<u32>
}
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 = Some(v);
v
} }
} }
So anytime we need the function we can simply call it by
// get a new cacher
let mut exp = Cacher::new(expensive_closure);
//calculate one time only
let v = exp.value(5);
println!("{:?}", v);
Conclusion
This concept is quite advanced and it will take more than just one day to grasp. Hopefully I will implement closures in a project soon to really appreciate their power.
Maybe I am also having coder's block 🤣. The goal is just to show up no matter what. No excuses. It will all make sense.
Please let me know what you are currently learning or looking forward to learning. Hopefully I can help.
Thanks for spending some time learning Rust with me. Until next time. Peace ✌🏼