Closures 101

Closures are an interesting CS concept and one that will frequently come up in interviews. I know I've been asked, and have asked, questions about closures for frontend (Javascript) positions numerous times. And in all honesty they're a difficult concept to define, especially when you're under the scrutiny of an interviewer. In this post I'd like to show how Rust leverages the concept of closures and why they might be used. But first, we need to discuss the concept of scope because it is so important for the full understanding of closures.

Scope

In computer programming, the scope of a name binding – an association of a name to an entity, such as a variable – is the region of a computer program where the binding is valid: where the name can be used to refer to the entity.

(emphasis mine)

That above quote is taken from the first line from of the Wikipedia article on scope and probably doesn't need much more clarification. But, hey, let's beat the horse. It's already dead...

So with the above loosely in mind let's take the below commented Rust code:

fn my_func() {
	// We'll call this space "my_func-space

	let x: i64 = 23;

	// ...more code
}

fn main() {
	// Let's call this space (within the `main` function), "main-space"

	let y: i64 = 12;

	my_func();

	// ...more code
}

The reader will -probably- intuitively understand the following:

  • If we tried to println!() the variable x from main-space, the attempt would fail

  • If we tried to println!() the variable y from my_func-space, the attempt would fail

While this is not necessarily true across all languages (Javascript in particular), it holds true in Rust. But why? Rust variables are not globally namespaced and, unlike in JavaScript, the variable will not be searched for in the next parent scope until found. They are confined to the space (scope) in which they were defined.

So, from above, y has been defined in main-space and x has been defined within my_func-space. So with that here's a very loose definition of scope:

scope: a space where a variable has been defined

By design Rust doesn't allow scopes leaking from function to function. So, using below, main-scope doesn't "creep into" my_func-scope which is why we cannot println!() the variable y from my_func; the scope is not valid there and therefore the binding for y is undefined (non-existent).

fn my_func() {
	/ *
	  * 	my_func-scope
	  * /
}

fn main() {
	/ *
	  *	main-scope
	  * /

	my_func( /* my_func-scope exists here */ );
}

So let's talk about closures.

Closures

First, here's an example of a closure in Rust:

let square_num = |x: i64| -> i64 { x * x };

square_num(3); // would return: 9

A Closure can be thought of as an inline, anonymous function. From the Rust docs:

The environment for a closure can include bindings from its enclosing scope in addition to parameters and local bindings.

(emphasis mine)

This stands in contrast to a typical function whose definition might look like this:

The environment for a function only includes bindings from its scope or those which are passed-in as parameters.

What we can now see is that a Closure isn't so different from a function, it just gives us a little more freedom. A decent amount more, actually. I'll explain more below.

If we're allowed to grab variables from the enclosing scope (with a Closure) we have a comfier way of writing functions. Here's such an example using a hypothetical shopping cart:

let shopping_cart_total = 20;
let tax_amount = 5;
let total_after_tax = |pre_tax: i64| -> i64 { pre_tax  + tax_amount };

println!("{}", total_after_tax(shopping_cart_total)}; // would print: 25

See what we just did? We violated the rules of "normal" functions. We called on a variable which was defined in an outer scope without passing it into our function (closure). This is one of the increases in ergonomics that Rust closures allow us. How would we create a similar functionality with a normal, errr... function?

fn total_after_tax(tax_amount: i64, pre_tax_total: i64) -> i64 {
	tax_amount + pre_tax_total
}

Gross. We'd have to add another parameter. The gains in these particular examples are negligibly small, but the idea is there. What's nicer is that we don't even need to declare the types of either the passed-in parameter or the return value, it's "figured out" by the compiler:

let shopping_cart_total = 20;
let tax_amount = 5;

let total_after_tax = |pre_tax_amount|  pre_tax_amount  + tax_amount;

println!("{}", total_after_tax(shopping_cart_total)}; // would print: 25

... and a four-lane highway becomes a two-lane comfort cruise.

(1000 pointless internet points to the person who names that reference)

That's pretty cool, huh? It's almost as if we're writing in a scripting language. As with so many things in life, however, there is a catch. We are not spared from the authoritarianism of the Rust borrow checker. For instance,

fn main() {
  let shopping_cart_total = 20;
  let tax_amount = 5;

  let total_after_tax = |pre_tax_amount|  pre_tax_amount  + tax_amount;

  // now if we try to take a mutable reference to `tax_amount`...
  let some_new_tax = &mut tax_amount;
}

would result in a compiler error: cannot borrow mutably, because our closure has immutably borrowed the value for tax_amount and will continue to do so until it (the closure) goes out of scope. Unfortunately for this program that means until the program has ended execution due to the scope of total_after_tax being all of main. We can circumvent this by doing one small tweak and by introducing the move keyword:

fn main() {
  let shopping_cart_total = 20;
  let mut tax_amount = 5;

  // Note the `move` keyword
  let total_after_tax = move |pre_tax_amount|  pre_tax_amount  + tax_amount;

  // now if we try to take a mutable reference to `tax_amount`...
  let some_new_tax = &mut tax_amount;
}

I'm not going to discuss the mut keyword here. I think I touched on it previously.

The really important piece is the move keyword. What we're doing is telling our closure to take ownership of the bindings (values) it might use from its enclosing environment (tax_amount). Because primitive types are defined by Rust to already inherit the Copy trait the value, in this case 5, is copied into the closure leaving tax_amount reference-able.

It's important to remember to implement the Copy trait manually for any custom structs you might create yourself. You'll see the error regardless, but you might as well get ahead of the compiler and remember to have, say, struct Foo copyable so you can use a move closure as intended. See my previous post on traits if this concept is new to you.


Closures can also allow for more functional-styled programming in Rust and there's a lot more to their utility than what I've outlined above. I'm simply giving an intro and I hope that has been accomplished. In a future post I'd like to cover the more detailed aspects of them, but until then I hope you enjoyed falling down the rabbit hole.

Awesome links that helped me out:

comment

Comments