Tuesday, February 04, 2020

Learning Rust - Day 9 - Closures and Iterators

This is the 9th entry of my learning Rust journal...

It captures some of the learning points while going through chapter 13 of the Rust Book. You can read other posts in this series by following the label learning rust.

This chapter did not present any new or mind bending concepts. Most modern languages nowadays have the concept of functions as first class citizens, closures and iterators. So the chapter was about taking note of how these concepts are encoded in Rust.

I did not really enjoy how this particular chapter was written, especially section 13.01. I think the pedagogy can be improved. The section spends way too much time in motivating an example, that in my opinion, clouds the essence of what is being explained. It so happened that I found another book on Rust Introduction to Rust which ended up being really well written: concise and well explained. I personally enjoyed the chapter on closure from this book than I did reading the Rust Book itself.

So to the content of this chapter, here are some of the things that stood out:

Syntaxes


Functions as Values

// normal function definition
fn add(a:u32, b:u32) -> u32 {
    a + b
};

// storing defined function into a variable
// keyword fn is used to define the type of a function
let adder_func : fn(u32, u32) -> (u32) = add;

assert_eq!(adder_func(2,3), 5)


Defining Closures

let max = |x: i32, y:i32| {
    if x > y {
        x
    } else {
        y
    }
};

assert_eq!(max(100,10), 100)

Closures do not need to have the type annotations specified, and if the body is a single expression, can be written in one line without the curly braces. Hence the above can also be written as:

let max = |x, y| if x > y { x } else { y };

assert_eq!(max(100,10), 100)

Traits

Closures can capture values from their environment in three ways, which directly map to the three ways ownership and references work in Rust:
  1. Taking ownership
  2. Immutable borrow (shared reference)
  3. Mutable borrow
These three ways corresponds to 3 traits.
  1. FnOnce
  2. Fn
  3. FnMut
Talking about traits, it looks like the implementation of closures in Rust can be approximated to having a backing struct with these traits implemented for it. I say approximated as I believe this is not 100% exactly how the compiler works. Using the approximation, if we have the following definition:

let mut a = vec![1, 2];
let mut b = vec![3, 4];
let mut c = vec![5, 6];
let my_closure = || {
    // Takes `a` by shared reference    
    assert_eq!(a[0], 1);
    // Takes `b` by mutable reference    
b[0] = 1;
    // Moves `c` into the closure    
    let d = c;
};

my_closure()

The above would be transformed into something like:

// Struct to represent the type of the closure
struct StructForMyClosure {
    a: &Vec<i32>,
    b: &mut Vec<i32>,
    c: Vec<i32>,
}

// Capture the variables in the environment
let x = StructForMyClosure{ a: &a, b: &mut b, c: c };

// Implements a trait..
impl FnOnce for StructForMyClosure {
    fn call_once(self) {
        assert_eq!(self.a[0], 1);
        self.b[0] = 1;
        let d = self.c;
    }
}

// my_closure() would internally call the trait function
x.call_once()

.iter() vs .iter_mut() vs .into_iter()


Turning into iterators, one of the few things that stood out to me was the difference between .iter(), .iter_mut() and .into_iter(). Again, unsurprisingly, the differences is related to how Rust handle borrowing/references.

.iter()

This allows having an iterator that have a shared reference with the vector. As seen in the snippet below:

let mut my_vector = vec![1, 2, 3, 4];

let mut iter = my_vector.iter();
// note &1 in Some(&1) indicating shared reference
assert_eq!(iter.next(), Some(&1));

// can still use my_vector// since it was borrowed immutably
my_vector; 

.iter_mut()

This allows for having an iterator that has a mutable borrow of the vector. As seen in the snippet below:

let mut my_vector = vec![1, 2, 3, 4];

let mut iter = my_vector.iter_mut();
// note &mut 1 in Some(&mut 1) indicating mutable borrow
assert_eq!(iter.next(), Some(&mut 1));
assert_eq!(iter.next(), Some(&mut 2));

// can still use my_vector
// since the mutable borrow is out of scope
my_vector;

Due to the mutable borrow the following won't compile, because the vector is being accessed when the mutable borrow is still in scope, violating the restriction that mutable borrow should be exclusive:

let mut my_vector = vec![1, 2, 3, 4];

let mut iter = my_vector.iter_mut();
// note &mut 1 in Some(&mut 1) indicating mutable borrow
assert_eq!(iter.next(), Some(&mut 1));

// leads to compilation error
my_vector;

assert_eq!(iter.next(), Some(&mut 2));

my_vector;

.into_iter()

This methods leads to an iterator that takes ownership of the vector. This means after the iterator is used, the original vector cannot be used anymore. Hence below snippet will compile:


let mut my_vector = vec![1, 2, 3, 4];

let mut iter = my_vector.into_iter();
assert_eq!(iter.next(), Some(1));

But this wont:

let mut my_vector = vec![1, 2, 3, 4];

let mut iter = my_vector.into_iter();
assert_eq!(iter.next(), Some(1));
my_vector;

Note that on a normal day, iterators would not be constructed and consumed by direct calling the next() method, instead they would be used with language construct like for in, eg:

A more real life of .iter() method would look like: 

let mut my_vector = vec![1, 2, 3, 4];

for x in my_vector.iter() {
    println!("{}", x)
}

// prints [1, 2, 3, 4]
println!("{:?}", my_vector)

While that of iter_mut() would look like:

let mut my_vector = vec![1, 2, 3, 4];

for x in my_vector.iter_mut() { 
   // since we have iter_mut
   // we can mutate the contents of the vector  
*x = *x + 1;
println!("{}", x)
}

// prints mutated value [2, 3, 4, 5]
println!("{:?}", my_vector)

and usage of into_iter() would look like:

let mut my_vector = vec![1, 2, 3, 4];

for x in my_vector.into_iter() {
    println!("{}", x)
}

// uncommenting below would lead to compilation error
// since ownership got moved due to into_iter()
// println!("{:?}", my_vector)

Also, even though this was not mentioned in the chapter, I was interested in knowing how to get the index in for for in. Found this can be achieved by calling the enumerate() method on the iterator.

For example:

let mut my_vector = vec![1, 2, 3, 4];

for (k, v) in my_vector.iter().enumerate() {
    /** Below prints:    
     Index: 0, value: 1    
     Index: 1, value: 2    
     Index: 2, value: 3    
     Index: 3, value: 4    
    **/    
   println!("Index: {}, value: {}", k,v)
}

Consuming adaptors and Iterator adaptors

The concept of having a lazy description of transformation over data structures and the action that executes the description is also part of iterators in Rust. In Rust, the methods that leads to lazy transformations are called Iterator adaptors, while the once that perform the transformation and results a value are called Consuming adaptors. Same thang different day you might say!


Example of the Iterator adaptors include methods like filter(),  map() while examples of consuming adaptors are sum(), count() etc.

A new syntax was also dropped in this chapter. It was referred to as associated types, but I won't be knowing what these are until chapter 19 :)

That is it for now, next chapter would be about Cargo and Cargo.io so I expect that to be a breeze!

No comments: