Enumerator 클래스

Enumerator 클래스 (Enumerator)

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

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를 서로 연결(chain)할 수 있어요. 예를 들어 리스트의 각 요소를 인덱스와 요소를 담은 문자열로 매핑하려면 이렇게 해요.

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

출처: Ruby 3.3 API

본문

외부 반복 (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만 외부 반복을 사용해요(Array#zip도 내부적으로 next를 사용하죠). 이 메서드들은 다른 내부 열거 메서드에는 영향을 주지 않아요(기본 반복 메서드 자체에 부수효과가 없는 한, 예: IO#each_line).

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

외부 반복이 내부 반복과 크게 다른 이유는 파이버(Fiber)를 사용하기 때문이에요.

  • 파이버는 내부 열거보다 약간의 오버헤드를 더해요.
  • 스택트레이스는 Enumerator 안쪽 스택만 포함하고 그 위쪽은 포함하지 않아요.
  • 파이버-로컬 변수는 Enumerator 파이버 안으로 상속되지 않고, 파이버-로컬 변수 없이 시작해요.
  • 파이버 스토리지 변수는 상속되며 Enumerator 파이버를 다루도록 설계됐어요. 스토리지 변수에 할당하는 건 현재 파이버에만 영향을 주므로, 호출자 파이버의 상태를 바꾸려면 추가적인 간접(indirection)이 필요해요.
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
  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)
end

외부 반복을 내부 반복으로 변환 (Convert External Iteration to Internal Iteration)

외부 반복자를 이용해 내부 반복자를 이렇게 구현할 수 있어요.

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 (Public Class Method)

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) 계산하는 방법을 지정하는 데 써요(Enumerator#size 참고). 값이거나 호출 가능한(callable) 객체일 수 있어요.

::produce (Public Class Method)

produce(initial = nil) { |prev| block } → enumerator — 블록을 반복 호출해서 무한(infinite) 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 같은 메서드와 함께 쓰면 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"]

::product (Public Class Method)

product(*enums) → enumerator, product(*enums) { |elts| ... } → enumerator — 주어진 열거 가능한 객체들의 데카르트 곱(Cartesian product)을 생성하는 새 enumerator를 만들어요. Enumerator::Product.new와 동등해요. 블록을 주면 N-요소 배열을 각각 호출하고 nil을 돌려줘요.

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

#+ (Public Instance Method)

e + enum → enumerator — 이 enumerator와 주어진 enumerable에서 만들어진 enumerator 객체를 돌려줘요.

e = (1..3).each + [4, 5]
e.to_a #=> [1, 2, 3, 4, 5]

#each (Public Instance Method)

each { |elm| block } → obj, each → enum, each(*appending_args) { |elm| block } → obj, each(*appending_args) → an_enumerator — 블록은 이 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 (Public Instance Method)

each_with_index {|(*args), idx| ... }Enumerator#with_index(0)과 같아요. 시작 오프셋이 없어요. 블록이 없으면 인덱스를 포함한 새 Enumerator를 돌려줘요.

#each_with_object (Public Instance Method)

each_with_object(obj) {|(*args), obj| ... } — 각 요소에 대해 임의의 객체 obj를 달고 블록을 반복하고, obj를 돌려줘요. 블록이 없으면 새 Enumerator를 돌려줘요. with_object의 별칭이에요.

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 (Public Instance Method)

feed obj → nil — e 안의 다음 yield가 돌려줄 값을 설정해요. 값을 설정하지 않으면 yieldnil을 돌려줘요. 이 값은 yield된 뒤 사라져요.

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

#inspect

inspect → string — e의 출력 가능한 버전을 만들어요.

#next (Public Instance Method)

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 (Public Instance Method)

next_values → array — 다음 객체를 배열로 돌려주고 내부 위치를 앞으로 이동해요. 끝에 도달하면 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

## 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 (Public Instance Method)

peek → object — 다음 객체를 돌려주되 내부 위치는 이동하지 않아요. 이미 끝이면 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 (Public Instance Method)

peek_values → array — Enumerator#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 (Public Instance Method)

rewind → e — 열거 시퀀스를 처음으로 되감아요. 감싸진 객체가 rewind 메서드에 응답하면 그 메서드가 호출돼요.

#size (Public Instance Method)

size → int, Float::INFINITY 또는 nil — enumerator의 크기를 돌려줘요. 지연 계산할 수 없으면 nil을 돌려줘요.

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

#with_index (Public Instance Method)

with_index(offset = 0) {|(*args), idx| ... }offset부터 시작하는 인덱스를 달고 각 요소에 대해 블록을 반복해요. 블록이 없으면 offset부터 시작하는 인덱스를 포함한 새 Enumerator를 돌려줘요.

#with_object (Public Instance Method)

with_object(obj) {|(*args), obj| ... } — 각 요소에 대해 obj를 달고 블록을 반복하고, obj를 돌려줘요. each_with_object의 별칭이에요.