Enumerator 클래스
Enumerator 클래스 (Enumerator)
내부 반복(internal iteration)과 외부 반복(external iteration)을 모두 지원하는 클래스예요.
Enumerator는 다음 메서드들로 만들 수 있어요.
Object#to_enumObject#enum_forEnumerator.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가 발생해요. rewind와 feed도 외부 반복 상태를 바꾸므로 같은 예외가 날 수 있어요.
외부 반복이 내부 반복과 크게 다른 이유는 파이버(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 }
::produce를 Enumerable#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가 돌려줄 값을 설정해요. 값을 설정하지 않으면 yield는 nil을 돌려줘요. 이 값은 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. 이 메서드는 yield와 yield 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의 별칭이에요.