
Node to Rust, Day 15: Closures
December 15, 2021
Introduction
Closures are a natural part of JavaScript. It’s hard to imagine what programming is like without them. Luckily, you don’t have to. The behavior of Rust’s closures is similar enough to JavaScript’s that you will be able to retain most of what you’re comfortable with.
Closures are defined as functions that retain references to (enclose) its surrounding state. I use the term “closure” here as a general term to mean an anonymous function, regardless of whether or not it references external variables.
Quick links
- Day 1: From nvm to rustup
- Day 2: From npm to cargo
- Day 3: Setting up VS Code
- Day 4: Hello World (and your first two WTFs)
- Day 5: Borrowing & Ownership
- Day 6: Strings, part 1
- Day 7: Syntax and Language, part 1
- Day 8: Language Part 2: From objects and classes to HashMaps and structs
- Day 9: Language Part 3: Class Methods for Rust Structs (+ enums!)
- Day 10: From Mixins to Traits
- Day 11: The Module System
- Day 12: Strings, Part 2
- Day 13: Results & Options
- Day 14: Managing Errors
- → Day 15: Closures
- Day 16: Lifetimes, references, and
'static
- Day 17: Arrays, Loops, and Iterators
- Day 18: Async
- Day 19: Starting a large project
- Day 20: CLI Arguments & Logging
- Day 21: Building and Running WebAssembly
- Day 22: Using JSON
- Day 23: Cheating The Borrow Checker
- Day 24: Crates & Tools
The code in this series can be found at wasmflow/node-to-rust
This guide is not a comprehensive Rust tutorial. This guide tries to balance technical accuracy with readability and errs on the side of “gets the point across” vs being 100% correct. If you have a correction or think something needs deeper clarification, send a note on Twitter at @jsoverson, @candle_corp, or join our Discord channel.
Closure syntax comparison
If you haven’t been following along with the code repository, now is a good time to start. This day’s code is available in Day 15 of the project on github.
This section maps JavaScript/TypeScript closures to the equivalent Rust syntax without much explanation. If you get lost, please reach out. Your perspective on what is confusing will help make this guide better. Don’t hestitate to message me on twitter at @jsoverson or in our Discord channel.
Basic closure syntax
This closure prints Hi! I'm in a closure
.
let closure = () => {
console.log("Hi! I'm in a closure");
};
closure();
let closure = || {
println!("Hi! I'm in a closure");
};
closure();
Hi! I'm in a closure
Rust uses pipes instead of parentheses for arguments and does not have a separator between the arguments and the body.
Closures with a single expression body
These closures show how you can omit the curly braces {}
for closures that consist of a single expression.
let double = (num: number) => num + num;
let num = 4;
console.log(`${num} + ${num} = ${double(num)}`);
let double = |num: i64| num + num;
let num = 4;
println!("{} + {} = {}", num, num, double(num));
4 + 4 = 8
Both JavaScript and Rust can omit the curly braces if the body consists of a single expression.
Closures referencing external variables
A proper closure references variables from its parent’s scope. That’s no problem in Rust.
let name = "Rebecca";
closure = () => {
console.log(`Hi, ${name}.`);
};
closure();
let name = "Rebecca";
let closure = || {
println!("Hi, {}.", name);
};
closure();
Hi, Rebecca.
Mutable variables need a mutable closure! The state of your closure is part of the closure. If you mutate a variable in a closure then the closure itself must be made mutable.
These closures increment a counter when executed.
let counter = 0;
closure = () => {
counter += 1;
console.log(`This closure has a counter. I've been run ${counter} times.`);
};
closure();
closure();
closure();
console.log(`The closure was called a total of ${counter} times`);
let mut counter = 0;
let mut closure = || {
counter += 1;
println!(
"This closure has a counter. I've been run {} times.",
counter
);
};
closure();
closure();
closure();
println!("The closure was called a total of {} times", counter);
This closure has a counter. I've been run 1 times.
This closure has a counter. I've been run 2 times.
This closure has a counter. I've been run 3 times.
The closure was called a total of 3 times
Returning a closure
Generating closures dynamically is straightforward once you get over the nuances in the different traits. The make-adder functions take in an addend and generate a closure that takes in a second number and sums the enclosed value with the passed value.
function makeAdder(left: number): (left: number) => number {
return (right: number) => {
console.log(`${left} + ${right} is ${left + right}`);
return left + right;
};
}
let plusTwo = makeAdder(2);
plusTwo(23);
fn make_adder(left: i32) -> impl Fn(i32) -> i32 {
move |right: i32| {
println!("{} + {} is {}", left, right, left + right);
left + right
}
}
let plus_two = make_adder(2);
plus_two(23);
2 + 23 is 25
The Fn
, FnMut
, and FnOnce
traits
Functions come in three flavors.
Fn
: a function that immutably borrows any variables it closes over.FnMut
: a function that mutably borrows variables it closes over.FnOnce
: a function that consumes (loses ownership of) of its values and thus can only be run once, e.g.
let name = "Dwayne".to_owned();
let consuming_closure = || name.into_bytes();
let bytes = consuming_closure();
let bytes = consuming_closure(); // This is a compilation error
The move
keyword
The move keyword tells Rust that the following block or closure takes ownership of any variables it references. It’s necessary above because we’re returning a closure that references left
which would normally be dropped when the function ends. When we move
it into the closure, we can return the closure without issue.
Composing functions
The compose
function takes two functions and returns a closure that runs both and pipes the output of one into the other.
Each input closure takes one argument of the generic type T
and returns a value also of type T
. The first of the two closures is the plus_two
closure from above. The second closure, times_two
, multiplies its input by two.
The generated closure, double_plus_two
, composes the original two into one.
function compose<T>(f: (left: T) => T, g: (left: T) => T): (left: T) => T {
return (right: T) => f(g(right));
}
let plusTwo = makeAdder(2); // ← makeAdder from above
let timesTwo = (i: number) => i * 2;
let doublePlusTwo = compose(plusTwo, timesTwo);
console.log(`${10} * 2 + 2 = ${doublePlusTwo(10)}`);
fn compose<T>(f: impl Fn(T) -> T, g: impl Fn(T) -> T) -> impl Fn(T) -> T {
move |i: T| f(g(i))
}
let plus_two = make_adder(2); // ← make_adder from above
let times_two = |i: i32| i * 2;
let double_plus_two = compose(plus_two, times_two);
println!("{} * 2 + 2 = {}", 10, double_plus_two(10));
10 * 2 + 2 = 22
Regular function references
This section shows how you can treat any function as a first-class citizen in Rust.
function regularFunction() {
console.log("I'm a regular function");
}
let fnRef = regularFunction;
fnRef();
fn regular_function() {
println!("I'm a regular function");
}
let fn_ref = regular_function;
fn_ref();
I'm a regular function
Storing closures in a struct
Storing functions can be a little trickier due to the different Fn*
traits and the dyn [trait]
behavior.
This code creates a class or struct that you instantiate with a closure. You can then call .run()
from the resulting instance to execute the stored closure.
class DynamicBehavior<T> {
closure: (num: T) => T;
constructor(closure: (num: T) => T) {
this.closure = closure;
}
run(arg: T): T {
return this.closure(arg);
}
}
let square = new DynamicBehavior((num: number) => num * num);
console.log(`${5} squared is ${square.run(5)}`);
struct DynamicBehavior<T> {
closure: Box<dyn Fn(T) -> T>,
}
impl<T> DynamicBehavior<T> {
fn new(closure: Box<dyn Fn(T) -> T>) -> Self {
Self { closure }
}
fn run(&self, arg: T) -> T {
(self.closure)(arg)
}
}
let square = DynamicBehavior::new(Box::new(|num: i64| num * num));
println!("{} squared is {}", 5, square.run(5))
Remember we can’t use impl [trait]
outside of a function’s parameters or return value, so to store a closure we need to store it as a dyn [trait]
. Also remember that dyn [trait]
is unsized and Rust doesn’t like that. We can Box
it to move passed Rust’s complaints (see: Day 14 : What’s a box?).
Additional reading
- The Rust Book: ch 13.01 - Closures
- The Rust Book: ch 19.05 - Advanced Functions and Closures
- Rust by Example: Closures
- Rust Reference: Closure expressions
Wrap-up
Rust’s closures are not as terrifying as some people make them out to be. You will eventually get to some gotchas and hairy parts, but we’ll tackle those when we deal with async. First though, we’ve put off lifetimes for long enough. We’ll get deeper into Rust’s borrow checker tomorrow before moving on to Arrays, iterators, async, and more.
As always, you can reach me personally on twitter at @jsoverson, the Candle team at @candle_corp, and our Discord channel.
Written By
