Rust closures as input parameters

5 minute read 23 May 2016

Edit: Update (May 25)

I am learning Rust and, as a beginner, I have sometimes problems achieving some little tasks that would be so easy in other programming languages I know better.

But when I met some Rust developers and they ask me about my difficulties, I often forget about them.

I therefore decided to write about my difficulties in Rust to keep track of them.

So today about closures.

When reading a blog post introducing Rust for Node.js Developers I made the same Todo application.

At the end, the program contains such piece of code:

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {
	if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
		todo.deleted = true;
	}
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {
	if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
		todo.completed = true;
	}
}

The first function marks a Todo as deleted if it can be found by its ID in the vector. The second function marks a Todo as completed, also if it can be found in the vector of Todos.

Some code is duplicated and I decided to refactor the common code in a third function, that would do something on a Todo if found in a vector.

This third function would take a closure as input parameter, like in pseudo-code:

fn with_todo_id(todos: &mut Vec<Todo>, todo_id: i16, f: <closure - do something on a Todo>) {
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        f(todo);
    }
}

so that the 2 initial functions are simplified like that:

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.deleted = true);
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.completed = true);
}

This closure is a side-effect on a Todo. It should accept a mutable Todo as parameter and return nothing.

One source of documentation for closures as input parameters mentions that there exist 3 kinds of closures:

  • Fn: takes captures by reference (&T)
  • FnMut: takes captures by mutable reference (&mut T)
  • FnOnce: takes captures by value (T)

This is a lot of information for a new developer.

I tried different possibilities, like:

fn with_todo_id(todos: &mut Vec<Todo>, todo_id: i16, f: &Fn(&mut Todo)) {
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        f(todo);
    }
}

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.deleted = true);
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.completed = true);
}

Without any success:

$ cargo run
   Compiling todo-list v0.1.0 (file:///Users/yannsimon/projects/rust/rust-playground/todo-list)
src/main.rs:27:38: 27:50 error: the type of this value must be known in this context
src/main.rs:27 	with_todo_id(todos, todo_id, |todo| todo.deleted = true);
               	                                    ^~~~~~~~~~~~

The official documentation was not so much help neither.

I asked for help on #rust-beginners. People on this channel are very helpful and kind. The community of Rust is awesome!

I was proposed 2 solutions. Both work, and I choose that one:

fn with_todo_id<P>(todos: &mut Vec<Todo>, todo_id: i16, f: P) where P: Fn(&mut Todo) {
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        f(todo);
    }
}

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.deleted = true);
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.completed = true);
}

Compared to my previous attempt, the f: &Fn(&mut Todo) is replaced by f: P where P: Fn(&mut Todo).

I still do not completely understand why this works and not the previous version. I was explained Rust can use the reference to the closure... I will continue reading documentation about it.... ;)

If you have any good source for this, please tell me.

In conclusion I still find closure as input parameters quite complex in Rust. I surely need to more understand the theory behind the language to fully understand them.

The Rust community is very helpful, but it may not scale if there are more and more beginners like me.

Update (May 25)

The following tweet from @rustlang provided me the good keywords to search for:

it's about trait objects vs type parameters, which can be tough when you're learning

@rustlang

Trait objects are used for dynamic dispatch, feature found in most OO languages.

With that in mind, I could understand the Rust book about closures.

If I use trait objects, this version works:

fn with_todo_id(todos: &mut Vec<Todo>, todo_id: i16, f: &Fn(&mut Todo)) {
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        f(todo);
    }
}

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, &|todo| todo.deleted = true);
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, &|todo| todo.completed = true);
}

Trait objects force Rust to use dynamic dispatch.

If I use type parameter instead of a trait object:

fn with_todo_id<P>(todos: &mut Vec<Todo>, todo_id: i16, f: P) where P: Fn(&mut Todo) {
    if let Some(todo) = todos.iter_mut().find(|todo| todo.id == todo_id) {
        f(todo);
    }
}

fn remove_todo(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.deleted = true);
}

fn mark_done(todos: &mut Vec<Todo>, todo_id: i16) {
    with_todo_id(todos, todo_id, |todo| todo.completed = true);
}

then Rust is able to monomorphize the closure and use static dispatch, and does not need any object for the dyamic dispatch.

Another great example of the zero-cost abstraction possible with Rust!