This content originally appeared on DEV Community and was authored by Ge Ji
Today, we’ll dive into advanced function features — higher-order functions and closures. These are core concepts in Dart’s functional programming paradigm, making code more concise and flexible, and are essential techniques in Flutter development.
I. Higher-Order Functions: Functions that Work with Other Functions
A higher-order function is a function that can accept other functions as parameters or return a function as a result. Simply put, it’s a function that “operates” on other functions.
1. Functions as Parameters
When functions are passed as parameters, we can abstract common logic, making functions more extensible.
Example: Generic Calculation Framework
// Higher-order function: accepts two numbers and an operation function
void calculate(int a, int b, int Function(int, int) operation) {
int result = operation(a, b);
print("Calculation result: $result");
}
// Define specific specific operation functions
int add(int x, int y) => x + y;
int multiply(int x, int y) => x * y;
void main() {
// Pass the addition function
calculate(3, 4, add); // Output: Calculation result: 7
// Pass the multiplication function (or directly pass an anonymous function)
calculate(3, 4, multiply); // Output: Calculation result: 12
calculate(3, 4, (x, y) => x - y); // Output: Calculation result: -1
}
In this example, calculate is a higher-order function. It doesn’t care about the specific operation logic, only about executing the passed operation function. This pattern is common in framework design.
2. Functions as Return Values
When functions return other functions, we can dynamically generate functions with specific behaviors.
Example: Generating Custom Calculators
// Higher-order function: returns corresponding operation function based on operator
Function getCalculator(String operator) {
switch (operator) {
case "+":
return (int a, int b) => a + b;
case "-":
return (int a, int b) => a - b;
case "*":
return (int a, int b) => a * b;
case "/":
return (int a, int b) => a ~/ b; // Integer division
default:
return (int a, int b) => 0;
}
}
void main() {
// Get an addition calculator
var adder = getCalculator("+");
print(adder(5, 3)); // Output: 8
// Get a multiplication calculator
var multiplier = getCalculator("*");
print(multiplier(5, 3)); // Output: 15
}
Here, getCalculator dynamically returns different operation functions based on the input operator, achieving the effect of “function on demand”.
II. Common Higher-Order Functions: forEach/map/reduce
Dart’s collection classes (List, Set, etc.) have many built-in higher-order functions that simplify data processing.
1. forEach: Iterating Over Collections
forEach accepts a function parameter and iterates over each element in the collection (which we touched on in the previous lesson).
void main() {
List<String> languages = ['Dart', 'Flutter', 'Java'];
// Iterate and print each element
languages.forEach((lang) {
print("Language: $lang");
});
// Output:
// Language: Dart
// Language: Flutter
// Language: Java
}
2. map: Transforming Collection Elements
map accepts a transformation function, converts each element in the collection according to rules, and returns a new iterable (usually converted to a list with toList()).
Example: Data Transformation
void main() {
List<int> numbers = [1, 2, 3, 4];
// Convert each number to its square
var squared = numbers.map((n) => n * n);
print(squared.toList()); // Output: [1, 4, 9, 16]
// Convert integer list to string list
var numberStrings = numbers.map((n) => "Number: $n");
print(
numberStrings.toList(),
); // Output: [Number: 1, Number: 2, Number: 3, Number: 4]
}
3. reduce: Aggregating Collection Elements
reduce accepts an aggregation function and combines elements in the collection into a single result (starting with the first element and progressively combining with the next).
Example: Summing and Multiplying
void main() {
List<int> numbers = [1, 2, 3, 4, 5];
// Sum: ((((1+2)+3)+4)+5) = 15
int sum = numbers.reduce((value, element) => value + element);
print("Sum: $sum"); // Output: Sum: 15
// Product: ((((1*2)*3)*4)*5) = 120
int product = numbers.reduce((value, element) => value * element);
print("Product: $product"); // Output: Product: 120
}
4. Chaining: Combining Higher-Order Functions
These higher-order functions can be chained together to implement complex data processing logic:
void main() {
List<int> scores = [85, 92, 78, 65, 90];
// Step 1: Filter scores greater than 80
// Step 2: Convert scores to strings like "Excellent: XX"
// Step 3: Iterate and print results
scores
.where((s) => s > 80) // Filter: [85, 92, 90]
.map(
(s) => "Excellent: $s",
) // Transform: ["Excellent: 85", "Excellent: 92", "Excellent: 90"]
.forEach((s) => print(s)); // Iterate and print
// Output:
// Excellent: 85
// Excellent: 92
// Excellent: 90
}
This chaining style is very concise, avoiding verbose loops and temporary variables.
III. Closures: “Binding” Between Functions and Variables
A closure is a structure formed when an inner function in a nested function references variables from the outer function, and the inner function is returned or passed outside. Simply put, it’s a function that “remembers” the environment in which it was created.
1. Basic Form of a Closure
// Outer function
Function makeCounter() {
int count = 0; // Local variable of outer function
// Inner function (closure): references outer count variable
int increment() {
count++;
return count;
}
return increment; // Return inner function
}
void main() {
// Get closure function
var counter = makeCounter();
// Calling multiple times shows count is "remembered"
print(counter()); // Output: 1
print(counter()); // Output: 2
print(counter()); // Output: 3
}
In this example, the increment function is a closure. It references the count variable from the outer function makeCounter. Even after makeCounter finishes executing, count isn’t destroyed but is “preserved” by the increment function.
2. Core Feature of Closures: Variable Isolation
Each closure maintains its own independent variable environment, without interference:
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
void main() {
var counter1 = makeCounter();
var counter2 = makeCounter();
print(counter1()); // 1 (counter1's count)
print(counter1()); // 2
print(counter2()); // 1 (counter2's count, independent of counter1)
print(counter2()); // 2
}
counter1 and counter2 are two independent closures with their own count variables that don’t affect each other, achieving variable isolation.
3. Practical Use Cases for Closures
- Encapsulating private variables: Dart doesn’t have a private keyword, but closures can simulate private variables.
- Caching calculation results: Storing results of expensive calculations to avoid redundant computations.
- Event handling: Preserving context information in callback functions.
Example: Simulating Private Variables
class User {
// Implement "private" password variable with closure
final _password = (() {
String _secret = "123456"; // Not directly accessible from outside
return {'get': () => _secret, 'set': (String newPwd) => _secret = newPwd};
})();
String name;
User(this.name);
// Only access "private" password through methods
String getPassword() => _password['get']!();
void setPassword(String newPwd) => _password['set']!(newPwd);
}
void main() {
User user = User("Zhang San");
print(user.getPassword()); // Output: 123456
user.setPassword("abcdef");
print(user.getPassword()); // Output: abcdef
// Cannot directly access _secret (compilation error)
// print(user._secret);
}
IV. Comprehensive Example with Higher-Order Functions and Closures
Let’s use a “shopping cart calculation” example to integrate what we’ve learned:
void main() {
// Shopping cart items: [name, price, quantity]
List<List<dynamic>> cart = [
['Apple', 5.99, 2],
['Banana', 3.99, 3],
['Orange', 4.50, 1],
];
// 1. Calculate total price for each item (price * quantity) - using map
var itemTotals = cart.map((item) => item[1] * item[2]);
// 2. Calculate total cart price - using reduce
double total = itemTotals.reduce((sum, price) => sum + price);
// 3. Create a price calculator with discount (closure)
Function createDiscountCalculator(double discount) {
return (double price) => price * (1 - discount);
}
// 4. Calculate final price with 10% discount
var discount90 = createDiscountCalculator(0.1);
double finalTotal = discount90(total);
print(
"Original price: ${total.toStringAsFixed(2)}",
); // Output: Original price: 28.45
print(
"After 10% discount: ${finalTotal.toStringAsFixed(2)}",
); // Output: After 10% discount: 25.61
}
In this example, we used map for data transformation, reduce for aggregation, and a closure to create a stateful discount calculator, demonstrating the flexibility of higher-order functions and closures.
This content originally appeared on DEV Community and was authored by Ge Ji