Referenz

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.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

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#

  1. Reach for the named terminal operation first. toList(), toSet(), to<Target>() for any other From<Iterate<Item>>, sum(), count(), joined(separator:), fold(initial, combine), first(), find { ... }, any { ... }, all { ... } and groupBy { ... } cover most pipelines without ever naming an Accumulator.

    const total = orders.map { order => order.amount }.sum()
    const byCity = customers.groupBy { customer => customer.city }
  2. Reach for collect with a standard-library Accumulator for a shape none of those name. counting(), summing { ... }, averaging { ... }, minBy { ... }/maxBy { ... }, partitioningBy { ... }, and groupingBy { ... }.then(downstream) for one collector per group.

    const stats = orders.collect(groupingBy { order: Order => order.city }.then(summing { order: Order => order.amount }))
  3. Run the same accumulator more than once when several results come from one pipeline of values. An Accumulator is a value; each call to collect fills 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)
  4. Write collector(initial, finish:, step:) when the state a run needs is one value. It answers an Accumulator whose accumulator folds step over every value and applies finish once, which covers most custom aggregates without a type of their own.

  5. Write a type with var fields that implements Accumulator when the state is more than one value, the way the standard library's own groupingBy(...).then(...) keeps one accumulator per group in a Map.

Pitfalls#

  • isDone() is what lets a terminal operation stop early. first(), find { ... } and collect behind take(n) never read a source to the end; a custom Accumulator that never overrides isDone() is asked to read everything, which is correct for a sum or a count and wrong for anything that could stop sooner.
  • An Accumulator value 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 to collect again for a different source.
  • fold and a hand-written Accumulator do the same pull. Reach for fold when the state is a simple running value and there is only one call site; reach for a named Accumulator the 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() or ListAccumulator<Item>() for a list, into<Set<Int>>() for anything with From<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 highValue
Esc