Collectors
An Accumulator describes what to do with the values of a pipeline and is the state of one run at the same time, because a value is a copy; collect fills a copy of the one it is given.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
collect is the general terminal operation of a pipeline: it takes an Accumulator, which says what to do with the
values, and pulls the source through it. An Accumulator is a value, so collect fills a copy of it and the one
the caller holds is untouched: the description of a run and the state of a run are one type, and there is no factory
step between them.
Example#
const numbers = [1, 2, 3, 4, 5]
const total = numbers.collect(summing { value: Int => value })
const byParity = numbers.collect(groupingBy { value: Int => value % 2 })
print total
print byParitySyntax#
trait Accumulator<Item, Output> {
var fn add(value: Item)
fn finish(): Output
fn isDone(): Bool { false }
}
source.collect(accumulator)Rules#
Accumulator<Item, Output>is the whole of it:add,finishandisDone.collectcopies what it is given, pushes every value into the copy withadd, and callsfinish()at the end. There is nostart(), because a copy of a value already is a fresh run.Accumulatoris push-based:addtakes one value at a time, andisDone()says whether more values would change the result. A driver asksisDone()before the first value and after everyadd, socollect(counting())over an infinite source never returns, butcollectbehindtaking(10)orfirst()stops reading after enough values arrived -isDonedefaults tofalse.A collection is not an
Accumulator, and there is no trait for "something withadd". What gathers into a collection is a type of its own beside it, the wayCollectorandCollectors.toList()are in Java:ListAccumulator<Item>is the one the standard library ships, andinto<Target>()is the general one for anyFrom<Iterate<Item>>target - it gathers into aListand callsTarget.fromonce at the end.const numbers = [3, 1, 2, 1] print numbers.collect(ListAccumulator<Int>()) print numbers.collect(into<Set<Int>>())A package that owns a collection may ship an accumulator that writes straight into it. Nothing picks such a one up automatically: a caller who wants it names it at the call, because which run a pipeline makes is written down and never inferred from the target type.
The standard library's collectors cover the common shapes:
counting(),summing { ... },averaging { ... }(anOption,Nonefor an empty pipeline),minBy { ... }/maxBy { ... }(alsoOption),joining(separator:, prefix:, suffix:),partitioningBy { ... }(a tuple of two lists), andgroupingBy { ... }(aMap<Key, List<Item>>, or.then(downstream)for a different one per group).const numbers = [1, 2, 3, 4, 5] const (even, odd) = numbers.collect(partitioningBy { _ % 2 == 0 }) print even print oddWrite one as a fold with a final step when the state is one value.
collector(initial, finish:, step:)answers anAccumulatorthat foldsstepover every value and appliesfinishonce at the end.fn median<Item>(value: Transform<Item, Int>): Accumulator<Item, Int?> { collector([], finish: { values: List<Int> => values.sorted { it => it }.skip(values.length() / 2).first() }) { values, item => values.appended value(item) } } const numbers = [5, 3, 9, 1, 7] print numbers.collect(median { value => value })Write one as a type with
varfields when the state is more than one value.ListAccumulator<Item>is the simplest example, andgroupingBy(...).then(...)is the standard library's own: one downstream accumulator per group, held in aMap<Key, Accumulator<Item, Output>>and copied from the downstream for every new group.type Extremes<Item: Compare> with Accumulator<Item, (Item?, Item?)> { var min: Item? = None var max: Item? = None var fn add(value: Item) { min = match min { Some(current) if current <= value => min _ => value } max = match max { Some(current) if current >= value => max _ => value } } fn finish(): (Item?, Item?) { (min, max) } } print([5, 3, 9, 1].collect(Extremes<Int>()))
What this is not#
An accumulator is not a one-shot value: collect copies it, so one value drives many runs. The same accumulator
runs again for a different source, and each run gets its own copy.
const salaryStatistics = summing { value: Int => value }
print([1, 2, 3].collect(salaryStatistics))
print([10, 20].collect(salaryStatistics))const salaryStatistics = summing { value: Int => value }
print(salaryStatistics())
// error: `salaryStatistics` is a `Merge<Int64, Int64>`, not a functionThe one thing the merged type does not say is whether what it was handed is fresh: an accumulator that has already
been added to and is then given to collect or to then starts every run from what is in it. Build one where it is
used, the way every collector of std/iteration answers a fresh value.
Related#
- Pipelines - the lazy stages that produce what an accumulator consumes.
- The collection traits - the five kinds, and why none of them is an
Accumulator. - Maps and sets - the
Map<Key, List<Item>>thatgroupingBybuilds.