Dart 35 🎯 Functional Iteration: map, where, fold, reduce, and expand
A loop is a sequence of instructions. A functional iteration is a transformation of a collection. The difference is not just style; it is what the code expresses. A for loop says “do this to each element.” A map says “this collection becomes that collection.” The functional operations—map, where, fold, reduce, expand—are the methods on Iterable that transform a collection without an explicit loop. They compose, they are lazy in some cases, and they are the idiomatic way to process data in Dart.
Key point: map transforms each element, where filters, fold reduces with an initial value, reduce reduces without one, and expand flattens. All five return a new Iterable except fold and reduce, which return a single value. The operations are lazy where the return type is Iterable; the computation happens when the iterable is consumed. The methods do not mutate the source collection.
Why functional iteration matters
The transformation problem. A list of users needs to become a list of names. A list of strings needs to become a list of their lengths. The transformation is a mapping from each element to a new value. map expresses it directly. The loop version requires an output list, an iteration, and an append. The map version is one expression.
The filtering problem. A list of orders needs to become a list of the completed orders. The filter is a predicate on each element. where expresses it directly. The loop version requires a condition and an append. The where version is one expression.
The reduction problem. A list of numbers needs to become their sum. A list of orders needs to become their total. The reduction is a fold over the elements. fold and reduce express it directly. The loop version requires an accumulator and an update.
The flattening problem. A list of lists needs to become a single list. A list of orders needs to become a list of all the items across the orders. The flattening is a mapping followed by a concatenation. expand expresses it directly.
The composition problem. The operations compose. users.where((u) => u.active).map((u) => u.name).toList() is a filter followed by a transformation. The chain reads as a pipeline: take the users, keep the active ones, map to names, collect. The loop version interleaves the filter and the transformation, and the intent is buried in the control flow.
The laziness problem. map, where, and expand return Iterables that are lazy. The computation happens when the iterable is consumed. A chain of lazy operations over a large collection is a single pass, not one pass per operation. The laziness is the performance benefit, and it is the reason the methods are preferred over the loop.
a. map and where
The map method transforms each element with a function. It returns an Iterable of the results.
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map((n) => n * 2);
print(doubled.toList()); // [2, 4, 6, 8, 10]
The map takes a function from int to int. The result is an Iterable<int>. The .toList() converts it to a List<int>.
The map can change the type.
var words = ['apple', 'banana', 'cherry'];
var lengths = words.map((w) => w.length);
print(lengths.toList()); // [5, 6, 6]
The map takes a function from String to int. The result is an Iterable<int>.
The where method filters the elements with a predicate. It returns an Iterable of the elements that satisfy the predicate.
var numbers = [1, 2, 3, 4, 5];
var evens = numbers.where((n) => n % 2 == 0);
print(evens.toList()); // [2, 4]
The where takes a function from int to bool. The result is an Iterable<int> of the elements for which the predicate is true.
The where can filter by the element’s properties.
class User {
final String name;
final bool active;
User(this.name, {this.active = true});
}
var users = [
User('Alice', active: true),
User('Bob', active: false),
User('Carol', active: true),
];
var activeNames = users.where((u) => u.active).map((u) => u.name);
print(activeNames.toList()); // ['Alice', 'Carol']
The where keeps the active users, and the map extracts the names. The two compose.
The map and where are lazy. The computation happens when the iterable is consumed.
var result = numbers
.map((n) {
print('Mapping $n');
return n * 2;
})
.where((n) {
print('Filtering $n');
return n > 4;
});
print('Before iteration');
for (var n in result) {
print('Result: $n');
}
The output is:
Before iteration
Mapping 1
Filtering 2
Mapping 2
Filtering 4
Mapping 3
Filtering 6
Result: 6
Mapping 4
Filtering 8
Result: 8
Mapping 5
Filtering 10
Result: 10
The map and where do not run until the for loop consumes the result. Each element is processed by both operations before the next element is processed. The chain is a single pass, and the laziness is the reason.
b. fold and reduce
The fold method reduces the elements to a single value. It takes an initial value and a function that combines the accumulator with each element.
var numbers = [1, 2, 3, 4, 5];
var sum = numbers.fold(0, (acc, n) => acc + n);
print(sum); // 15
The fold takes an initial value 0 and a function (acc, n) => acc + n. The accumulator starts at 0, and each element is added. The result is 15.
The fold can change the type of the accumulator.
var words = ['a', 'b', 'c'];
var concatenated = words.fold('', (acc, w) => acc + w);
print(concatenated); // 'abc'
The accumulator starts as an empty string, and each word is concatenated. The result is 'abc'.
The fold can build a collection.
var numbers = [1, 2, 3, 4, 5];
var grouped = numbers.fold<Map<String, List<int>>>(
{'even': [], 'odd': []},
(acc, n) {
if (n % 2 == 0) {
acc['even']!.add(n);
} else {
acc['odd']!.add(n);
}
return acc;
},
);
print(grouped); // {even: [2, 4], odd: [1, 3, 5]}
The accumulator is a map with two keys. Each number is added to the appropriate list. The result is the grouped map.
The reduce method is like fold but without an initial value. The first element is the initial accumulator.
var numbers = [1, 2, 3, 4, 5];
var sum = numbers.reduce((acc, n) => acc + n);
print(sum); // 15
The reduce takes only the combining function. The first element is the accumulator, and the rest are combined. The result is 15.
The reduce throws if the collection is empty.
var empty = <int>[];
// var sum = empty.reduce((acc, n) => acc + n); // error
The fold with an initial value is the safe version. It works for the empty collection because the initial value is the result.
var empty = <int>[];
var sum = empty.fold(0, (acc, n) => acc + n);
print(sum); // 0
The fold with 0 returns 0 for the empty list. The reduce would throw.
The reduce is preferred when the collection is known to be non-empty and the first element is the natural initial value. The fold is preferred when the initial value is different or the collection might be empty.
c. expand and the composition
The expand method maps each element to an Iterable and flattens the results.
var lists = [[1, 2], [3, 4], [5, 6]];
var flat = lists.expand((list) => list);
print(flat.toList()); // [1, 2, 3, 4, 5, 6]
The expand takes a function from List<int> to Iterable<int>. The function returns the list itself, and the expand concatenates the lists. The result is a flat list.
The expand can produce a different number of elements per input.
var words = ['hello', 'world'];
var letters = words.expand((w) => w.split(''));
print(letters.toList()); // ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
The expand takes a function from String to Iterable<String>. The function splits the word into letters, and the expand concatenates them.
The expand is the combination of map and flatten.
// These are equivalent
var flat1 = lists.expand((list) => list);
var flat2 = lists.map((list) => list).expand((list) => list);
The expand is the direct form. The map followed by expand is the long form.
The composition of the five methods is the pipeline.
var orders = [
Order('A', items: [Item('apple', 1.0), Item('banana', 0.5)]),
Order('B', items: [Item('cherry', 2.0)]),
Order('C', items: [Item('date', 3.0), Item('elderberry', 4.0)]),
];
var total = orders
.where((o) => o.items.isNotEmpty)
.expand((o) => o.items)
.map((i) => i.price)
.fold(0.0, (sum, price) => sum + price);
print(total); // 10.5
The pipeline filters the orders with items, flattens the items, maps to prices, and folds the sum. Each step is a transformation, and the chain reads as a description of the computation.
The where, map, and expand are lazy. The fold is eager; it consumes the iterable and produces the value. The chain is a single pass, and the laziness is the efficiency.
A comparison with the loop version:
var total = 0.0;
for (var order in orders) {
if (order.items.isEmpty) continue;
for (var item in order.items) {
total += item.price;
}
}
The loop version has the same result but interleaves the filter, the iteration, and the accumulation. The functional version separates them into a chain, and the intent is visible in the operations.
The choice between the two is a matter of style and readability. The functional chain is idiomatic in Dart and is preferred when the operations compose naturally. The loop is preferred when the logic is complex or when the performance requires the explicit control.
Complete Example Session
// ============================================
// PART 1: MAP
// ============================================
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map((n) => n * 2);
print(doubled.toList()); // [2, 4, 6, 8, 10]
// ============================================
// PART 2: MAP CHANGING TYPE
// ============================================
var words = ['apple', 'banana', 'cherry'];
var lengths = words.map((w) => w.length);
print(lengths.toList()); // [5, 6, 6]
// ============================================
// PART 3: WHERE
// ============================================
var numbers = [1, 2, 3, 4, 5];
var evens = numbers.where((n) => n % 2 == 0);
print(evens.toList()); // [2, 4]
// ============================================
// PART 4: MAP AND WHERE
// ============================================
var activeNames = users.where((u) => u.active).map((u) => u.name);
print(activeNames.toList()); // ['Alice', 'Carol']
// ============================================
// PART 5: LAZINESS
// ============================================
var result = numbers.map((n) => n * 2).where((n) => n > 4);
// Nothing happens until result is consumed
print(result.toList());
// ============================================
// PART 6: FOLD
// ============================================
var sum = [1, 2, 3, 4, 5].fold(0, (acc, n) => acc + n);
print(sum); // 15
// ============================================
// PART 7: FOLD BUILDING A MAP
// ============================================
var grouped = [1, 2, 3, 4, 5].fold<Map<String, List<int>>>(
{'even': [], 'odd': []},
(acc, n) {
if (n % 2 == 0) {
acc['even']!.add(n);
} else {
acc['odd']!.add(n);
}
return acc;
},
);
print(grouped); // {even: [2, 4], odd: [1, 3, 5]}
// ============================================
// PART 8: REDUCE
// ============================================
var sum = [1, 2, 3, 4, 5].reduce((acc, n) => acc + n);
print(sum); // 15
// var empty = <int>[].reduce((a, b) => a + b); // throws
// ============================================
// PART 9: EXPAND
// ============================================
var lists = [[1, 2], [3, 4], [5, 6]];
var flat = lists.expand((list) => list);
print(flat.toList()); // [1, 2, 3, 4, 5, 6]
// ============================================
// PART 10: COMPOSITION
// ============================================
var total = orders
.where((o) => o.items.isNotEmpty)
.expand((o) => o.items)
.map((i) => i.price)
.fold(0.0, (sum, price) => sum + price);
print(total); // 10.5
The ten parts covered map, map changing type, where, map and where, laziness, fold, fold building a map, reduce, expand, and the composition.
Quick Reference
The Five Methods
| Method | Takes | Returns |
|---|---|---|
map | T → R | Iterable<R> |
where | T → bool | Iterable<T> |
fold | initial + (A, T) → A | A |
reduce | (T, T) → T | T |
expand | T → Iterable<R> | Iterable<R> |
Empty Collection Behavior
| Method | Empty |
|---|---|
map | [] |
where | [] |
fold | the initial value |
reduce | throws |
expand | [] |
Laziness
| Method | Lazy |
|---|---|
map | Yes |
where | Yes |
expand | Yes |
fold | No |
reduce | No |
Composition
| Chain | Purpose |
|---|---|
.where().map() | Filter and transform |
.map().where() | Transform and filter |
.where().expand() | Filter and flatten |
.map().fold() | Transform and reduce |
.where().expand().map().fold() | Full pipeline |
Collection Conversion
| Method | Result |
|---|---|
.toList() | List<T> |
.toSet() | Set<T> |
.toMap() | Map<K, V> |
.join(sep) | String |
Best Practices
✅ Do This:
// Use map for transformations
var names = users.map((u) => u.name).toList(); // ✅
// Use where for filtering
var active = users.where((u) => u.active).toList(); // ✅
// Use fold for the safe reduction
var sum = numbers.fold(0, (a, b) => a + b); // ✅
// Use reduce when the first element is the initial value
var max = numbers.reduce((a, b) => a > b ? a : b); // ✅
// Use expand for flattening
var flat = lists.expand((l) => l).toList(); // ✅
// Compose the operations
var result = items
.where((i) => i.active)
.map((i) => i.name)
.toList(); // ✅
// Convert the lazy iterable with toList
var list = numbers.map((n) => n * 2).toList(); // ✅
❌ Don’t Do This:
// Don't use reduce on a possibly-empty collection
var sum = empty.reduce((a, b) => a + b); // throws // ❌
// Don't forget that map is lazy
numbers.map((n) => print(n)); // nothing printed // ❌
// Don't mutate the source in map
numbers.map((n) { list.add(n); return n; }); // ❌
// Don't chain too many operations without a reason
numbers.map((n) => n).where((n) => true).map((n) => n)... // ⚠️
// Don't use a loop when the operations compose
var result = [];
for (var n in numbers) { if (n > 0) result.add(n * 2); } // ⚠️
// Don't forget toList when a list is needed
var doubled = numbers.map((n) => n * 2); // Iterable // ⚠️
// Don't use fold when a simpler method exists
var sum = numbers.fold(0, (a, b) => a + b); // fine // ✅
var total = numbers.reduce((a, b) => a + b); // if non-empty // ✅
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Nothing happens | Lazy map not consumed | .toList() |
reduce throws | Empty collection | Use fold |
| Type error | map changes type | Annotate or cast |
| Source mutated | Side effects in map | Keep functions pure |
| Infinite loop | expand produces infinite | Check the function |
| Wrong order | map before where | Reorder |
| Stack overflow | Deep recursion in fold | Use a loop |
Real-World Examples
1. Extract Field
var names = users.map((u) => u.name).toList();
2. Filter Active
var active = users.where((u) => u.active).toList();
3. Sum
var total = prices.fold(0.0, (sum, p) => sum + p);
4. Max
var max = numbers.reduce((a, b) => a > b ? a : b);
5. Flatten
var flat = lists.expand((l) => l).toList();
6. Transform and Filter
var activeNames = users.where((u) => u.active).map((u) => u.name).toList();
7. Group by Key
var grouped = items.fold<Map<String, List<Item>>>(
{},
(acc, item) {
(acc[item.category] ??= []).add(item);
return acc;
},
);
8. Unique Values
var unique = numbers.toSet().toList();
9. Count Matching
var count = items.where((i) => i.active).length;
10. Full Pipeline
var total = orders
.where((o) => o.status == 'completed')
.expand((o) => o.items)
.map((i) => i.price * i.quantity)
.fold(0.0, (sum, price) => sum + price);
Visual
The Five Methods
┌─────────────────────────────────────────────────────────────┐
│ map: T → R │
│ [1, 2, 3] → [2, 4, 6] │
│ │
│ where: T → bool │
│ [1, 2, 3, 4] → [2, 4] │
│ │
│ fold: (A, T) → A │
│ [1, 2, 3] + 0 → 6 │
│ │
│ reduce: (T, T) → T │
│ [1, 2, 3] → 6 │
│ │
│ expand: T → Iterable<R> │
│ [[1, 2], [3, 4]] → [1, 2, 3, 4] │
│ │
└─────────────────────────────────────────────────────────────┘
Composition Pipeline
┌─────────────────────────────────────────────────────────────┐
│ orders │
│ │ │
│ ▼ where(o => o.status == 'completed') │
│ [completed orders] │
│ │ │
│ ▼ expand(o => o.items) │
│ [all items] │
│ │ │
│ ▼ map(i => i.price * i.quantity) │
│ [line totals] │
│ │ │
│ ▼ fold(0.0, (sum, total) => sum + total) │
│ grand total │
│ │
│ Each step transforms the collection. │
│ The chain is a single pass. │
│ │
└─────────────────────────────────────────────────────────────┘
Laziness
┌─────────────────────────────────────────────────────────────┐
│ var result = numbers.map(transform).where(filter); │
│ │
│ Nothing happens yet. │
│ The result is a lazy Iterable. │
│ │
│ result.toList(); │
│ │
│ Now the computation runs: │
│ For each element: │
│ 1. transform │
│ 2. filter │
│ 3. add to result if filter passes │
│ │
│ A single pass. No intermediate lists. │
│ │
└─────────────────────────────────────────────────────────────┘
fold vs reduce
┌─────────────────────────────────────────────────────────────┐
│ FOLD │
│ numbers.fold(0, (acc, n) => acc + n) │
│ │
│ acc = 0 │
│ acc = 0 + 1 = 1 │
│ acc = 1 + 2 = 3 │
│ acc = 3 + 3 = 6 │
│ Result: 6 │
│ │
│ Empty collection: returns the initial value │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ REDUCE │
│ numbers.reduce((acc, n) => acc + n) │
│ │
│ acc = 1 (first element) │
│ acc = 1 + 2 = 3 │
│ acc = 3 + 3 = 6 │
│ Result: 6 │
│ │
│ Empty collection: throws │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Method | Signature | Result |
|---|---|---|
map | R Function(T) | Iterable<R> |
where | bool Function(T) | Iterable<T> |
fold | A Function(A, T) + initial | A |
reduce | T Function(T, T) | T |
expand | Iterable<R> Function(T) | Iterable<R> |
| Lazy | map, where, expand | Yes |
| Eager | fold, reduce | Yes |
Empty reduce | Throws | Use fold |
Key takeaways:
maptransforms each element. The function takes an element and returns a new value. The result is anIterableof the new values. The type can change.wherefilters the elements. The predicate takes an element and returns abool. The result is anIterableof the elements for which the predicate istrue.foldreduces with an initial value. The accumulator starts at the initial value, and each element is combined. The result is the final accumulator. Thefoldworks for empty collections.reducereduces without an initial value. The first element is the accumulator, and the rest are combined. Thereducethrows for empty collections. Usefoldwhen the collection might be empty.expandflattens. The function takes an element and returns anIterable. The result is the concatenation of all the returned iterables. Theexpandis the combination ofmapand flatten.map,where, andexpandare lazy. The computation happens when the iterable is consumed. A chain of lazy operations over a large collection is a single pass, not one pass per operation.- The methods compose. The chain reads as a pipeline: filter, transform, flatten, reduce. The functional version separates the operations, and the intent is visible.
Remember: The five methods are the functional iteration toolkit. map transforms, where filters, fold and reduce reduce, and expand flattens. They compose into pipelines, they are lazy where the return type is Iterable, and they do not mutate the source. Use them for the transformations that compose naturally. Use a loop when the logic is complex or the performance requires the explicit control. The functional style is idiomatic in Dart, and the chain is the expression of the computation.
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!