Dart Advanced Features

Functional Programming: Higher-Order Functions

50 min Lesson 8 of 16

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!
}
Note: A closure is a function that captures variables from its enclosing scope. In the 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>
}
Warning: 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]}
}
Tip: Use 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)
Warning: 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.

Tip: When chaining multiple collection operations (.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.