At rwx, we’re investing some of our engineering resources towards development of the Roc language project. Roc is a new, pre-alpha purely-functional language that offers a trove of new developer productivity features. We won’t get into all of them today, but we’d like to tell you about how Roc compiles closures with excellent runtime performance characteristics. One of Roc’s goals is to produce performant binaries when they’re needed, and how closures are represented at runtime is an important part of the design space.

You might know closures as “lambdas” or “function objects” from other languages. Roc takes an approach to closure compilation that differs from most other language compilers by applying a technique know as defunctionalization at the type-system level. We’ll discuss what all that means below!

This is the first part of a long story about compiling closures; in the future, we’ll describe a novel mechanism for compiling Roc-style closures in the presence of ad-hoc polymorphism like Rust’s traits, C++ concepts, or Go’s interfaces$^{10}$.

<aside> 🛠 rwx is building products to improve engineering productivity for large-scale software projects. If you’re interested in tackling problems in the developer experience space, we’re hiring!

</aside>

Defining Closures

There are many different names for the concept we’ll be talking about today. To be precise, I’d like to talk about a language feature that allows you to write functions that can

  1. be constructed on-the-fly
  2. capture values from the lexical scope they’re constructed in
  3. be used as values in the language, e.g. passed around to other functions
  4. conditionally be used in place of other functions of the same shape$^1$

Here’s a concrete description of closures satisfying the above criteria in Python, C++, and Roc. Each snippet defines a function that takes an integer N, and conditionally returns a closure that takes an integer M and increases M by N via some “strategy” - either addition or multiplication.

def increase_with(n, increase_op):
  if increase_op == "add":
    return lambda m: m + n
  else:
    assert increase_op == "mul"
    return lambda m: m * n
#include <functional>

enum IncreaseOp { add, mul };

std::function<int (int)> increase_with(int n, IncreaseOp increase_op) {
    if (increase_op == IncreaseOp::add) {
        return [n](int m) { return m + n; };
    } else {
        return [n](int m) { return m * n; };
    }
}

increaseWith = \\n, increaseOp ->
  when increaseOp is
	  Add -> \\m -> m + n
	  Mul -> \\m -> m * n

Closure Conversion

The Idea

Compiling closures doesn’t have to be tricky, but making their runtime representation efficient may take some care. If we think about a non-capturing function (think of a top-level function exported by a library in any language), calling it is determined by two things:

  1. The code of the function
  2. The parameters passed to the function at the call site