Dart Closures and Lexical Scoping


Closures and Lexical Scoping in Dart

In Dart, closures are functions that "remember" the environment in which they were created, even if they are executed outside of that environment. This feature is closely tied to lexical scoping, where variables are scoped based on where they are defined in the code, rather than where they are called from.

When a function (closure) is created within another function, it has access to the outer function’s variables even after the outer function has finished executing.


Lexical Scoping

Lexical scoping means that the scope of a variable is determined by its position in the source code. Variables are available in the function or block in which they are declared, and any inner functions (closures) have access to variables from their outer scope.

Here’s an example of lexical scoping:

void outerFunction() { var outerVar = "I am outside!"; void innerFunction() { print(outerVar); // Accessing outerVar from the outer scope } innerFunction(); } void main() { outerFunction(); }

Output:

I am outside!

In this example, innerFunction has access to outerVar, even though outerVar is defined in the outerFunction. This is an example of lexical scoping, where innerFunction can "see" variables from the scope it was defined in.


Closures in Dart

A closure occurs when a function is defined inside another function and captures the variables from the outer function's scope. The inner function "remembers" the values of the variables from its enclosing function, even if the outer function has finished executing. This is the essence of closures.

Let’s see an example of closures in Dart:

Function makeMultiplier(int multiplier) { return (int number) { return number * multiplier; }; } void main() { var multiplyBy2 = makeMultiplier(2); // The closure remembers the value of multiplier var multiplyBy3 = makeMultiplier(3); // The closure remembers the value of multiplier print(multiplyBy2(5)); // Output: 10 print(multiplyBy3(5)); // Output: 15 }

Output:

10 15

In this example:

  • makeMultiplier is a function that returns another function (a closure).
  • The returned function captures the multiplier value from the outer function and uses it when called.
  • multiplyBy2 remembers 2 as the multiplier, and multiplyBy3 remembers 3 as the multiplier.

Even though the makeMultiplier function finishes execution, the closures (multiplyBy2 and multiplyBy3) continue to have access to the multiplier variable because they "remember" it. This behavior is due to closures and lexical scoping.


Practical Use of Closures

Closures are commonly used in situations where you need to pass around a function that retains its context, such as for event handling, timers, and asynchronous programming.

Example with Timer (asynchronous closure):

void main() { var counter = 0; var timer = Timer.periodic(Duration(seconds: 1), (timer) { counter++; // The closure captures the `counter` variable print("Counter: $counter"); if (counter >= 5) { timer.cancel(); // Stop the timer after 5 seconds } }); }

Output (over 5 seconds):

Counter: 1 Counter: 2 Counter: 3 Counter: 4 Counter: 5

In this example:

  • The closure inside the Timer.periodic function keeps updating the counter variable every second.
  • The timer "remembers" the counter variable, and its state is updated on each timer tick. This is a closure in action, as the function inside the timer has access to variables from its surrounding scope (counter in this case).

Summary of Closures and Lexical Scoping in Dart

  1. Lexical scoping refers to how the scope of a variable is determined by where it is defined in the code (the location of the declaration, not where it is used).
  2. Closures are functions that can capture and remember variables from their enclosing scope, even if that scope is no longer active.
  3. Closures allow for functions to "remember" the context in which they were created, which is useful for things like callbacks, event handlers, and maintaining state in asynchronous operations.

Closures and lexical scoping are powerful features that can make your code more flexible and maintainable, allowing for more advanced patterns like function factories and maintaining state across function calls.