How do I create a custom iterator so my type works with cat and for-in?

← Streams and Pipelines · Ref: Q236

To make a custom type work with cat (stream source) and for-in loops, your type needs to provide an iterator() method. EK9 uses the iterator pattern: your type returns an Iterator of T, which the stream or loop consumes.

THE ITERATOR CONTRACT

An iterator must support two operations:

  hasNext()      Returns true if more elements are available.
  next()         Returns the next element and advances.

The for-in loop calls hasNext() before each iteration and next() to get each element. The cat operation does the same to feed elements into a pipeline.

PATTERN 1: DELEGATE TO A COLLECTION

The simplest approach is to store items in a List and delegate iteration:

  Library
    books as List of Book
    iterator() as pure
      <- rtn as Iterator of Book: books.iterator()

This is the most common pattern. Any class that wraps a collection can expose it for iteration.

PATTERN 2: DYNAMIC CLASS ITERATOR

For lazy generation (values computed on demand), create a dynamic class that implements Iterator of T:

  NumberRange
    iterator()
      <- rtn as Iterator of Integer: createIterator(start, limit)
  createIterator()
    -> start as Integer, limit as Integer
    <- rtn as Iterator of Integer?
    current <- Integer(start)
    rtn: (current, limit) is Iterator of Integer as class
      override hasNext() as pure
        <- rtn as Boolean: current? and limit? and current <= limit
      override next()
        <- rtn as Integer: Integer(current)
        current++
      default operator ?

The dynamic class captures 'current' and 'limit' by value. Each call to next() advances the internal position. The hasNext() method checks if more elements remain.

USING WITH FOR-IN

Once your type has iterator(), for-in works automatically:

  range <- NumberRange(1, 5)
  for num in range
    stdout.println($num)

USING WITH CAT (STREAM SOURCE)

The same type works as a stream source:

  range <- NumberRange(1, 10)
  evens <- cat range | filter by isEven | collect as List of Integer

This is the bridge between OOP types and functional stream processing. Any type with iterator() becomes a first-class stream source.

FRESH ITERATORS

The iterator() method should create a fresh iterator each time. This allows multiple independent iterations over the same source.

BUILT-IN ITERABLE TYPES

These types already provide iterators: List, Dict (iterates DictEntry values), Optional (yields 0 or 1 elements), Result (yields ok value if present), and all enumeration types.

See Q65 for for-in loops. See Q85 for Optional as iterator. See Q87 for Result as iterator. See Q89 for stream pipeline basics. See Q99 for enumeration iteration. See Q115 for dynamic classes. See Q224 for enum streams. See Q235 for complete stream operations reference. See Q237 for streams vs loops decision guide.

Example

defines module qa.streams.customiterators

  defines class

    NumberRange
      start as Integer?
      limit as Integer?

      default private NumberRange() as pure

      NumberRange() as pure
        ->
          start as Integer
          limit as Integer
        this.start :=? start
        this.limit :=? limit

      iterator()
        <- rtn as Iterator of Integer: createIterator(start, limit)

      default operator ?

  defines function

    createIterator()
      ->
        start as Integer
        limit as Integer
      <- rtn as Iterator of Integer?

      current <- Integer(start)
      rtn: (current, limit) is Iterator of Integer as class
        override hasNext() as pure
          <- rtn as Boolean: current? and limit? and current <= limit

        override next()
          <- rtn as Integer: Integer(current)
          current++

        default operator ?

    isEven() as pure
      -> num as Integer
      <- rtn as Boolean: num mod 2 == 0

  defines program

    CustomIteratorDemo()
      stdout <- Stdout()

      // === FOR-IN WITH CUSTOM ITERATOR ===

      range <- NumberRange(1, 5)
      stdout.println("For-in iteration:")
      for num in range
        stdout.println(`  ${num}`)

      // === CAT WITH CUSTOM ITERATOR (stream source) ===

      range2 <- NumberRange(1, 10)
      evens <- cat range2 | filter by isEven | collect as List of Integer
      stdout.println(`Even numbers 1-10: ${evens}`)

      // === MULTIPLE ITERATIONS (fresh iterator each time) ===

      range3 <- NumberRange(1, 3)
      first <- cat range3 | collect as List of Integer
      second <- cat range3 | collect as List of Integer
      stdout.println(`First pass: ${first}`)
      stdout.println(`Second pass: ${second}`)

Common mistakes

E50060 — Stdout does not have a display() method. The correct method is println(). Calling a non-existent method triggers E50060 — method not resolved. See ek9 -h E50060 for details.

Incorrect:

stdout.display(`Even numbers 1-10: ${evens}`)

Correct:

stdout.println(`Even numbers 1-10: ${evens}`)
Other ways to ask this
  • How do I make my own type iterable in EK9?
  • How do I implement the iterator pattern for a custom class in EK9?
  • How do I stream my own type with cat in EK9?

Coming from another language?

Java: implement Iterable<T> with iterator() method, Iterator<T> with hasNext() and next(), for-each loop and Stream.of() consume iterables. Python: implement __iter__() returning self with __next__(), raise StopIteration when done, for-in and list comprehensions consume iterables. Rust: implement Iterator trait with next() returning Option<T>, for-in and .iter() consume iterators, IntoIterator for owned iteration. Go: no iterator interface, manual for-range with index, channels for generator pattern. Kotlin: implement Iterable<T> or Iterator<T>, sequence builders for lazy generation. JavaScript: implement Symbol.iterator returning object with next() returning value and done. EK9: provide iterator() method returning Iterator of T, use dynamic class is Iterator of T as class with override hasNext() and override next(), works with both for-in loops and cat stream pipelines.

Keywords: dynamic, class, stream, capture, next, iterator, pipe, for-in, hasNext, source, pipeline, pattern, define, iterable, loop, anonymous, implement, custom, cat, type, closure