Enumerator 클래스

Enumerator 클래스

내부 반복(internal iteration)과 외부 반복(external iteration)을 모두 지원하는 클래스가 Enumerator예요.

Enumerator는 다음 메서드들로 만들 수 있어요:

  • Object#to_enum
  • Object#enum_for
  • Enumerator.new

대부분의 메서드는 두 가지 형태가 있어요. 열거의 각 항목마다 내용을 평가하는 블록 형태, 그리고 그 반복을 감싼 새 Enumerator를 돌려주는 비블록 형태예요.

enumerator = %w(one two three).each
puts enumerator.class # => Enumerator

enumerator.each_with_object("foo") do |item, obj|
  puts "#{obj}: #{item}"
end

# foo: one
# foo: two
# foo: three

enum_with_obj = enumerator.each_with_object("foo")
puts enum_with_obj.class # => Enumerator

enum_with_obj.each do |item, obj|
  puts "#{obj}: #{item}"
end

# foo: one
# foo: two
# foo: three

이렇게 하면 Enumerator를 연결해서 쓸 수 있어요. 예를 들어 리스트의 각 요소를, 인덱스와 요소를 문자열로 담은 문자열에 매핑하려면:

puts %w[foo bar baz].map.with_index { |w, i| "#{i}:#{w}" }
# => ["0:foo", "1:bar", "2:baz"]

외부 반복(External Iteration)

Enumerator는 외부 반복자로도 쓸 수 있어요. 예를 들어 Enumerator#next는 반복자의 다음 값을 돌려주고, Enumerator가 끝에 도달했으면 StopIteration을 던져요.

e = [1,2,3].each   # returns an enumerator object.
puts e.next   # => 1
puts e.next   # => 2
puts e.next   # => 3
puts e.next   # raises StopIteration

