When Ferrous Metals Corrode, pt. XIII

Intro

For this post I'm looking at Chapter 14. Closures in the Rust Programming Book. A whole chapter dedicated to closures, count me in!

Capturing Variables

Rust closures can capture vars from the enclosing scope, as in other languages.

Closures That Borrow

By default closures borrow refs to captured vars, e.g. in the example below the closure where 'stat' is used, the closure gets a ref to the stat.

fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
    cities.sort_by_key(|city| -city.get_statistic(stat));
}

The usual rules around data lifetime apply – the ref must not outlive the scope of its target

Closures That Steal

An example where a closure is stored in key_fn and used in the thread:

use std::thread;

fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
    -> thread::JoinHandle<Vec<City>>
{
    let key_fn = |city: &City| -> i64 { -city.get_statistic(stat) };

    thread::spawn(|| {
        cities.sort_by_key(key_fn);
        cities
    })
}

This won't compile, as Rust can't guarantee that the reference to stat in key_fn is used safely, and this also applies to cities; we can't know that the thread finishes before start_sorting_thread() exits.

The solution is to move ownership into the thread, e.g. define key_fn thusly:

let key_fn = move |city: &City| -> i64 { -city.get_statistic(stat) };

Rules of ownership must be followed here as well, e.g. one can't use city after it's been moved into the closure, unless it were Copy (in which case it would be copied). If it's not Copy and we needed to access it later, we'd need to Clone it.

Function and Closure Types

Functions are first class objects and therefore have a type, e.g.:

// accepts a vec and a fn
fn count_selected_cities(cities: &Vec<City>,
                         test_fn: fn(&City) -> bool) -> usize
{
    let mut count = 0;
    for city in cities {
        if test_fn(city) {
            count += 1;
        }
    }
    count
}
// ...for instance this test_fn
fn has_monster_attacks(city: &City) -> bool {
    city.monster_attack_risk > 0.0
}

That fun type is not compatible with closures though.

There is a trait that covers both functions and closures though, Fn. This version of count_selected_cities which accepts any value that implements Fn:

fn count_selected_cities<F>(cities: &Vec<City>, test_fn: F) -> usize
    where F: Fn(&City) -> bool
{ ... }

Syntax comparison:

fn(&City) -> bool    // fn type (functions only)
Fn(&City) -> bool    // Fn trait (both functions and closures)

Closures in Rust have an ad hoc type created by the compiler, depending on which data it borrows or steals. They all implement Fn though.

Closure Performance

Rust closures don't have to live on the heap, and they can be inlined, so as a rule they should be fast

Closures and Safety

Closures That Kill

It follows from Rusts ownership rules that you can't call a closure twice that drops a value from it's env – you'd be accessing a moved value:

let my_str = "hello".to_string();
let f = || drop(my_str);

f();  // ok
f();  // error: use of moved value

FnOnce

Similar to the above but with the Fn trait:

fn call_twice<F>(closure: F) where F: Fn() {
    closure();
    closure();
}
let my_str = "hello".to_string();
let f = || drop(my_str);
call_twice(f);

Fn() is shorthand for Fn() -> () btw.

Rust doesn't let us call this twice; it complains that our closure in f only has the FnOnce trait, not Fn. All closures that drop values are FnOnce not Fn.

FnMut

FnMut is for the category of closures that mutate their data or refs, e.g.:

let mut i = 0;
let incr = || {
    i += 1;  // incr borrows a mut reference to i
    println!("Ding! i is now: {}", i);
};
call_twice(incr);

We end up with this hierarchy of Fn-related traits:

Fn

closures (and funs) that we call multiple times without restriction

FnMut

closures that can be called multiple times – if we declare it mut

FnOnce

closures that can be called once, if the caller owns the closure

Declaring a closure mut looks something like this:

fn call_twice<F>(mut clos: F) where F: FnMut() {
    clos();
    clos();
}

With this, call_twice() accepts all FnMut closures (which implies all Fn closures/funs).

Copy and Clone for Closures

Closures are data, specifically structs – move closures contain the values they capture, non-move contain refs to those.

Closures may be Copy- and Clone-able, like regular structs. Non-move, non-mutating closures e.g. are both Copy and Clone. Move closures that capture Copy vars is itself Copy, and similarly for Clone.

Callbacks

If we're using closures for callbacks and for example want to keep track of closures in a map we might be tempted to use something like the below. This stores Fn closures for processing request/responses in a hashmap, indexed with strings:

struct BasicRouter<C> where C: Fn(&Request) -> Response {
    routes: HashMap<String, C>
}

Unfortunately capturing closures have unique types, as they capture different variables. And with the above we asked for them to all be of the same type.

We could constrain the callbacks via boxed trait objects instead:

type BoxedCallback = Box<dyn Fn(&Request) -> Response>;

struct BasicRouter {
    routes: HashMap<String, BoxedCallback>
}

Or, we could constrain with function pointers instead (which wouldn't capture anything):

struct FnPointerRouter {
    routes: HashMap<String, fn(&Request) -> Response>
}

Using Closures Effectively

The book explains some issues around using closures with the traditional MVC pattern. Traditionally in MVC, components (Model, View and Controller) have a reference to each other (at least for the controller and model/view respectively). This is done either via a direct reference or via callbacks that are used to handle updates.

This approach doesn't work well with Rust – here each object has an explicit owner, and we must not introduce reference cycles.

There are alternatives, e.g. via a different architecture such as Facebooks Flux, or by indexing objects or callbacks and passing around indices. In any case, "sea of objects" is not a design pattern well suited to Rust