How do I use collect as to accumulate stream items into a custom type in EK9?

← Streams and Pipelines · Ref: Q954

The 'collect as Type' stream terminal accumulates pipeline items into a custom type instance.

Your type needs TWO things:

  1. A default constructor (no arguments) to create the initial empty accumulator
  2. An 'operator |' that accepts the pipeline element type to receive each item

HOW IT WORKS

The stream pipeline:

  result <- cat [1, 2, 3] | collect as Counter

Is equivalent to:

  collector <- Counter()         Step 1: default constructor
  collector | 1                  Step 2: pipe each item
  collector | 2
  collector | 3
  result <- collector            Step 3: return accumulator

USING A RECORD

Records are ideal for collect-as because field initializers auto-generate a default constructor:

  Counter
    count as Integer: 0
    operator |
      -> item as Integer
      count++

WITH FOR-RANGE

Combine with for-range to count or sum ranges:

  total <- for i in 1 ... 10 | collect as Counter

If your type only has parameterized constructors (no default), the compiler reports E07835.

See Q235 for all stream operations. See Q931 for tail and skip. See Q122 for collect into built-in types.

Example

defines module qa.streams.collectas

  defines record

    <?-
      A simple counter that accumulates Integer items from a stream.
      Has a default constructor (via field initializer) and operator |.
    -?>
    Counter
      count as Integer: 0

      operator |
        -> item as Integer
        if item?
          count++

      operator $ as pure
        <- rtn as String: String()
        if count?
          rtn :=? $count

      default operator ?

  defines trait

    <?-
      A trait has no constructors — cannot be used with 'collect as' (E07835).
    -?>
    Accumulator
      operator |
        -> item as Integer

  defines program

    CollectAsDemo()
      stdout <- Stdout()

      //Collect integers into a Counter using 'collect as'
      result <- cat [1, 2, 3, 4, 5] | collect as Counter
      stdout.println(`Count: ${result}`)

      //Works with for-range too
      rangeResult <- for i in 1 ... 10 | collect as Counter
      stdout.println(`Range count: ${rangeResult}`)

Common mistakes

E07835 — The collect-as pattern needs to create an initial empty instance via the default constructor. If your type only has parameterized constructors, add a default constructor or use a record with field initializers. See ek9 -h E07835 for details.

Incorrect:

collect as Accumulator

Correct:

collect as Counter
Other ways to ask this
  • How does collect as work with custom types in EK9?
  • What does my type need to work with collect as?
  • What triggers E07835 COLLECT_REQUIRES_DEFAULT_CONSTRUCTOR?

Coming from another language?

Java: Stream.collect(Collector) with Supplier/Accumulator/Combiner. Python: functools.reduce(). Rust: Iterator::fold() or collect() with FromIterator. Go: manual loop accumulation. EK9: collect as Type with default constructor + operator | pattern.

Keywords: counter, record, collect as, E07835, custom type, reduce, default constructor, operator pipe, collect, accumulate, stream, fold, aggregate