How do I use tee and uniq in stream pipelines in EK9?

← Collections and Data Structures · Ref: Q133

EK9 provides tee for capturing intermediate pipeline state and uniq for removing duplicate elements. Both are pipeline operations that integrate with cat | collect syntax.

TEE FOR SIDE EFFECTS

Tee copies each element to a variable while passing it through:

  captured <- List() of String
  copy <- cat items | tee in captured | collect as List of String

After the pipeline, captured contains exactly the items that flowed through that point. Useful for debugging or capturing intermediate state.

TEE IS A LAZY PEEK

A head bounds the PULL, so a tee before a head records only the items the head demanded: cat items | tee in seen | head 3 leaves 3 items in seen, and cat items | sort | tee in seen | head 3 also leaves 3 (the sort sees everything, the tee only what is emitted past it). To capture every item, put the tee before a stage that must see everything (sort, tail, join) or drop the head.

TEE FOR DEBUGGING

Insert tee at any point in a pipeline to observe values:

  cat items
    | filter by isValid
    | tee in afterFilter
    | map with transform
    | collect as List of String

afterFilter shows what passed the filter before transformation.

UNIQ KEEPS THE FIRST OF EACH

uniq keeps the first occurrence of each element anywhere in the stream and drops every later one that is == to it - global, not just consecutive repeats:

  cat items | uniq | collect as List of String

No sort is needed; add one only to get the result in order:

  cat items | sort | uniq | collect as List of String

The element's #? (hashcode) only narrows the search; == decides.

UNIQ BY KEY EXTRACTOR

Remove duplicates based on a specific field:

  cat items | uniq by extractKey | collect as List of Item

The key extractor function determines what counts as a duplicate.

COMBINING TEE AND UNIQ

Capture state before deduplication:

  cat items
    | sort
    | tee in beforeUniq
    | uniq
    | collect as List of String

beforeUniq has all sorted items; the result has duplicates removed.

See Q89 for basic stream pipelines. See Q96 for hashcode operator (#?). See Q120 for sort (optional before uniq, only to order the result). See Q124 for group by. See Q235 for complete stream operations reference. See Q237 for streams vs loops decision guide.

Example

defines module qa.collections.teeuniq

  defines function

    firstChar() as pure
      -> word as String
      <- rtn as String: $#<word

    longEnough() as pure
      -> word as String
      <- rtn <- Boolean()
      expectedCount <- 4
      rtn: length word >= expectedCount

  defines program

    TeeUniqDemo()
      stdout <- Stdout()

      words <- ["banana", "apple", "cherry", "apple", "banana", "date", "cherry", "elderberry"]

      // === TEE: CAPTURE INTERMEDIATE STATE ===

      afterFilter <- List() of String
      result <- cat words
        | filter by longEnough
        | tee in afterFilter
        | sort
        | collect as List of String
      stdout.println(`After filter (via tee): ${afterFilter}`)
      stdout.println(`Final sorted: ${result}`)

      // === UNIQ: REMOVE DUPLICATES ===

      // uniq is global first-seen; the sort only orders the result
      unique <- cat words | sort | uniq | collect as List of String
      stdout.println(`Unique: ${unique}`)

      // === UNIQ BY KEY EXTRACTOR ===

      // One word per first character
      uniqueByFirst <- cat words | sort | uniq by firstChar | collect as List of String
      stdout.println(`Unique by first char: ${uniqueByFirst}`)

      // === TEE + UNIQ TOGETHER ===

      beforeUniq <- List() of String
      deduped <- cat words
        | sort
        | tee in beforeUniq
        | uniq
        | collect as List of String
      stdout.println(`Before uniq: ${beforeUniq}`)
      stdout.println(`After uniq: ${deduped}`)

Common mistakes

E50060 — EK9 has no distinct() or stream() methods. Use | uniq in a pipeline to keep the first of each equal element (add | sort to order the result). See ek9 -h E50060 for details.

Incorrect:

unique <- words.stream().distinct().collect()

Correct:

unique <- cat words | sort | uniq | collect as List of String

E07830 — The collect type must match the pipeline's element type. A String pipeline cannot collect into List of Integer. See ek9 -h E07830 for details.

Incorrect:

unique <- cat words | sort | uniq | collect as List of Integer

Correct:

unique <- cat words | sort | uniq | collect as List of String

E07830 — The collect type must match the pipeline's element type. After uniq by firstChar, the stream still contains String elements, not Integer. See ek9 -h E07830 for details.

Incorrect:

uniqueByFirst <- cat words | sort | uniq by firstChar | collect as List of Integer

Correct:

uniqueByFirst <- cat words | sort | uniq by firstChar | collect as List of String

E50060 — EK9 has no distinct() or stream() methods. Use | uniq in a pipeline to keep the first of each equal element (add | sort to order the result). See ek9 -h E50060 for details.

Incorrect:

unique <- words.stream().distinct().collect()

Correct:

unique <- cat words | sort | uniq | collect as List of String

E07830 — The collect type must match the pipeline's element type. A String pipeline cannot collect into List of Integer. See ek9 -h E07830 for details.

Incorrect:

| tee in afterFilter
        | sort
        | collect as List of Integer

Correct:

| tee in afterFilter
        | sort
        | collect as List of String

E07830 — The tee and collect must match the pipeline type. After uniq the stream still contains Strings, so collecting into List of Integer fails. See ek9 -h E07830 for details.

Incorrect:

| tee in beforeUniq
        | uniq
        | collect as List of Integer

Correct:

| tee in beforeUniq
        | uniq
        | collect as List of String

E50060 — EK9 List has no toString() method. Use the $ prefix operator or string interpolation for string conversion. See ek9 -h E50060 for details.

Incorrect:

stdout.println(unique.toString())

Correct:

stdout.println(`Unique: ${unique}`)
Other ways to ask this
  • How do I capture intermediate stream results in EK9?
  • How do I remove duplicates from a stream in EK9?
  • How do I debug a stream pipeline in EK9?
  • How do I take a side copy of data as it flows through a stream pipeline in EK9?
  • How do I observe or inspect elements mid-pipeline in EK9?
  • Capture intermediate sorted results into a side list using tee.
  • I need to save a snapshot of the stream at a mid-pipeline stage
  • In Unix I'd use tee to copy output to a file while continuing the pipeline. Show the EK9 tee
  • Given a sort | head pipeline, which items does a tee between them capture?
  • Tap into a stream pipeline to collect intermediate results without disrupting flow

Coming from another language?

Unix: cmd | tee file.txt | next. Java: stream.peek() for side effects (like tee), stream.distinct() for unique elements. Python: no built-in tee (itertools.tee is different), set() for unique. JavaScript: no built-in pipeline tee, [...new Set(arr)] for unique. Rust: iter.inspect() for tee (lazy, like EK9 tee before head); iter.dedup() only drops CONSECUTIVE repeats, unlike EK9 uniq, which is global first-seen like Java distinct(). Go: manual implementation for both. EK9: | tee in variable for capture, | uniq for global dedup by ==, | uniq by func for dedup by key.

Keywords: inspect, data-structure, observe, collection, copy, stream, unique, intermediate, debug, duplicate, snapshot, uniq, effect, tee, capture, side, side list, pipeline, tap, intercept