by kazinator | 2025-04-04 13:51:49+00:00
> In any programming language, when you capture values with a lambda/arrow function

It seems like just a few years ago that few programmers knew what these concepts are: mostly just the few that were exposed to Lisp or Scheme in college.

Now it's in "any language" and we have to be exposed to incorrect mansplaining about it from a C++ point of view.

> there are two ways to pass variables: By value (copy) or by reference (passing a pointer). Some languages, like C++, let you pick:

Lexical capture isn't "pass".

What this person doesn't understand is that C++ lambdas are fake lambdas, which do not implement environment capture. C++ lambdas will not furnish you with a correct understanding of lambdas.

(Furthermore, no language should ever imitate what C++ has done to lambdas.)

Capture isn't the passage of arguments to parameters, which can be call by value or reference, etc. Capture is a retention of the environment of bindings, itself.

The issue here is simply that

1. The Javascript lambda is correctly implementing lexical capture.

2. The Javascript loop is not creating a fresh binding for the loop variable in each iteration. It binds one variable, and mutates its value.

Mutating the value is the correct thing to do for a loop construct which lets the program separately express initialization, guard testing and increment. The semantics of such a loop requires that the next iteration's guard have access to the previous iteration's value. We can still have hacks under the hood so that a lexical closure will capture a different variable in each iteration, but it's not worth it, and the program can do that itself. Javascript is doing the right thing here, and it cannot just be fixed. In any case, vast numbers of programs depend on the variable i being a single instance that is created and initialized once and then survives from one iteration to the next.

Now lambdas can in fact be implemented by copy. Compilers for languages with lambda can take various strategies for representing the environment and how it is handled under capture. One possible mechanism is conversion to a flattened environment vector, whereby every new lambda gets a new copy of such a vector.

The entire nested lexical scope group becomes one object in which every variable has a fixed offset that the compiled code can refer to. You then have to treat individual variables in that flat environment according to whether any given variable is shared, mutated or both.

The worst case is when multiple closures capture the same variable (it is shared) and the variable is mutated such that one closure changes it and another one must see the change. This is the situation with the loop index i variable in the JS loop. This means that under a flat, copied environment strategy, the variable will have to be implemented as a reference cell in the environment vector. Variables which are not mutated can just be values in the vector. Variables which are mutated, but not shared among closures, likewise.

This is all under the hood though; there are no programmer-visible annotations for indicating how to treat each captured variable. It always looks as if the entire environment at the point of capture is being taken by reference. The compiler generates reference semantics for those variables which need it.

At the implementation level, with particular strategies for handling environments under lambda, we can think about capturing references or value copies. C++ lambdas imitate this sort of implementation-level thinking and define the language construct around it, in a way that avoids the master concept of capturing the environment.