Dart from the ground up

Doing work on a whole list at once

Replacing loops with calls that each answer one question: map to change every value, where to keep the ones that pass a test, and fold and reduce to boil a list down to one value.

Part 8 of 12

Intermediate18 min readBack to Courses

One rule, applied to every value

map takes a list and a rule, and gives back the list of answers. The part in parentheses is a small unnamed function, and c is one value from the list, so a rule that adds fifty to every price is written cents.map((c) => c + 50). Nothing in the original list changes.

What map hands back is a lazy sequence rather than a finished list. That is why the examples finish the job with .toList() before printing, or with .join(", ") when the answers are text. Reading cents.map((c) => c + 50).toList() left to right says what happens in the order it happens: change every value, then gather the answers into a list.

Keeping only what passes a test

where takes the same kind of rule, but the rule answers true or false and only the values that say true survive: cents.where((c) => c >= 500). Count them with .length, or turn them into a list with .toList() to print them.

Chained together, these calls read like a sentence about your data: keep the big ones, then double them, then add them up. A loop could do the same work, but a reader of the loop has to run it in their head, while a reader of the chain can read the questions one at a time.

Boiling a list down to one value

fold walks the list carrying a running answer with it: cents.fold<int>(0, (sum, c) => sum + c) starts from 0 and adds each price in turn, so the value at the end is the total. The type in the angle brackets says what the running answer is — a number here, and text when you are building a sentence.

reduce is the shorter cousin with no starting value: it uses the first item as the start and answers the single value that is left. cents.reduce((a, b) => a > b ? a : b) gives the largest. Reach for fold when the answer may be a different type or the list may be empty; reach for reduce when the answer is obviously one of the items.

Nothing runs until something asks

map and where do not build a list. They return a lazy sequence that knows the rule and the source, and the rule runs when something reads the sequence: a toList, a join, a length or a loop. Print the sequence itself and Dart shows its values in parentheses, which is a hint that you are looking at a view rather than a list.

Laziness is why a chain can stop early. where(...).take(3) reads only until it has three matches instead of filtering the whole list first. It also means a rule can run more than once if you read the same sequence twice, so store the result in a final when the work is expensive or when the rule has an effect.

The type in fold<int> is the type of the answer being carried, not the type of the list. Use fold<String> to build a sentence, fold<int> to add, and fold<Map<String, int>> to group. The starting value must be that type too.

Questions that answer true or false

any asks whether at least one value passes the rule, and every asks whether all of them do. Both stop as soon as the answer is settled, which is why every on a long list can be cheap when the first item fails.

take and skip cut a sequence by position, and firstWhere finds the first match — with an orElse for the case where there might not be one. These read as questions about the data rather than as loop mechanics, which is the point of the whole chapter.

Chains are read left to right, and each call sees the result of the one before it. Put the cheapest, most selective test first: a where that removes most values makes every later step smaller. That is also the order in which a reader will follow the sentence.

Map, where, fold and reduce

void main() {
  final cents = <int>[450, 3200, 150];
  print(cents.map((c) => c + 50).toList());
  print(cents.where((c) => c < 1000).toList());
  print(cents.fold<int>(0, (sum, c) => sum + c));
  print(cents.reduce((a, b) => a > b ? a : b));
}

Four questions about one list of prices

Run it and read which question each printed line answers. Then change the filter to keep the prices under 1000, and add a line that uses fold to build the total of the doubled prices instead of the original ones.

Output
Click "Run" to execute your code

Three mistakes with these calls. Sorting the result of where directly — it is a lazy sequence, so call .toList() first. Calling reduce on an empty list, which has no first item to start from; use fold with a starting value. And forgetting that map leaves the original alone, then wondering why the source list still prints the old values.

Try these next

  • Take a list of prices, keep the ones above 500, double them, and print the total.
  • Use any and every to answer two different questions about the same list.
  • Build one sentence from a list of names with fold<String> and no trailing space.

Questions about names and prices

Run it and match each line to the call that produced it. Then change one price so `allEven` becomes false, and add a line that uses `skip` to drop the first three names.

Output
Click "Run" to execute your code

The core library has no sorted call: order a list with sort and a comparator, as chapter seven shows. Sort changes the list in place, so sort a copy when the original still has work to do.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does map hand back?