BattlefyBlogHistoryOpen menu
Close menuHistory

Closures are weird

Ronald Chen November 28th 2022

We often call arrow functions “closures”, but that is a misnomer. Arrow functions can be closures, but not always. A normal function can be a closure too!

let a = 1;

const closureArrowFunction = () => {
  a += 1;
};

const nonClosureArrowFunction = () => {
  return 42;
}

function closureFunction() {
  a += 1;
}

function nonClosureFunction() {
  return 42;
}

A closure closes over syntactic scope. closureArrowFunction and closureFunction close over a.

Closures are weird because they can close over local variables.

let a = 1;

const foo = () => {
  let b = 2;

  const bar = () => {
    a += 1;
    b += 2;
    return a + b;
  };
  return bar;
};

const bar = foo();
console.log(a); // prints 1
console.log(bar()); // prints 6
console.log(a); // prints 2

The global variable a is pretty simple. Any function can access it. a exists for the entire lifecycle of the program. But what about b?

What is the lifecycle of the local variable b? Naively it may seem like b should exist for as long as the invocation of foo, but the bar closure uses b. If b is removed after the call to foo, bar wouldn't be able to change b.

The answer is b exists until all references to it are garbage collected. This means it is not possible to implement closures without a garbage collector.

Isn’t that weird? Something as simple as closures requires a full-on garbage collector.

Closures are intrinsic to the event loop

Async/await (which are just promises) and callbacks are required to work with the event loop.

Callbacks are just closures and the promise API use closures.

// callback
setTimeout(someClosure, 1000);

// promise
promise
  .then(someClosure)
  .catch(someErrorClosure);

// async/await
try {
  const result = await promise;
  someClosure(result);
} catch (error) {
  someErrorClosure(error);
}  

But what about languages that don’t have closures? Surely function pointers are a sufficient alternative.

Function pointers are not closures

Function pointers in systems programming languages like Zig may seem like closures as one can pass function pointers just like closures.

fn setTimeout(comptime callback: fn () void, nanoseconds: u64) void {
  // this is pointless, as this blocks the current thread
  // unlike Javascript’s setTimeout
  std.time.sleep(nanoseconds);
  callback();
}

fn printHello() void {
  std.debug.print("Hello\n", .{});
}

pub fn main() void {
  setTimeout(printHello, 1_000_000_000);
}

But there is no way for function pointers to close over local variables.

fn setTimeout(comptime callback: fn () void, nanoseconds: u64) void {
  // this is pointless, as this blocks the current thread
  // unlike Javascript’s setTimeout
  std.time.sleep(nanoseconds);
  callback();
}

fn printHello(const name: []const u8) void {
  std.debug.print("Hello {s}\n", .{name});
}

pub fn main() void {
  const name = "Bob";

  // no way to pass name into printHello without using a global
  setTimeout(printHello, 1_000_000_000); // compiler error, setTimeout expects a function with no params
}

This is why for function pointers, there is often a context object passed to the callback.

fn setTimeout(callback: fn (context: anytype) void, context: anytype, nanoseconds: u64) void {
  // this is pointless, as this blocks the current thread
  // unlike Javascript’s setTimeout
  std.time.sleep(nanoseconds);
  callback(context);
}

fn printHello(context: anytype) void {
  const name: []const u8 = context.name;
  std.debug.print("Hello {s}\n", .{name});
}

pub fn main() void {
  const context = .{
    .name = "Bob",
  };

  setTimeout(printHello, context, 1_000_000_000);
}

Aside: This is a contrived example; one wouldn’t use a function pointer for this use case.

Do you dream in closures? We’d like to hear from you, Battlefy is hiring.

2024

  • MLBB Custom Lobby Feature
    Stefan Wilson - April 16th 2024

2023

2022

Powered by
BATTLEFY