Functional Programming: Higher-Order Functions
Functions as First-Class Citizens
In Dart, functions are first-class objects. This means functions can be assigned to variables, passed as arguments to other functions, returned from functions, and stored in data structures -- just like any other value. This is the foundation of functional programming in Dart.
Functions Are Objects
void main() {
// Assign a function to a variable
final greet = (String name) => 'Hello, $name!';
print(greet('Edrees')); // Hello, Edrees!
// Store functions in a list
final operations = <int Function(int, int)>[
(a, b) => a + b,
(a, b) => a - b,
(a, b) => a * b,
];
for (final op in operations) {
print(op(10, 3)); // 13, 7, 30
}
// Store functions in a map
final validators = <String, bool Function(String)>{
'email': (s) => s.contains('@'),
'phone': (s) => s.length >= 10,
'name': (s) => s.isNotEmpty,
};
print(validators['email']!('test@mail.com')); // true
print(validators['phone']!('12345')); // false
// Check the type of a function
print(greet.runtimeType); // (String) => String
print(greet is Function); // true
}
Passing Functions as Arguments
A higher-order function is a function that takes one or more functions as parameters, or returns a function. This is one of the most powerful patterns in Dart, enabling you to write highly reusable, composable code.
Functions as Parameters
// A higher-order function that takes a comparison function
List<T> customSort<T>(List<T> items, int Function(T, T) compare) {
final sorted = List<T>.from(items);
sorted.sort(compare);
return sorted;
}
// A higher-order function that takes a predicate
List<T> filterItems<T>(List<T> items, bool Function(T) predicate) {
return items.where(predicate).toList();
}
// A higher-order function that takes a transformer
List<R> transformItems<T, R>(List<T> items, R Function(T) transform) {
return items.map(transform).toList();
}
void main() {
final names = ['Edrees', 'Ahmed', 'Sara', 'Zain', 'Layla'];
// Sort by length
final byLength = customSort(names, (a, b) => a.length.compareTo(b.length));
print(byLength); // [Sara, Zain, Ahmed, Layla, Edrees]
// Filter names with more than 4 characters
final longNames = filterItems(names, (n) => n.length > 4);
print(longNames); // [Edrees, Ahmed, Layla]
// Transform to uppercase
final upper = transformItems(names, (n) => n.toUpperCase());
print(upper); // [EDREES, AHMED, SARA, ZAIN, LAYLA]
}
Returning Functions
Functions can also return other functions. This enables powerful patterns like function factories, currying, and closures that "remember" their creation context.
Functions Returning Functions
// Function factory: creates specialized validators
bool Function(String) createLengthValidator(int minLength, int maxLength) {
return (String value) => value.length >= minLength && value.length <= maxLength;
}
// Function factory: creates mathematical operations
double Function(double) createMultiplier(double factor) {
return (double value) => value * factor;
}
// Closure: the returned function "remembers" the counter
int Function() createCounter([int start = 0]) {
int count = start;
return () => count++;
}
// Currying: transform a multi-argument function into a chain
String Function(String) Function(String) greetWith(String greeting) {
return (String title) {
return (String name) => '$greeting, $title $name!';
};
}
void main() {
// Create specialized validators
final usernameValidator = createLengthValidator(3, 20);
final passwordValidator = createLengthValidator(8, 128);
print(usernameValidator('Ed')); // false (too short)
print(usernameValidator('Edrees')); // true
print(passwordValidator('12345')); // false (too short)
print(passwordValidator('secure_password_123')); // true
// Create multipliers
final double2x = createMultiplier(2);
final taxRate = createMultiplier(1.15);
print(double2x(50)); // 100.0
print(taxRate(100)); // 115.0
// Counter demonstrates closure
final counter = createCounter(10);
print(counter()); // 10
print(counter()); // 11
print(counter()); // 12
// Currying
final helloMr = greetWith('Hello')('Mr.');
print(helloMr('Ahmed')); // Hello, Mr. Ahmed!
print(helloMr('Zain')); // Hello, Mr. Zain!
}
createCounter example, the returned function captures the count variable and "remembers" it between calls. Each call to createCounter creates a new, independent closure with its own count variable.map(), where(), and reduce()
These are the three most fundamental higher-order methods on Dart collections. They form the backbone of functional-style data processing.
map() -- Transform Every Element
void main() {
final numbers = [1, 2, 3, 4, 5];
// map() applies a function to each element and returns a new Iterable
final doubled = numbers.map((n) => n * 2);
print(doubled.toList()); // [2, 4, 6, 8, 10]
final words = ['hello', 'world', 'dart'];
final capitalized = words.map((w) => w[0].toUpperCase() + w.substring(1));
print(capitalized.toList()); // [Hello, World, Dart]
// map() with index using .indexed (Dart 3)
final indexed = words.indexed.map((pair) => '${pair.$1}: ${pair.$2}');
print(indexed.toList()); // [0: hello, 1: world, 2: dart]
// Type transformation: map() can change the element type
final lengths = words.map((w) => w.length);
print(lengths.toList()); // [5, 5, 4]
// lengths is Iterable<int>, not Iterable<String>
}
map() returns a lazy Iterable, not a List. The transformation function is called only when the iterable is consumed (iterated, converted to a list, etc.). This means that if the transformation has side effects, they will not happen until the iterable is accessed. Always call .toList() if you need immediate evaluation or need to use the result as a List.where() -- Filter Elements
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// where() keeps only elements that pass the test
final evens = numbers.where((n) => n.isEven);
print(evens.toList()); // [2, 4, 6, 8, 10]
final greaterThan5 = numbers.where((n) => n > 5);
print(greaterThan5.toList()); // [6, 7, 8, 9, 10]
// Chain with map
final evenSquares = numbers
.where((n) => n.isEven)
.map((n) => n * n);
print(evenSquares.toList()); // [4, 16, 36, 64, 100]
// whereType<T>() -- filter by type
final mixed = [1, 'hello', 3.14, true, 42, 'world'];
final strings = mixed.whereType<String>();
print(strings.toList()); // [hello, world]
final ints = mixed.whereType<int>();
print(ints.toList()); // [1, 42]
}
reduce() and fold() -- Aggregate to a Single Value
void main() {
final numbers = [1, 2, 3, 4, 5];
// reduce() combines all elements using a function
// Starts with the first element as the initial accumulator
final sum = numbers.reduce((acc, n) => acc + n);
print(sum); // 15
final product = numbers.reduce((acc, n) => acc * n);
print(product); // 120
final maxVal = numbers.reduce((a, b) => a > b ? a : b);
print(maxVal); // 5
// fold() is like reduce() but takes an explicit initial value
// fold() can also change the return type
final sumWithStart = numbers.fold(100, (acc, n) => acc + n);
print(sumWithStart); // 115
// fold() to concatenate strings from ints
final csv = numbers.fold('', (acc, n) => acc.isEmpty ? '$n' : '$acc,$n');
print(csv); // 1,2,3,4,5
// fold() to build a map
final words = ['apple', 'banana', 'avocado', 'blueberry', 'apricot'];
final grouped = words.fold<Map<String, List<String>>>(
{},
(map, word) {
final key = word[0]; // First letter
(map[key] ??= []).add(word);
return map;
},
);
print(grouped);
// {a: [apple, avocado, apricot], b: [banana, blueberry]}
}
fold() instead of reduce() when: (1) the collection might be empty (reduce() throws on empty collections), (2) you need a specific initial value, or (3) the result type differs from the element type. reduce() is simpler when you know the collection is non-empty and the result type matches the element type.expand(), every(), and any()
These higher-order methods complement map(), where(), and reduce() for common collection operations.
expand(), every(), and any()
void main() {
// expand() -- flatMap: map each element to an iterable, then flatten
final sentences = ['Hello world', 'Dart is great'];
final allWords = sentences.expand((s) => s.split(' '));
print(allWords.toList()); // [Hello, world, Dart, is, great]
// Expanding nested lists
final nested = [[1, 2], [3, 4], [5]];
final flat = nested.expand((list) => list);
print(flat.toList()); // [1, 2, 3, 4, 5]
// Duplicate each element
final numbers = [1, 2, 3];
final duplicated = numbers.expand((n) => [n, n]);
print(duplicated.toList()); // [1, 1, 2, 2, 3, 3]
// every() -- test if ALL elements satisfy a condition
final ages = [25, 30, 18, 42];
print(ages.every((age) => age >= 18)); // true (all adults)
print(ages.every((age) => age >= 21)); // false (18 fails)
// any() -- test if AT LEAST ONE element satisfies a condition
final scores = [45, 62, 88, 71];
print(scores.any((s) => s >= 90)); // false (no A grades)
print(scores.any((s) => s >= 80)); // true (88 passes)
print(scores.any((s) => s < 50)); // true (45 fails)
}
forEach vs for Loop
forEach() applies a function to each element, but it has important differences from a regular for loop.
forEach vs for Loop
void main() {
final names = ['Edrees', 'Ahmed', 'Sara'];
// forEach -- applies a function to each element
names.forEach((name) => print('Hello, $name!'));
// LIMITATION 1: Cannot use break or continue
// This will NOT compile:
// names.forEach((name) {
// if (name == 'Ahmed') break; // ERROR!
// });
// LIMITATION 2: Cannot use await inside forEach
// This will NOT work as expected:
// names.forEach((name) async {
// await Future.delayed(Duration(seconds: 1));
// print(name); // All print at once, not sequentially!
// });
// USE for LOOP when you need break, continue, or await
for (final name in names) {
if (name == 'Ahmed') continue; // Works!
print(name);
}
// for loop with await
for (final name in names) {
await Future.delayed(Duration(milliseconds: 100));
print(name); // Prints sequentially with delay
}
}
// RULE OF THUMB:
// - Use forEach for simple side effects (logging, printing)
// - Use for-in loop for everything else (especially if you need
// break, continue, await, or index access)
forEach() with an async callback is a common bug. The forEach method does not await the callback, so all async operations fire simultaneously rather than sequentially. Always use a for-in loop when you need sequential async processing.Function Composition
Function composition is the process of combining simple functions to build more complex ones. Instead of calling functions inside functions, you create a new function that chains them together.
Composing Functions
// Generic compose function: applies f after g
// compose(f, g)(x) = f(g(x))
B Function(A) compose<A, B, C>(
B Function(C) f,
C Function(A) g,
) {
return (A x) => f(g(x));
}
// Pipe: applies functions left-to-right (more intuitive)
// pipe(f, g)(x) = g(f(x))
C Function(A) pipe<A, B, C>(
B Function(A) first,
C Function(B) second,
) {
return (A x) => second(first(x));
}
void main() {
// Simple functions to compose
int doubleIt(int n) => n * 2;
int addTen(int n) => n + 10;
String stringify(int n) => 'Result: $n';
// Compose: stringify(addTen(doubleIt(5)))
final transform = compose(stringify, compose(addTen, doubleIt));
print(transform(5)); // Result: 20
// 5 → doubleIt → 10 → addTen → 20 → stringify → "Result: 20"
// Using extension methods for cleaner chaining
final result = 5
.let(doubleIt) // 10
.let(addTen) // 20
.let(stringify); // "Result: 20"
print(result);
}
// Extension for pipe-style chaining
extension Pipe<T> on T {
R let<R>(R Function(T) f) => f(this);
}
Practical Example: Data Transformation Pipeline
Here is a real-world example of using higher-order functions to build a data processing pipeline, similar to what you would find in a backend service or data analytics tool.
Data Transformation Pipeline
class Transaction {
final String id;
final String category;
final double amount;
final DateTime date;
final bool isRefund;
Transaction(this.id, this.category, this.amount, this.date, this.isRefund);
@override
String toString() => 'Transaction($id, $category, \$${amount.toStringAsFixed(2)})';
}
void main() {
final transactions = [
Transaction('t1', 'food', 25.50, DateTime(2024, 1, 15), false),
Transaction('t2', 'transport', 12.00, DateTime(2024, 1, 15), false),
Transaction('t3', 'food', 45.00, DateTime(2024, 1, 16), false),
Transaction('t4', 'food', 15.00, DateTime(2024, 1, 16), true),
Transaction('t5', 'entertainment', 60.00, DateTime(2024, 1, 17), false),
Transaction('t6', 'transport', 8.50, DateTime(2024, 1, 17), false),
Transaction('t7', 'food', 35.00, DateTime(2024, 1, 18), false),
];
// Pipeline: Filter → Transform → Aggregate
// Step 1: Exclude refunds
// Step 2: Get food transactions only
// Step 3: Calculate total and average
final foodTotal = transactions
.where((t) => !t.isRefund) // Exclude refunds
.where((t) => t.category == 'food') // Food only
.map((t) => t.amount) // Extract amounts
.fold(0.0, (sum, amount) => sum + amount); // Sum
print('Food total (excl. refunds): \$${foodTotal.toStringAsFixed(2)}');
// Food total (excl. refunds): $105.50
// Group by category and sum
final byCategory = transactions
.where((t) => !t.isRefund)
.fold<Map<String, double>>(
{},
(map, t) {
map[t.category] = (map[t.category] ?? 0) + t.amount;
return map;
},
);
print('Spending by category: $byCategory');
// {food: 105.5, transport: 20.5, entertainment: 60.0}
// Find the highest spending category
final topCategory = byCategory.entries
.reduce((a, b) => a.value > b.value ? a : b);
print('Top category: ${topCategory.key} (\$${topCategory.value.toStringAsFixed(2)})');
// Top category: food ($105.50)
}
Practical Example: Middleware Chain
Higher-order functions are the foundation of middleware patterns used in web frameworks, HTTP clients, and event processing systems.
Middleware Chain Pattern
// A middleware takes a request and a "next" function
typedef Middleware = Future<Map<String, dynamic>> Function(
Map<String, dynamic> request,
Future<Map<String, dynamic>> Function(Map<String, dynamic>) next,
);
// Logging middleware
Middleware loggingMiddleware() {
return (request, next) async {
print('→ ${request['method']} ${request['path']}');
final stopwatch = Stopwatch()..start();
final response = await next(request);
stopwatch.stop();
print('← ${response['status']} (${stopwatch.elapsedMilliseconds}ms)');
return response;
};
}
// Auth middleware
Middleware authMiddleware(Set<String> validTokens) {
return (request, next) async {
final token = request['token'] as String?;
if (token == null || !validTokens.contains(token)) {
return {'status': 401, 'body': 'Unauthorized'};
}
return next(request);
};
}
// Compose middlewares into a single handler
Future<Map<String, dynamic>> Function(Map<String, dynamic>) composeMiddleware(
List<Middleware> middlewares,
Future<Map<String, dynamic>> Function(Map<String, dynamic>) handler,
) {
return middlewares.reversed.fold(
handler,
(next, middleware) => (request) => middleware(request, next),
);
}
// Simple request handler
Future<Map<String, dynamic>> handleRequest(Map<String, dynamic> request) async {
return {'status': 200, 'body': 'Hello from ${request['path']}'};
}
Future<void> main() async {
final validTokens = {'token_abc', 'token_xyz'};
// Build the middleware chain
final handler = composeMiddleware(
[loggingMiddleware(), authMiddleware(validTokens)],
handleRequest,
);
// Authenticated request
final response1 = await handler({
'method': 'GET',
'path': '/api/users',
'token': 'token_abc',
});
print('Response: ${response1['body']}');
// → GET /api/users
// ← 200 (Xms)
// Response: Hello from /api/users
// Unauthenticated request
final response2 = await handler({
'method': 'GET',
'path': '/api/secret',
'token': 'invalid',
});
print('Response: ${response2['body']}');
// → GET /api/secret
// ← 401 (Xms)
// Response: Unauthorized
}
Practical Example: Event Handler Registry
Higher-order functions make event handling systems clean and extensible.
Event Handler with Higher-Order Functions
class EventBus {
final _handlers = <String, List<void Function(Map<String, dynamic>)>>{};
// Register a handler (returns an unsubscribe function)
void Function() on(String event, void Function(Map<String, dynamic>) handler) {
(_handlers[event] ??= []).add(handler);
// Return a function that removes this handler
return () {
_handlers[event]?.remove(handler);
};
}
// Register a one-time handler
void once(String event, void Function(Map<String, dynamic>) handler) {
late void Function() unsubscribe;
unsubscribe = on(event, (data) {
handler(data);
unsubscribe();
});
}
// Emit an event
void emit(String event, [Map<String, dynamic> data = const {}]) {
final handlers = _handlers[event];
if (handlers != null) {
// Create a copy to avoid modification during iteration
for (final handler in List.from(handlers)) {
handler(data);
}
}
}
}
void main() {
final bus = EventBus();
// Register handlers
final unsub = bus.on('user:login', (data) {
print('User logged in: ${data['name']}');
});
bus.on('user:login', (data) {
print('Send welcome email to ${data['email']}');
});
// One-time handler
bus.once('user:login', (data) {
print('First login bonus for ${data['name']}!');
});
// Emit events
bus.emit('user:login', {'name': 'Edrees', 'email': 'edrees@example.com'});
// User logged in: Edrees
// Send welcome email to edrees@example.com
// First login bonus for Edrees!
print('---');
bus.emit('user:login', {'name': 'Ahmed', 'email': 'ahmed@example.com'});
// User logged in: Ahmed
// Send welcome email to ahmed@example.com
// (No bonus -- once handler was removed)
// Unsubscribe first handler
unsub();
print('---');
bus.emit('user:login', {'name': 'Sara', 'email': 'sara@example.com'});
// Send welcome email to sara@example.com
// (Only second handler remains)
}
Summary
Higher-order functions are at the heart of functional programming in Dart. By treating functions as first-class citizens, you can write highly reusable, composable, and testable code. Master map(), where(), reduce(), and fold() for collection processing. Use function composition and closures for building pipelines, middleware chains, and event systems. Remember: prefer for-in loops over forEach() when you need break, continue, or await. Higher-order functions are not just a technique -- they are a way of thinking that leads to cleaner, more maintainable Dart code.
.where().map().fold()), be mindful that each operation creates a new lazy iterable. If you need to reuse intermediate results, call .toList() to materialize the iterable. Otherwise, chaining is efficient because lazy evaluation means elements are processed one at a time through the entire chain, without creating intermediate lists in memory.