Enumerator 클래스
Enumerator 클래스
내부 반복(internal iteration)과 외부 반복(external iteration)을 모두 지원하는 클래스가 Enumerator예요.
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를 연결해서 쓸 수 있어요. 예를 들어 리스트의 각 요소를, 인덱스와 요소를 문자열로 담은 문자열에 매핑하려면:
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가 발생해요. rewind와 feed도 외부 반복 상태를 바꾸므로 역시 FrozenError를 던질 수 있어요.
외부 반복은 Fiber를 쓰기 때문에 내부 반복과 상당히 달라요:
Fiber는 내부 열거에 비해 약간의 오버헤드를 더해요.- 스택 트레이스는
Enumerator에서부터의 스택만 포함하고, 그 위는 포함하지 않아요. - Fiber 로컬 변수는
EnumeratorFiber안에서 상속되지 않아요. 대신 Fiber 로컬 변수 없이 시작해요. Fiber스토리지 변수는 상속되고EnumeratorFiber를 다루도록 설계됐어요.Fiber스토리지 변수에 할당하면 현재Fiber에만 영향을 주므로,EnumeratorFiber의 호출자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 }::produce를Enumerable#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 → nil—e안의 다음 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 → string— e의 출력 가능한 버전을 만들어요. -
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이 던져져요. 환경에서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 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 → 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 → 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 # => nilenumerator 크기는 부정확할 수 있고 힌트로 취급하는 게 좋아요. 예를 들어
::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