next, next_values, peek, peek_values만이 외부 반복을 쓰는 메서드예요 (그리고 내부적으로 next를 쓰는 Array#zip(Enumerable-not-Array)도).

이 메서드들은, 내부적으로 쓰는 반복 메서드 자체에 부작용이 있지 않는 한(예: IO#each_line), 다른 내부 열거 메서드에는 영향을 주지 않아요.

동결된(frozen) enumerator에 이 메서드들을 호출하면 FrozenError가 발생해요. rewindfeed도 외부 반복 상태를 바꾸므로 역시 FrozenError를 던질 수 있어요.

외부 반복은 Fiber를 쓰기 때문에 내부 반복과 상당히 달라요:

  • Fiber는 내부 열거에 비해 약간의 오버헤드를 더해요.
  • 스택 트레이스는 Enumerator에서부터의 스택만 포함하고, 그 위는 포함하지 않아요.
  • Fiber 로컬 변수는 Enumerator Fiber 안에서 상속되지 않아요. 대신 Fiber 로컬 변수 없이 시작해요.
  • Fiber 스토리지 변수는 상속되고 Enumerator Fiber를 다루도록 설계됐어요. Fiber 스토리지 변수에 할당하면 현재 Fiber에만 영향을 주므로, Enumerator Fiber의 호출자 Fiber에서 상태를 바꾸려면 추가 간접성(예: Fiber 스토리지 변수에 어떤 객체를 넣고 그 ivar를 변경)이 필요해요.

구체적으로:

Thread.current[:fiber_local] = 1
Fiber[:storage_var] = 1
e = Enumerator.new do |y|
  p Thread.current[:fiber_local] # for external iteration: nil, for internal iteration: 1
  p Fiber[:storage_var] # => 1, inherited
  Fiber[:storage_var] += 1
  y << 42
end

p e.next # => 42
p Fiber[:storage_var] # => 1 (it ran in a different Fiber)

e.each { p _1 }
p Fiber[:storage_var] # => 2 (it ran in the same Fiber/"stack" as the current Fiber)

외부 반복을 내부 반복으로 변환하기

외부 반복자로 내부 반복자를 구현할 수 있어요:

def ext_each(e)
  while true
    begin
      vs = e.next_values
    rescue StopIteration
      return $!.result
    end
    y = yield(*vs)
    e.feed y
  end
end

o = Object.new

def o.each
  puts yield
  puts yield(1)
  puts yield(1, 2)
  3
end

# use o.each as an internal iterator directly.
puts o.each {|*x| puts x; [:b, *x] }
# => [], [:b], [1], [:b, 1], [1, 2], [:b, 1, 2], 3

# convert o.each to an external iterator for
# implementing an internal iterator.
puts ext_each(o.to_enum) {|*x| puts x; [:b, *x] }
# => [], [:b], [1], [:b, 1], [1, 2], [:b, 1, 2], 3

클래스 메서드

  • new(size = nil) { |yielder| ... }Enumerable로 쓸 수 있는 새 Enumerator 객체를 만들어요. 반복은 주어진 블록으로 정의되는데, 블록 인자로 주어지는 "yielder" 객체를 yield 메서드(별칭 <<)를 호출해서 값을 내보내는 데 쓸 수 있어요:

    fib = Enumerator.new do |y|
      a = b = 1
      loop do
        y << a
        a, b = b, a + b
      end
    end
    
    fib.take(10) # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    

    선택적 인자로 lazy한 방식의 크기 계산 방법을 지정할 수 있어요 (see Enumerator#size). 값이나 호출 가능한(callable) 객체가 될 수 있어요.

  • produce(initial = nil, size: nil) { |prev| block } → enumerator — 아무 블록에서나 무한 enumerator를 만들어요. 블록을 계속 호출하는 방식이죠. 이전 반복의 결과가 다음 반복에 전달돼요. initial을 주면 첫 번째 반복에 전달되고 enumerator의 첫 번째 요소가 돼요. 주지 않으면 첫 번째 반복은 nil을 받고, 그 결과가 반복자의 첫 번째 요소가 돼요.

    블록에서 StopIteration을 던지면 반복이 멈춰요.

    Enumerator.produce(1, &:succ)   # => enumerator of 1, 2, 3, 4, ....
    
    Enumerator.produce { rand(10) } # => infinite random number sequence
    
    ancestors = Enumerator.produce(node) { |prev| node = prev.parent or raise StopIteration }
    enclosing_section = ancestors.find { |n| n.type == :section }
    

    ::produceEnumerable#detect, Enumerable#slice_after, Enumerable#take_while 같은 Enumerable 메서드와 함께 쓰면 while/until 루프의 Enumerator 기반 대안을 만들 수 있어요:

    # Find next Tuesday
    require "date"
    Enumerator.produce(Date.today, &:succ).detect(&:tuesday?)
    
    # Simple lexer:
    require "strscan"
    scanner = StringScanner.new("7+38/6")
    PATTERN = %r{\d+|[-/+*]}
    Enumerator.produce { scanner.scan(PATTERN) }.slice_after { scanner.eos? }.first
    # => ["7", "+", "38", "/", "6"]
    

    선택적 size 키워드 인자는 enumerator의 크기를 지정하며 Enumerator#size로 얻을 수 있어요. 정수, Float::INFINITY, 호출 가능한 객체(예: lambda), 또는 크기를 알 수 없음을 나타내는 nil이 될 수 있어요. 지정하지 않으면 크기는 기본적으로 Float::INFINITY예요.

    # Infinite enumerator
    enum = Enumerator.produce(1, size: Float::INFINITY, &:succ)
    enum.size  # => Float::INFINITY
    
    # Finite enumerator with known/computable size
    abs_dir = File.expand_path("./baz") # => "/foo/bar/baz"
    traverser = Enumerator.produce(abs_dir, size: -> { abs_dir.count("/") + 1 }) {
      raise StopIteration if it == "/"
      File.dirname(it)
    }
    traverser.size  # => 4
    
    # Finite enumerator with unknown size
    calendar = Enumerator.produce(Date.today, size: nil) {
      it.monday? ? raise(StopIteration) : it + 1
    }
    calendar.size  # => nil
    
  • product(*enums) → enumerator — 주어진 열거 가능한 객체들의 카테시안 곱을 만들어 내는 새 enumerator 객체를 생성해요. Enumerator::Product.new와 동등해요.

    e = Enumerator.product(1..3, [4, 5])
    e.to_a #=> [[1, 4], [1, 5], [2, 4], [2, 5], [3, 4], [3, 5]]
    e.size #=> 6
    

    블록을 주면 만들어지는 각 N-요소 배열로 블록을 호출하고 nil을 돌려줘요.

인스턴스 메서드

  • e + enum → enumerator — 이 enumerator와 주어진 열거 가능한 객체에서 만들어진 enumerator 객체를 돌려줘요.

    e = (1..3).each + [4, 5]
    e.to_a #=> [1, 2, 3, 4, 5]
    
  • each { |elm| block } → obj — 이 Enumerator가 만들어진 방식에 따라 블록을 반복해요. 블록도 인자도 없으면 self를 돌려줘요.

    "Hello, world!".scan(/\w+/)                     #=> ["Hello", "world"]
    "Hello, world!".to_enum(:scan, /\w+/).to_a      #=> ["Hello", "world"]
    "Hello, world!".to_enum(:scan).each(/\w+/).to_a #=> ["Hello", "world"]
    
    obj = Object.new
    
    def obj.each_arg(a, b=:b, *rest)
      yield a
      yield b
      yield rest
      :method_returned
    end
    
    enum = obj.to_enum :each_arg, :a, :x
    
    enum.each.to_a                  #=> [:a, :x, []]
    enum.each.equal?(enum)          #=> true
    enum.each { |elm| elm }         #=> :method_returned
    
    enum.each(:y, :z).to_a          #=> [:a, :x, [:y, :z]]
    enum.each(:y, :z).equal?(enum)  #=> false
    enum.each(:y, :z) { |elm| elm } #=> :method_returned
    
  • each_with_index {|(*args), idx| ... }Enumerator#with_index(0)와 같아요. 즉 시작 오프셋이 없어요. 블록을 주지 않으면 인덱스를 포함한 새 Enumerator를 돌려줘요.

  • each_with_object(obj) {|(*args), obj| ... } — 각 요소마다 임의의 객체 obj와 함께 주어진 블록을 반복하고 obj를 돌려줘요. 블록을 주지 않으면 새 Enumerator를 돌려줘요.

    to_three = Enumerator.new do |y|
      3.times do |x|
        y << x
      end
    end
    
    to_three_with_string = to_three.with_object("foo")
    to_three_with_string.each do |x,string|
      puts "#{string}: #{x}"
    end
    
    # => foo: 0
    # => foo: 1
    # => foo: 2
    
  • feed obj → nile 안의 다음 yield가 돌려줄 값을 설정해요. 값을 설정하지 않으면 yield는 nil을 돌려줘요. 이 값은 yield된 뒤에 지워져요.

    # Array#map passes the array's elements to "yield" and collects the
    # results of "yield" as an array.
    # Following example shows that "next" returns the passed elements and
    # values passed to "feed" are collected as an array which can be
    # obtained by StopIteration#result.
    e = [1,2,3].map
    p e.next           #=> 1
    e.feed "a"
    p e.next           #=> 2
    e.feed "b"
    p e.next           #=> 3
    e.feed "c"
    begin
      e.next
    rescue StopIteration
      p $!.result      #=> ["a", "b", "c"]
    end
    
    o = Object.new
    def o.each
      x = yield         # (2) blocks
      p x               # (5) => "foo"
      x = yield         # (6) blocks
      p x               # (8) => nil
      x = yield         # (9) blocks
      p x               # not reached w/o another e.next
    end
    
    e = o.to_enum
    e.next              # (1)
    e.feed "foo"        # (3)
    e.next              # (4)
    e.next              # (7)
                        # (10)
    
  • inspect → stringe의 출력 가능한 버전을 만들어요.

  • next → object — enumerator에서 다음 객체를 돌려주고 내부 위치를 앞으로 이동시켜요. 위치가 끝에 도달하면 StopIteration이 던져져요.

    a = [1,2,3]
    e = a.to_enum
    p e.next   #=> 1
    p e.next   #=> 2
    p e.next   #=> 3
    p e.next   #raises StopIteration
    

    외부 반복자에 대한 클래스 차원의 참고 사항을 보세요.

  • next_values → array — enumerator에서 다음 객체를 배열로 돌려주고 내부 위치를 앞으로 이동시켜요. 위치가 끝에 도달하면 StopIteration이 던져져요. 환경에서 yieldyield nil을 구분할 수 있어요.

    o = Object.new
    def o.each
      yield
      yield 1
      yield 1, 2
      yield nil
      yield [1, 2]
    end
    e = o.to_enum
    p e.next_values
    p e.next_values
    p e.next_values
    p e.next_values
    p e.next_values
    e = o.to_enum
    p e.next
    p e.next
    p e.next
    p e.next
    p e.next
    
    ## yield args       next_values      next
    #  yield            []               nil
    #  yield 1          [1]              1
    #  yield 1, 2       [1, 2]           [1, 2]
    #  yield nil        [nil]            nil
    #  yield [1, 2]     [[1, 2]]         [1, 2]
    
  • peek → object — enumerator에서 다음 객체를 돌려주지만 내부 위치를 앞으로 이동시키지 않아요. 위치가 이미 끝이면 StopIteration이 던져져요.

    a = [1,2,3]
    e = a.to_enum
    p e.next   #=> 1
    p e.peek   #=> 2
    p e.peek   #=> 2
    p e.peek   #=> 2
    p e.next   #=> 2
    p e.next   #=> 3
    p e.peek   #raises StopIteration
    
  • peek_values → arrayEnumerator#next_values처럼 다음 객체를 배열로 돌려주지만 내부 위치를 앞으로 이동시키지 않아요. 위치가 이미 끝이면 StopIteration이 던져져요.

    o = Object.new
    def o.each
      yield
      yield 1
      yield 1, 2
    end
    e = o.to_enum
    p e.peek_values    #=> []
    e.next
    p e.peek_values    #=> [1]
    p e.peek_values    #=> [1]
    e.next
    p e.peek_values    #=> [1, 2]
    e.next
    p e.peek_values    # raises StopIteration
    
  • rewind → e — 열거 순서를 처음으로 되감아요. 감싼 객체가 "rewind" 메서드에 응답하면 그 메서드가 호출돼요.

  • size → int, Float::INFINITY or nil — enumerator의 크기를 돌려주거나, lazy하게 계산할 수 없으면 nil을 돌려줘요.

    (1..100).to_a.permutation(4).size # => 94109400
    loop.size # => Float::INFINITY
    (1..100).drop_while.size # => nil
    

    enumerator 크기는 부정확할 수 있고 힌트로 취급하는 게 좋아요. 예를 들어 ::new에 준 크기가 정확한지 확인하지 않아요:

    e = Enumerator.new(5) { |y| 2.times { y << it} }
    e.size # => 5
    e.to_a.size # => 2
    

    또 다른 예시는 size 인자 없이 ::produce로 만든 enumerator예요. 이런 enumerator는 크기에 Infinity를 돌려주지만, 넘긴 블록이 StopIteration을 던지면 부정확해요:

    e = Enumerator.produce(1) { it + 1 }
    e.size # => Infinity
    
    e = Enumerator.produce(1) { it > 3 ? raise(StopIteration) : it + 1 }
    e.size # => Infinity
    e.to_a.size # => 4
    
  • with_index(offset = 0) {|(*args), idx| ... } — 각 요소마다 offset에서 시작하는 인덱스와 함께 주어진 블록을 반복해요. 블록을 주지 않으면 offset에서 시작하는 인덱스를 포함한 새 Enumerator를 돌려줘요. offset은 시작 인덱스예요.

  • with_object(obj) {|(*args), obj| ... } — 각 요소마다 임의의 객체 obj와 함께 주어진 블록을 반복하고 obj를 돌려줘요. 블록을 주지 않으면 새 Enumerator를 돌려줘요.

    to_three = Enumerator.new do |y|
      3.times do |x|
        y << x
      end
    end
    
    to_three_with_string = to_three.with_object("foo")
    to_three_with_string.each do |x,string|
      puts "#{string}: #{x}"
    end
    
    # => foo: 0
    # => foo: 1
    # => foo: 2
    

출처: Ruby 4.0 API - Enumerator