Dart 13 🎯 First-Class Functions, Higher-Order Functions, and Anonymous Closures
In Dart, functions are not a special syntactic category. They are objects. A function can be stored in a variable, passed as an argument, returned from another function, and composed with other functions. This property — being first-class — is what makes Dart’s functional programming features possible. The collection methods map, where, reduce, and fold take functions as arguments. Callbacks, event handlers, and strategies are all functions passed around as values.
A higher-order function is any function that takes a function as an argument or returns a function. The term describes the role, not a special syntax. List.map is a higher-order function because it takes a function and applies it to each element. makeCounter is a higher-order function because it returns a function. Dart’s standard library is full of higher-order functions, and writing them is part of everyday Dart programming.
An anonymous closure is a function without a name, defined inline where it is needed. It captures variables from the enclosing scope, which makes it possible to write callbacks that remember state without threading that state through parameters. The combination of first-class functions, higher-order functions, and closures is what allows Dart code to express transformation and behavior as data.
This chapter covers functions as values, function types, passing and returning functions, anonymous functions and closures, the capture semantics that make closures powerful and occasionally surprising, and the patterns that use higher-order functions idiomatically.
Key point: Functions are objects in Dart. They can be assigned to variables, passed as arguments, and returned from other functions. A higher-order function takes a function or returns one. An anonymous closure is an inline function that captures variables from its enclosing scope. The collection methods map, where, reduce, and fold are higher-order functions that take closures as arguments.
Why first-class functions matter
The composition problem. Code that is organized around values can be composed. Functions that are values can be composed too: a filter function that takes a predicate can be combined with a map function that takes a transformation, and the result is a pipeline. Without first-class functions, each combination would require its own loop.
The callback problem. Asynchronous operations, event handlers, and sorting comparators all need a way to specify “what to do when this happens” without knowing when it will happen. Passing a function as an argument is the mechanism.
The strategy problem. Some algorithms have a fixed structure but vary in one part: a sort order, a comparison, a transformation. Passing that part as a function makes the algorithm reusable without duplication.
The encapsulation problem. A closure captures variables from its scope, which lets a function remember state without exposing that state. A counter that returns a function incrementing a private variable is the canonical example.
The expressiveness problem. A for loop says how to iterate. A map call says what the result should be. When the operation is a transformation or a filter, the higher-order form is more direct and less error-prone.
a. Functions as values
A function can be assigned to a variable, and the variable can be called like the function.
int add(int a, int b) => a + b;
void main() {
var operation = add;
print(operation(2, 3)); // 5
}
The type of the variable is a function type. Dart infers it from the assignment, but it can be written explicitly.
int Function(int, int) operation = add;
The function type has the parameter types in parentheses and the return type before the Function keyword. A function that takes two ints and returns an int has the type int Function(int, int).
void Function() noArgs; // takes nothing, returns nothing
String Function(String) transform; // takes a string, returns a string
bool Function(int) predicate; // takes an int, returns a bool
List<int> Function(List<int>) process; // takes a list, returns a list
The Function type without parameters is the general type for any function. It is used when the signature is unknown or when the function will be cast later.
Function anyFunction = add;
Using the general type loses the ability to call the function without casting, so it is better to use the specific type when it is known.
b. Passing functions as arguments
A function that takes another function as a parameter is a higher-order function. The parameter is declared with a function type.
void apply(int a, int b, int Function(int, int) operation) {
print(operation(a, b));
}
void main() {
apply(2, 3, add); // 5
apply(2, 3, (a, b) => a * b); // 6
}
The second call passes an anonymous function directly. The type is inferred from the parameter’s type, so the parameter types in the anonymous function can be omitted.
apply(2, 3, (a, b) => a * b);
The List methods are higher-order functions:
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map((n) => n * 2).toList();
var evens = numbers.where((n) => n % 2 == 0).toList();
var sum = numbers.reduce((a, b) => a + b);
Each method takes a function and applies it in a specific way. map transforms each element, where filters, and reduce combines.
Sorting with a comparator is another common case:
var people = ['Alice', 'bob', 'Carol'];
people.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
The comparator is a function that returns a negative number, zero, or a positive number depending on the order of the two arguments.
c. Returning functions
A function can return another function. This is the other form of a higher-order function.
int Function(int) makeMultiplier(int factor) {
return (int x) => x * factor;
}
void main() {
var double = makeMultiplier(2);
var triple = makeMultiplier(3);
print(double(5)); // 10
print(triple(5)); // 15
}
The returned function captures the factor parameter. Each call to makeMultiplier creates a new closure with its own factor. The two multipliers are independent.
A function that returns a function can be used to build configurable behavior:
bool Function(String) makeValidator(int minLength) {
return (String value) => value.length >= minLength;
}
var isValidUsername = makeValidator(3);
var isValidPassword = makeValidator(8);
print(isValidUsername('ab')); // false
print(isValidPassword('secret123')); // true
The validator captures the minimum length and applies it. The caller does not need to pass the length on each call.
d. Anonymous functions and closures
An anonymous function is a function without a name. It is defined inline where it is needed.
var square = (int x) => x * x;
print(square(5)); // 25
var greet = (String name) {
print('Hello, $name!');
};
greet('Alice');
An anonymous function can be passed directly as an argument:
var numbers = [1, 2, 3, 4, 5];
var evens = numbers.where((n) => n % 2 == 0).toList();
A closure is an anonymous function that captures variables from its enclosing scope. The captured variables persist as long as the closure exists.
Function makeCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3
}
The closure captures count by reference, not by value. Each call to counter increments the same count variable. The variable is private to the closure; no other code can access it.
A second call to makeCounter produces a new closure with its own count:
var counter1 = makeCounter();
var counter2 = makeCounter();
print(counter1()); // 1
print(counter1()); // 2
print(counter2()); // 1 (independent)
Each closure has its own captured state.
e. Capture semantics
Closures capture variables, not values. This distinction matters when the captured variable changes after the closure is created.
void main() {
var message = 'Hello';
var greet = () => print(message);
message = 'Goodbye';
greet(); // Goodbye — the closure sees the current value
}
The closure captures the variable message, and when it runs, it reads the variable’s current value. Changing the variable after the closure is created changes what the closure sees.
This behavior is usually what is wanted, but it can be surprising in loops.
void main() {
var callbacks = <Function>[];
for (var i = 0; i < 3; i++) {
callbacks.add(() => print(i));
}
for (var callback in callbacks) {
callback(); // 0, 1, 2
}
}
In Dart, the loop variable i is scoped to each iteration, so each closure captures a different i. The output is 0, 1, 2. This is different from languages where the loop variable is shared across iterations and all closures capture the same variable, producing 3, 3, 3.
If the closure needs to capture the value at the time of creation, assign it to a local variable inside the loop:
for (var i = 0; i < 3; i++) {
var captured = i;
callbacks.add(() => print(captured));
}
This is rarely necessary in Dart because of the per-iteration scoping, but it is a useful technique to know.
f. Higher-order functions in the standard library
Dart’s standard library provides many higher-order functions. The most common are on Iterable and List.
| Method | Purpose | Signature |
|---|---|---|
map | Transform each element | Iterable<R> map<R>(R Function(T) f) |
where | Filter elements | Iterable<T> where(bool Function(T) f) |
reduce | Combine to one value | T reduce(T Function(T, T) f) |
fold | Combine with initial value | R fold<R>(R initial, R Function(R, T) f) |
any | True if any element matches | bool any(bool Function(T) f) |
every | True if all elements match | bool every(bool Function(T) f) |
forEach | Apply function to each | void forEach(void Function(T) f) |
sort | Sort with comparator | void sort([int Function(T, T)? f]) |
The methods can be chained:
var result = numbers
.where((n) => n % 2 == 0)
.map((n) => n * n)
.reduce((a, b) => a + b);
This pipeline filters, transforms, and aggregates in one expression. It is equivalent to a loop but expresses the intent more directly.
The fold method is the general form of reduce. It takes an initial value and a combining function:
var sum = numbers.fold<int>(0, (acc, n) => acc + n);
var product = numbers.fold<int>(1, (acc, n) => acc * n);
The type parameter on fold specifies the accumulator type, which can differ from the element type.
Complete Example Session
// ============================================
// PART 1: FUNCTION AS VALUE
// ============================================
int add(int a, int b) => a + b;
void main() {
var operation = add;
print(operation(2, 3)); // 5
}
// ============================================
// PART 2: EXPLICIT FUNCTION TYPE
// ============================================
void main() {
int Function(int, int) operation = (a, b) => a + b;
print(operation(2, 3)); // 5
}
// ============================================
// PART 3: PASSING A FUNCTION
// ============================================
void apply(int a, int b, int Function(int, int) op) {
print(op(a, b));
}
void main() {
apply(2, 3, (a, b) => a + b); // 5
apply(2, 3, (a, b) => a * b); // 6
}
// ============================================
// PART 4: RETURNING A FUNCTION
// ============================================
int Function(int) makeMultiplier(int factor) {
return (int x) => x * factor;
}
void main() {
var double = makeMultiplier(2);
var triple = makeMultiplier(3);
print(double(5)); // 10
print(triple(5)); // 15
}
// ============================================
// PART 5: ANONYMOUS FUNCTION
// ============================================
void main() {
var square = (int x) => x * x;
print(square(5)); // 25
var greet = (String name) => print('Hello, $name!');
greet('Alice');
}
// ============================================
// PART 6: CLOSURE COUNTER
// ============================================
Function makeCounter() {
var count = 0;
return () => ++count;
}
void main() {
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
}
// ============================================
// PART 7: INDEPENDENT CLOSURES
// ============================================
void main() {
var counter1 = makeCounter();
var counter2 = makeCounter();
print(counter1()); // 1
print(counter1()); // 2
print(counter2()); // 1
}
// ============================================
// PART 8: CAPTURE BY REFERENCE
// ============================================
void main() {
var message = 'Hello';
var greet = () => print(message);
message = 'Goodbye';
greet(); // Goodbye
}
// ============================================
// PART 9: CLOSURES IN LOOPS
// ============================================
void main() {
var callbacks = <Function>[];
for (var i = 0; i < 3; i++) {
callbacks.add(() => print(i));
}
for (var callback in callbacks) {
callback(); // 0, 1, 2
}
}
// ============================================
// PART 10: CHAINED HIGHER-ORDER FUNCTIONS
// ============================================
void main() {
var numbers = [1, 2, 3, 4, 5, 6];
var result = numbers
.where((n) => n % 2 == 0)
.map((n) => n * n)
.fold<int>(0, (acc, n) => acc + n);
print(result); // 4 + 16 + 36 = 56
}
These ten parts cover functions as values, explicit function types, passing functions, returning functions, anonymous functions, closures, independent closures, capture by reference, closures in loops, and chained higher-order functions.
Quick Reference
Function Types
| Type | Meaning |
|---|---|
void Function() | No parameters, no return |
int Function(int, int) | Two ints, returns int |
String Function(String) | One string, returns string |
bool Function(int) | One int, returns bool |
Function | Any function |
int Function(int)? | Nullable function |
Higher-Order Methods
| Method | Purpose |
|---|---|
map | Transform each element |
where | Filter elements |
reduce | Combine to one value |
fold | Combine with initial value |
any | Any element matches |
every | All elements match |
forEach | Apply to each element |
sort | Sort with comparator |
Anonymous Function Forms
| Form | Example |
|---|---|
| Arrow | (int x) => x * x |
| Block | (int x) { return x * x; } |
| No parameters | () => print('hi') |
| Assigned | var f = (int x) => x * 2; |
| Passed inline | list.where((n) => n > 0) |
Closure Capture
| Behavior | Detail |
|---|---|
| Captures variables | Not values |
| Sees current value | When the closure runs |
| Persists state | As long as the closure exists |
| Per-iteration in loops | Dart scopes the loop variable |
Best Practices
✅ Do This:
// Use function types explicitly
int Function(int, int) op = (a, b) => a + b;
// Chain collection methods
var result = list.where((n) => n > 0).map((n) => n * 2).toList();
// Return functions for configurability
var double = makeMultiplier(2);
// Use closures for private state
var counter = makeCounter();
❌ Don’t Do This:
// Using Function type when specific type is known
Function op = (int a, int b) => a + b; // ❌ loses type info
// Reimplementing map with a loop
var doubled = []; // ❌ use map
for (var n in numbers) doubled.add(n * 2);
// Capturing loop variable when value is needed
callbacks.add(() => print(i)); // ⚠️ works in Dart, but be aware
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Type inference failure | Ambiguous closure parameters | Add explicit types |
| Closure sees unexpected value | Captures by reference | Capture a copy if needed |
Function type loses info | Used the general type | Use the specific function type |
fold type mismatch | Accumulator type differs | Specify the type parameter |
reduce on empty list | No initial value | Use fold with a default |
| Comparator wrong order | Return value inverted | Negative for less than |
Real-World Examples
1. Function Variable
var operation = (int a, int b) => a + b;
2. Predicate
bool Function(int) isEven = (n) => n % 2 == 0;
3. Transformer
var upper = (String s) => s.toUpperCase();
4. Multiplier Factory
int Function(int) makeMultiplier(int factor) => (x) => x * factor;
5. Validator Factory
bool Function(String) minLength(int n) => (s) => s.length >= n;
6. Counter Closure
Function counter() {
var count = 0;
return () => ++count;
}
7. Map and Filter
var evens = numbers.where((n) => n % 2 == 0).toList();
var doubled = numbers.map((n) => n * 2).toList();
8. Fold with Type
var sum = numbers.fold<int>(0, (acc, n) => acc + n);
9. Sort with Comparator
people.sort((a, b) => a.age.compareTo(b.age));
10. Chained Pipeline
var result = items
.where((i) => i.active)
.map((i) => i.name)
.toList();
Visual
Functions as Values
┌──────────────────────────────────────────────────────────────┐
│ int add(int a, int b) => a + b; │
│ │ │
│ ▼ │
│ var operation = add; │
│ └── operation is a reference to the function │
│ │
│ operation(2, 3); // 5 │
│ └── Called through the variable │
│ │
│ int Function(int, int) op = add; │
│ └── Explicit function type │
└──────────────────────────────────────────────────────────────┘
Higher-Order Functions
┌──────────────────────────────────────────────────────────────┐
│ TAKES a function: │
│ void apply(int a, int b, int Function(int, int) op) { │
│ print(op(a, b)); │
│ } │
│ │
│ RETURNS a function: │
│ int Function(int) makeMultiplier(int factor) { │
│ return (x) => x * factor; │
│ } │
│ │
│ Both are higher-order functions. │
└──────────────────────────────────────────────────────────────┘
Closure Capture
┌──────────────────────────────────────────────────────────────┐
│ Function makeCounter() { │
│ var count = 0; ← captured variable │
│ return () { │
│ count++; │
│ return count; │
│ }; │
│ } │
│ │
│ var counter = makeCounter(); │
│ └── count persists between calls │
│ │
│ counter(); // 1 │
│ counter(); // 2 │
│ counter(); // 3 │
│ │
│ var other = makeCounter(); │
│ └── independent count │
└──────────────────────────────────────────────────────────────┘
Chained Pipeline
┌──────────────────────────────────────────────────────────────┐
│ [1, 2, 3, 4, 5, 6] │
│ │ │
│ ▼ where((n) => n % 2 == 0) │
│ [2, 4, 6] │
│ │ │
│ ▼ map((n) => n * n) │
│ [4, 16, 36] │
│ │ │
│ ▼ fold(0, (acc, n) => acc + n) │
│ 56 │
│ │
│ Each step is a higher-order function taking a closure. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| First-class functions | Functions are objects |
| Function type | int Function(int, int) |
| Higher-order function | Takes or returns a function |
| Anonymous function | Function without a name |
| Closure | Anonymous function that captures scope |
| Capture semantics | Captures variables by reference |
map | Transform elements |
where | Filter elements |
reduce | Combine to one value |
fold | Combine with initial value |
sort | Sort with comparator |
| Loop variable scoping | Per-iteration in Dart |
Key takeaways:
- Functions are objects in Dart. They can be assigned to variables, passed as arguments, returned from functions, and stored in collections. The type is written as
ReturnType Function(ParamTypes). - A higher-order function takes a function or returns one.
List.mapis a higher-order function;makeMultiplieris a higher-order function. The term describes the role, not a special syntax. - Anonymous functions are defined inline. They are written with the same arrow or block syntax as named functions, without a name. They are passed directly to higher-order functions.
- Closures capture variables by reference. The captured variable persists as long as the closure exists, and the closure sees the variable’s current value when it runs. This is what makes closures stateful.
- Each closure has its own captured state. Calling
makeCountertwice produces two independent closures, each with its owncount. - Dart scopes loop variables per iteration. Closures created in a loop capture different values for each iteration, which differs from languages where the loop variable is shared.
- The collection methods
map,where,reduce, andfoldare higher-order functions. They take closures as arguments and express transformation, filtering, and aggregation more directly than explicit loops. - Use
foldwhenreduceis not appropriate.reduceuses the first element as the initial value and fails on an empty list.foldtakes an explicit initial value and a type parameter.
Remember: First-class functions are the foundation of Dart’s functional style. Because functions are values, they can be passed and returned, which enables callbacks, strategies, and the collection methods that Dart uses heavily. Higher-order functions are functions that work with other functions, and they are the mechanism for composition. Closures capture variables from their enclosing scope, which gives them memory and makes them useful for stateful callbacks and private state. Understanding capture semantics — variables by reference, per-iteration scoping in loops — prevents the surprising behaviors that closures can produce. With these features, Dart code can express transformation and behavior as data, which makes it more concise and more composable than code built only from loops and named functions.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!