Collect a pipeline into what you need
Reach for the named terminal operation when there is one - toList, sum, joined, groupBy - and fall back to collect with an Accumulator for anything else, including your own accumulator.
Nothing in a pipeline runs until its terminal operation pulls the values through. Most of the time the result you
need already has a name on Iterate itself; collect with an Accumulator is what is left for everything that does
not.
Steps#
Reach for the named terminal operation first.
toList(),toSet(),to<Target>()for any otherFrom<Iterate<Item>>,sum(),count(),joined(separator:),fold(initial, combine),first(),find { ... },any { ... },all { ... }andgroupBy { ... }cover most pipelines without ever naming anAccumulator.const total = orders.map { order => order.amount }.sum() const byCity = customers.groupBy { customer => customer.city }Reach for
collectwith a standard-libraryAccumulatorfor a shape none of those name.counting(),summing { ... },averaging { ... },minBy { ... }/maxBy { ... },partitioningBy { ... }, andgroupingBy { ... }.then(downstream)for one collector per group.const stats = orders.collect(groupingBy { order: Order => order.city }.then(summing { order: Order => order.amount }))Run the same accumulator more than once when several results come from one pipeline of values. An
Accumulatoris a value; each call tocollectfills a copy of its own.const amounts = summing { order: Order => order.amount } const thisMonth = orders.filter { order => order.isThisMonth() }.collect(amounts) const lastMonth = orders.filter { order => order.isLastMonth() }.collect(amounts)Write
collector(initial, finish:, step:)when the state a run needs is one value. It answers anAccumulatorwhose accumulator foldsstepover every value and appliesfinishonce, which covers most custom aggregates without a type of their own.Write a
typewithvarfields that implementsAccumulatorwhen the state is more than one value, the way the standard library's owngroupingBy(...).then(...)keeps one accumulator per group in aMap.
Pitfalls#
isDone()is what lets a terminal operation stop early.first(),find { ... }andcollectbehindtake(n)never read a source to the end; a customAccumulatorthat never overridesisDone()is asked to read everything, which is correct for a sum or a count and wrong for anything that could stop sooner.- An
Accumulatorvalue is not itself a result.orders.collect(summing { order: Order => order.amount })runs it; the value on its own is a description that can be handed tocollectagain for a different source. foldand a hand-writtenAccumulatordo the same pull. Reach forfoldwhen the state is a simple running value and there is only one call site; reach for a namedAccumulatorthe moment two pipelines need the same aggregate, so the logic is written once.- A collection is not an
Accumulator.pipeline.collect(existingList)is not a thing: name the accumulator that gathers into one -listing()orListAccumulator<Item>()for a list,into<Set<Int>>()for anything withFrom<Iterate<Item>>- and append the result to what you already have.
Full example#
type Order {
city: String
amount: Int
}
const orders = [
Order("Berlin", 120),
Order("Berlin", 80),
Order("Munich", 200),
]
const total = orders.map { order => order.amount }.sum()
print total
const totalsByCity = orders.collect(groupingBy { order: Order => order.city }.then(summing { order: Order => order.amount }))
print totalsByCity
const highValue = orders.collect(partitioningBy { order => order.amount >= 100 })
print highValueRelated#
- Pipelines - source, lazy stages and terminal operation, in full.
- Collectors -
Accumulatorand every standard collector in one place. - Sort by more than one key - a
sortedstage in front of the same kind of pipeline.