Lambdas are Restricted "Closures"

A "closure" is an anonymous procedure that is able to capture long-lived references to the variables defined outside the body of the lambda, importantly, keeping that reference even as the lambda itself leaves the scope. This is exactly how Python or Java lambdas work, for example.

Unfortunately, this leads to hard-to-understand code as you end up with "spooky action at a distance" where calling a lambda can cause some faraway data to be changed without necessarily realizing or intending for that to be happening. This would be fatal for Claro's more advanced "Fearless Concurrency" goal, because it represents hidden mutable state which would invalidate Claro's goals of guaranteeing that multithreaded code unable to run into data races.

So, to address these issues, when lambdas reference variables in outer scopes, the variables are captured as a local copy of the current value referenced by that variable. Claro's Lambdas have no mechanism to mutate anything not passed in as an explicit argument, and they cannot carry any mutable state.

Read more about how Claro prevents data-races here.

Static Validation

Claro will statically validate that lambdas don't violate the above restrictions:

Fig 1:


var i = 0;
var f = (x: int) -> int {
    i = x + i; # `i` is captured, and illegally updated.
    return i;
};

Compilation Errors:

lambda_closures_EX1_example.claro:3: Illegal Mutation of Lambda Captured Variable: The value of all variables captured within a lambda context are final and may not be changed. This restriction ensures that lambdas do not lead to so-called "spooky action at a distance" and is essential to guaranteeing that Graph Procedures are data-race free by construction.
    i = x + i; # `i` is captured, and illegally updated.
    ^
Warning! The following declared symbols are unused! [f]
2 Errors

Captured Variables "Shadow" Variables in the Outer Scope

When a lambda captures a variable from the outer scope, the captured variable inside the lambda is effectively completely independent from the original variable in the outer scope. It simply "shadows" the name of the outer scope variable. In this way, lambdas are guaranteed to be safe to call in any threading context as thread-related ordering alone can't affect the value returned by the lambda:

Fig 2:


var i = 1;
var f = (x: int) -> int {
    # This lambda captures a **copy** of the variable `i`.
    return x + i;
};

for (x in [0, 1, 5, 5]) {
  print("i:    {i}");
  print("f({x}): {f(x)}\n");
  i = i * 10;  # <-- This update of `i` is not observed by the lambda.
}

Output:

i:    1
f(0): 1

i:    10
f(1): 2

i:    100
f(5): 6

i:    1000
f(5): 6

Manually Emulating Traditional "Closures"

While Claro's design decisions around Lambdas make sense in the name of enabling "Fearless Concurrency", the restrictions may seem like they prevent certain design patterns that may be completely valid when used carefully in a single-threaded context. But worry not! You can of course implement "closure" semantics yourself (albeit in a more C++ style with explicit variable captures).

Fig 3:


newtype ClosureFn<State, Out> : struct {
  state: State,
  fn: function<State -> Out>
}

function callClosure<State, Out>(closureFn: ClosureFn<State, Out>) -> Out {
  var toApply = unwrap(closureFn).fn;
  return toApply(unwrap(closureFn).state);
}

var mutList = mut [1];
var getAndIncrClosure: ClosureFn<mut [int], int> =
  ClosureFn({
    state = mutList,
    fn = l -> {
      var res = l[0];
      l[0] = l[0] + 1; # Update the "captured" state.
      return res;
    }
  });

print(mutList);                        # mut [1]
print(callClosure(getAndIncrClosure)); # 1
print(callClosure(getAndIncrClosure)); # 2
print(callClosure(getAndIncrClosure)); # 3
# "Spooky Action at a Distance" mutating `mutList` on the lines above when
# `mutList` was never directly referenced.
print(mutList);                        # mut [4]

Output:

mut [1]
1
2
3
mut [4]

Note: The beauty of this design is that even though Claro doesn't prevent you from emulating traditional "closures" on your own if you so chose, Claro can still statically identify that this ClosureFn<State, Out> type is unsafe for multithreaded contexts and will be able to prevent you from using this to create a data race!