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)
Defining Iterator for own types
A custom type needs to implement the Iterator trait, which is defined as:
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }
For example, if I have a custom datatype of ranges with even numbers, the definition of such a data type, iterator implementation and usage will look like this:
// Custom type
pub struct EvenRanges { pub first: i32, pub last: i32} // Constructor for custom type
impl EvenRanges { pub fn new(first: i32, last: i32) -> EvenRanges { return EvenRanges { first, last} } } // implementation of Iterator
impl Iterator for EvenRanges { // This is a new syntax and has to do with associative types
// this will be explained in chapter 19
type Item = i32; fn next(&mut self) -> Option<Self::Item> { let next = if (self.first + 1) % 2 == 0 { self.first + 1
} else { self.first + 2
}; if next < self.last { self.first = next; Some(next) } else { None
} } } fn main() { let ranges = EvenRanges::new(2, 10); // This is possible because // EvenRanges implements EvenRanges
for i in ranges { println!("{:}", i) }
}
Traits
Closures can capture values from their environment in three ways, which directly map to the three ways ownership and references work in Rust:- Taking ownership
- Immutable borrow (shared reference)
- Mutable borrow
These three ways corresponds to 3 traits.
- FnOnce
- Fn
- 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:
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:
.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:
But this wont:
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:
and usage of into_iter() would look like:
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:
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.
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:
Post a Comment