Ruby에는 반복을 수행하는 다양한 방법(루프, 블록 및 열거자)이 있습니다. 대부분의 Ruby 프로그래머는 최소한 루프와 블록에 익숙하지만 Enumerator
및 Fiber
종종 어둠 속에 머문다. 이번 Ruby Magic 에디션에서는 게스트 작성자 Julik이 Enumerable
을 조명합니다. 및 Fiber
enumerable을 제어하고 블록을 뒤집어서 흐름을 제어하는 방법을 설명합니다.
일시 중단 블록 및 연쇄 반복
Enumerator
를 반환하는 방법을 설명한 Ruby Magic의 이전 버전에서 Enumerator에 대해 논의했습니다. 자신의 #each
에서 방법과 그 용도. Enumerator
의 더욱 광범위한 사용 사례 및 Fiber
그것은 그들이 비행 중 "블록을 일시 중단"할 수 있다는 것입니다. #each
에 주어진 블록뿐만 아니라 또는 #each
에 대한 전체 호출 , 하지만 어떤 블록이든!
이것은 블록을 사용하는 대신 순차적 호출을 기대하는 호출자에 대한 브리지로 블록을 사용하여 작동하는 메서드에 대한 shim을 구현하는 데 사용할 수 있는 매우 강력한 구성입니다. 예를 들어 데이터베이스 핸들을 열고 검색한 각 항목을 읽고 싶다고 상상해 보십시오.
db.with_each_row_of_result(sql_stmt) do |row|
yield row
end
블록 API는 블록이 종료될 때 잠재적으로 모든 종류의 정리를 수행할 수 있기 때문에 훌륭합니다. 그러나 일부 소비자는 다음과 같은 방식으로 데이터베이스 작업을 원할 수 있습니다.
@cursor = cursor
# later:
row = @cursor.next_row
send_row_to_event_stream(row)
실제로는 "지금은" 블록 실행을 "일시 중지"하고 나중에 블록 내에서 계속하기를 원한다는 의미입니다. 따라서 호출자는 호출 수신자(블록을 수행하는 메서드)의 손에 있는 대신 흐름 제어를 인계받습니다.
연이어 반복자
이 패턴의 가장 일반적인 용도 중 하나는 여러 반복자를 함께 연결하는 것입니다. 그렇게 할 때 반복에 사용했던 메소드(예:#each
), 대신 yield
를 사용하여 블록이 우리에게 보내는 값을 "잡는" 데 사용할 수 있는 Enumerator 객체를 반환합니다. 성명:
range = 1..8
each_enum = range.each # => <Enumerator...>
그러면 열거자를 연결할 수 있습니다. 이를 통해 "인덱스를 제외한 모든 반복"과 같은 작업을 수행할 수 있습니다. 이 예에서는 #map
을 호출합니다. 범위에 대해 Enumerable
가져오기 물체. 그런 다음 #with_index
를 연결합니다. 인덱스를 사용하여 범위를 반복하려면:
(1..3).map.with_index {|element_n, index| [element_n, index] }
#=> [[1, 0], [2, 1], [3, 2]]
이것은 특히 시스템이 이벤트를 사용하는 경우 매우 유용할 수 있습니다. Ruby는 Enumerator 생성기로 모든 메서드를 래핑하는 내장 메서드를 제공하므로 정확히 이를 수행할 수 있습니다. with_each_row_of_result
에서 행을 하나씩 "가져오기"한다고 상상해보십시오. , 우리에게 제공하는 방법 대신에.
@cursor = db.to_enum(:with_each_row_of_result, sql_stmt)
schedule_for_later do
begin
row = @cursor.next
send_row_to_event_stream(row)
rescue StopIteration # the block has ended and the cursor is empty, the cleanup has taken place
end
end
이를 직접 구현하면 다음과 같이 될 것입니다.
cursor = Enumerator.new do |yielder|
db.with_each_row_of_result(sql_stmt) do |row|
yielder.yield row
end
end
블록 뒤집기
Rails를 사용하면 응답 본문을 Enumerator로 할당할 수 있습니다. next
를 호출합니다. Enumerator에서 응답 본문으로 할당하고 반환된 값은 Rack 응답에 기록될 문자열이 될 것으로 예상합니다. 예를 들어 #each
에 대한 호출을 반환할 수 있습니다. Rails 응답 본문으로서의 Range 메소드:
class MyController < ApplicationController
def index
response.body = ('a'..'z').each
end
end
이것이 제가 블록을 뒤집는 것이라고 부르는 것입니다. 본질적으로 블록(또는 Ruby의 블록이기도 한 루프)에서 "시간을 정지"할 수 있게 해주는 제어 흐름 도우미입니다.
그러나 열거자에는 약간 덜 유용하게 만드는 제한 속성이 있습니다. 다음과 같이 하고 싶다고 상상해 보세요.
File.open('output.tmp', 'wb') do |f|
# Yield file for writing, continuously
loop { yield(f) }
end
열거자로 감싸서 작성해 보겠습니다.
writer_enum = File.to_enum(:open, 'output.tmp', 'wb')
file = en.next
file << data
file << more_data
모든 것이 잘 작동합니다. 그러나 문제가 있습니다. 열거자가 블록을 "완료"하고 파일을 닫고 종료할 수 있도록 쓰기를 완료했다고 어떻게 알릴 수 있습니까? 이렇게 하면 리소스 정리(파일이 닫힘)와 같은 여러 중요한 단계가 수행되고 버퍼링된 모든 쓰기가 디스크로 플러시됩니다. File
에 대한 액세스 권한이 있습니다. 개체를 닫을 수 있지만 열거자가 우리를 위해 닫는 것을 관리하기를 원합니다. 열거자가 블록을 지나도록 해야 합니다.
또 다른 장애물은 때때로 우리가 일시 중단된 블록 내에서 일어나는 일에 대한 인수를 전달하기를 원한다는 것입니다. 다음과 같은 의미를 가진 블록 수락 메서드가 있다고 상상해 보세요.
write_file_through_encryptor(file_name) do |writable|
writable << "Some data"
writable << "Some more data"
writable << "Even more data"
end
하지만 호출 코드에서는 다음과 같이 사용하려고 합니다.
writable = write_file_through_encryptor(file_name)
writable << "Some data"
# ...later on
writable << "Some more data"
writable.finish
이상적으로는 메서드 호출을 다음과 같은 트릭을 허용하는 구조로 래핑합니다.
write_file_through_encryptor(file_name) do |writable|
loop do
yield_and_wait_for_next_call(writable)
# Then we somehow break out of this loop to let the block complete
end
end
이런 식으로 글을 마무리한다면 어떨까요?
deferred_writable = write_file_through_encryptor(file_name)
deferred_writable.next("Some data")
deferred_writable.next("Some more data")
deferred_writable.next("Even more data")
deferred_writable.next(:terminate)
이 경우 :terminate
를 사용합니다. 블록을 완료하고 반환할 수 있음을 메서드에 알려주는 마법의 값입니다. 이 Enumerator
Enumerator#next
에 인수를 전달할 수 없기 때문에 실제로 도움이 되지 않습니다. . 할 수 있다면 다음과 같이 할 수 있습니다.
deferred_writable = write_file_through_encryptor(file_name)
deferred_writable.next("Some data")
...
deferred_writable.next(:terminate)
Ruby's Fibers 입력
이것이 바로 Fibers가 허용하는 것입니다. Fiber를 사용하면 재진입할 때마다 인수를 수락할 수 있습니다. , 그래서 우리는 다음과 같이 래퍼를 구현할 수 있습니다:
deferred_writable = Fiber.new do |data_to_write_or_termination|
write_file_through_encryptor(filename) do |f|
# Here we enter the block context of the fiber, reentry will be to the start of this block
loop do
# When we call Fiber.yield our fiber will be suspended—we won't reach the
# "data_to_write_or_termination = " assignment before our fiber gets resumed
data_to_write_or_termination = Fiber.yield
end
end
end
작동 방식은 다음과 같습니다. .resume
을 처음 호출할 때 deferred_writable
에서 , 광섬유에 들어가 첫 번째 Fiber.yield
까지 이동합니다. 문 또는 가장 바깥쪽 Fiber 블록의 끝 중 먼저 오는 것. Fiber.yield
를 호출할 때 , 그것은 당신에게 다시 제어를 제공합니다. 열거자를 기억하십니까? 차단이 일시중지됩니다. , 다음에 .resume
을 호출할 때 , resume
에 대한 인수 새로운 data_to_write
가 됩니다. .
deferred_writes = Fiber.new do |data_to_write|
loop do
$stderr.puts "Received #{data_to_write} to work with"
data_to_write = Fiber.yield
end
end
# => #<Fiber:0x007f9f531783e8>
deferred_writes.resume("Hello") #=> Received Hello to work with
deferred_writes.resume("Goodbye") #=> Received Goodbye to work with
따라서 Fiber 내에서 코드 흐름이 시작됩니다. Fiber#resume
에 대한 첫 번째 호출 시 , Fiber.yield
에 대한 첫 번째 호출에서 일시 중단됨 , 그리고 계속 Fiber#resume
에 대한 후속 호출 시 , 반환 값이 Fiber.yield
인 경우 resume
에 대한 인수가 됨 . 코드는 Fiber.yield
가 있는 지점에서 계속 실행됩니다. 마지막으로 호출되었습니다.
이것은 Fiber에 대한 초기 인수가 Fiber.yield
의 반환 값이 아니라 블록 인수로 전달된다는 점에서 Fiber의 약간의 기이함입니다. .
이를 염두에 두고 resume
에 특수 인수를 전달하면 , 중지할지 여부를 Fiber 내에서 결정할 수 있습니다. 시도해 봅시다:
deferred_writes = Fiber.new do |data_to_write|
loop do
$stderr.puts "Received #{data_to_write} to work with"
break if data_to_write == :terminate # Break out of the loop, or...
write_to_output(data_to_write) # ...write to the output
data_to_write = Fiber.yield # suspend ourselves and wait for the next `resume`
end
# We end up here if we break out of the loop above. There is no Fiber.yield
# statement anywhere, so the Fiber will terminate and become "dead".
end
deferred_writes.resume("Hello") #=> Received Hello to work with
deferred_writes.resume("Goodbye") #=> Received Goodbye to work with
deferred_writes.resume(:terminate)
deferred_writes.resume("Some more data after close") # FiberError: dead fiber called
이러한 시설이 매우 유용할 수 있는 상황이 많이 있습니다. Fiber는 수동으로 재개할 수 있는 일시 중단된 코드 블록을 포함하므로 Fiber는 이벤트 리액터를 구현하고 단일 스레드 내에서 동시 작업을 처리하는 데 사용할 수 있습니다. 가볍기 때문에 단일 클라이언트를 단일 Fiber에 할당하고 필요에 따라 이러한 Fiber 개체 간에 전환하여 Fiber를 사용하는 서버를 구현할 수 있습니다.
client_fiber = Fiber.new do |socket|
loop do
received_from_client = socket.read_nonblock(10)
sent_to_client = socket.write_nonblock("OK")
Fiber.yield # Return control back to the caller and wait for it to call 'resume' on us
end
end
client_fibers << client_fiber
# and then in your main webserver loop
client_fibers.each do |client_fiber|
client_fiber.resume # Receive data from the client if any, and send it an OK
end
Ruby에는 fiber
라는 추가 표준 라이브러리가 있습니다. 이를 통해 한 광섬유에서 다른 광섬유로 제어를 명시적으로 전송할 수 있으며 이는 이러한 용도에 대한 추가 기능이 될 수 있습니다.
데이터 방출 속도 제어
루비 블록이 데이터를 내보내는 속도를 제어할 수 있기를 원할 때 파이버 및 열거자의 또 다른 훌륭한 용도가 발생할 수 있습니다. 예를 들어 zip_tricks에서 라이브러리를 사용하는 기본 방법으로 다음 블록 사용을 지원합니다.
ZipTricks::Streamer.open(output_io) do |z|
z.write_deflated_file("big.csv") do |destination|
columns.each do |col|
destination << column
end
end
end
따라서 ZIP 아카이브를 생성하는 코드 부분에서 "푸시" 제어를 허용하며 출력되는 데이터의 양과 빈도를 제어하는 것은 불가능합니다. ZIP를 예를 들어 5MB 단위로 작성하려면(AWS S3 객체 스토리지의 제한 사항임) 사용자 지정 output_io
를 생성해야 합니다. <<
수락을 "거부"하는 개체 이 메서드는 세그먼트를 S3 멀티파트 부분으로 분할해야 할 때 호출합니다. 그러나 컨트롤을 반전하여 "당기기"로 만들 수 있습니다. 우리는 여전히 큰 CSV 파일을 작성하는 데 동일한 블록을 사용하지만 제공하는 출력에 따라 파일을 재개하고 중지합니다. 따라서 다음과 같은 사용이 가능합니다.
output_enum = ZipTricks::Streamer.output_enum do |z|
z.write_deflated_file("big.csv") do |destination|
columns.each do |col|
destination << column
end
end
end
# At this point nothing has been generated or written yet
enum = output_enum.each # Create an Enumerator
bin_str = enum.next # Let the block generate some binary data and then suspend it
output.write(bin_str) # Our block is suspended and waiting for the next invocation of `next`
이를 통해 ZIP 파일 생성기가 데이터를 내보내는 속도를 제어할 수 있습니다.
따라서 Enumerator와 Fiber는 제어 흐름 메커니즘입니다. "push" 블록을 메소드 호출을 허용하는 "pull" 객체로 바꾸기 위한 것입니다.
Fibers 및 Enumerators에는 단 하나의 함정이 있습니다. ensure
와 같은 항목이 있는 경우 블록에서 또는 블록이 완료된 후 수행해야 하는 작업에서 충분한 시간을 호출하는 것은 호출자에게 달려 있습니다. 어떤 면에서는 JavaScript에서 Promise를 사용할 때의 제약 조건과 비슷합니다.
결론
이것으로 Ruby의 흐름 제어 열거형에 대한 조사를 마칩니다. 그 과정에서 Julik은 Enumerable
및 Fiber
클래스, 호출자가 데이터 흐름을 결정한 예제에 뛰어들었습니다. Fiber
에 대해서도 배웠습니다. 의 추가 마법으로 각 블록 재진입에 인수를 전달할 수 있습니다. 즐거운 흐름 제어!
마법을 꾸준히 사용하려면 Ruby Magic을 구독하세요. 월간 에디션을 받은 편지함으로 바로 전달해 드리겠습니다.