Iterator

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Also known as: Cursor

Participants

RoleIn this exampleResponsibility
AggregateWaveHolds the enemies and defines ways through them (#each,
Internal Iterator#each { |kind| ... }The wave drives the loop and yields each enemy to a block.
External IteratorEnumeratorThe caller drives the loop with

Ruby implementation

iterator.rb
# The Aggregate: a wave of enemies. It defines #each and Enumerable supplies
# the rest. Callers never see how the wave stores its enemies.
class Wave
  include Enumerable

  TOUGHNESS = %i[goblin orc troll].freeze

  def initialize(counts)
    @counts = counts
  end

  # The internal iterator: enemies in spawn order, weakest kinds first.
  # Without a block it returns an Enumerator, Ruby's external iterator.
  def each
    return enum_for(:each) unless block_given?

    TOUGHNESS.each do |kind|
      @counts.fetch(kind, 0).times { yield kind }
    end
    self
  end

  # A second way through the same wave: toughest kinds first.
  def each_toughest_first
    return enum_for(:each_toughest_first) unless block_given?

    reverse_each { |kind| yield kind }
    self
  end
end

In use

wave = Wave.new(troll: 1, goblin: 2, orc: 1)

wave.to_a                     # => [:goblin, :goblin, :orc, :troll]
wave.each_toughest_first.to_a # => [:troll, :orc, :goblin, :goblin]
wave.tally                    # => {:goblin=>2, :orc=>1, :troll=>1}

spawner = wave.each           # external iterator
spawner.next                  # => :goblin
spawner.next                  # => :goblin

The external iterator suits the game well: each tick, the spawner asks for next only when it’s time for another enemy.

Notes

Ruby idioms

  • The pattern is built into the language. Define #each, include Enumerable, and you get about 60 methods: map, select, tally, each_slice, lazy…
  • return enum_for(:each) unless block_given? is the idiom that gives callers an external iterator (Enumerator) for free.
  • Several traversals, one aggregate. Any method that yields can be a traversal, like #each_toughest_first here. Callers never see how the wave stores its counts.

Reach for it when

  • A class holds a collection. Making it Enumerable is almost always worth the three lines.
  • Several places repeat the same walk over the same data. Write the walk once, as a method that yields.

Watch for

  • Exposing the inner hash with attr_reader :counts lets callers change it. Yield elements instead.

In Ruby, you can easily make any object iterable by defining #each and including Enumerable. Call #each without a block and you get an external iterator too.