Computer >> कंप्यूटर >  >> प्रोग्रामिंग >> Ruby

रूबी में फाइबर और एन्यूमरेटर - टर्निंग ब्लॉक्स इनसाइड आउट

रूबी के पास पुनरावृत्ति करने के विभिन्न तरीके हैं- लूप, ब्लॉक और एन्यूमरेटर। अधिकांश रूबी प्रोग्रामर कम से कम लूप और ब्लॉक से परिचित हैं लेकिन Enumerator और Fiber अक्सर अँधेरे में रहना। रूबी मैजिक के इस संस्करण में, अतिथि लेखक जूलिक ने Enumerable . पर प्रकाश डाला है और Fiber फ्लो कंट्रोलिंग एन्यूमरेबल्स और टर्निंग ब्लॉक्स की व्याख्या करने के लिए।

निलंबित ब्लॉक और जंजीर पुनरावृत्ति

हमने रूबी मैजिक के पिछले संस्करण में एन्यूमरेटर पर चर्चा की है, जहां हमने वर्णन किया है कि कैसे एक Enumerator को वापस किया जाए। आपके अपने #each . से विधि और इसके लिए क्या उपयोग किया जा सकता है। Enumerator . के लिए और भी व्यापक उपयोग का मामला और Fiber यह है कि वे मध्य-उड़ान में "ब्लॉक को निलंबित" कर सकते हैं। सिर्फ ब्लॉक ही नहीं #each . को दिया गया है या संपूर्ण कॉल #each . पर , लेकिन कोई ब्लॉक!

यह एक बहुत ही शक्तिशाली निर्माण है, जिसका उपयोग उन तरीकों के लिए शिम को लागू करने के लिए किया जा सकता है जो कॉलर्स के लिए एक पुल के रूप में ब्लॉक का उपयोग करके काम करते हैं जो ब्लॉक लेने के बजाय अनुक्रमिक कॉल की अपेक्षा करते हैं। उदाहरण के लिए, कल्पना कीजिए कि हम एक डेटाबेस हैंडल खोलना चाहते हैं और प्रत्येक आइटम को पढ़ना चाहते हैं जिसे हमने पुनः प्राप्त किया है:

db.with_each_row_of_result(sql_stmt) do |row|
  yield row
end

ब्लॉक एपीआई बहुत अच्छा है क्योंकि ब्लॉक समाप्त होने पर यह संभावित रूप से हमारे लिए सभी प्रकार की सफाई करेगा। हालांकि, कुछ उपभोक्ता डेटाबेस के साथ इस तरह से काम करना चाहेंगे:

@cursor = cursor
 
# later:
row = @cursor.next_row
send_row_to_event_stream(row)

व्यवहार में, इसका मतलब है कि हम "अभी के लिए" ब्लॉक के निष्पादन को "निलंबित" करना चाहते हैं और बाद में ब्लॉक के भीतर जारी रखना चाहते हैं। इस प्रकार, कॉलर कैली (ब्लॉक को निष्पादित करने की विधि) के हाथों में होने के बजाय प्रवाह नियंत्रण लेता है।

चेनिंग इटरेटर्स

इस पैटर्न के सबसे आम उपयोगों में से एक कई पुनरावृत्तियों को एक साथ जोड़ना है। जब हम ऐसा करते हैं, तो जिन विधियों का हम पुनरावृति के लिए उपयोग करते हैं (जैसे #each ), इसके बजाय एक एन्यूमरेटर ऑब्जेक्ट लौटाएं, जिसका उपयोग हम उन मानों को "हथियाने" के लिए कर सकते हैं जो ब्लॉक हमें yield का उपयोग करके भेजता है कथन:

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]]

यह बहुत उपयोगी हो सकता है, खासकर यदि आपका सिस्टम ईवेंट का उपयोग करता है। रूबी एक एन्यूमरेटर जनरेटर के साथ किसी भी विधि को लपेटने के लिए एक अंतर्निहित विधि प्रदान करता है, जो हमें इसे ठीक से पूरा करने की अनुमति देता है। कल्पना कीजिए कि हम अपने 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

ब्लॉक को अंदर से बाहर करना

रेल हमें प्रतिक्रिया निकाय को एक एन्यूमरेटर भी असाइन करने की अनुमति देता है। यह next पर कॉल करेगा एन्यूमरेटर पर हम प्रतिक्रिया निकाय के रूप में असाइन करते हैं और उम्मीद करते हैं कि लौटाया गया मान एक स्ट्रिंग होगा - जिसे रैक प्रतिक्रिया में लिखा जाएगा। उदाहरण के लिए, हम #each . पर कॉल वापस कर सकते हैं रेल प्रतिक्रिया निकाय के रूप में रेंज की विधि:

class MyController < ApplicationController
  def index
    response.body = ('a'..'z').each
  end
end

इसे मैं ब्लॉक को अंदर बाहर करना कहता हूं संक्षेप में, यह एक नियंत्रण प्रवाह सहायक है जो हमें उड़ान के बीच में एक ब्लॉक (या एक लूप, जो रूबी में एक ब्लॉक भी है) में "समय को फ्रीज" करने की अनुमति देता है।

हालांकि, एन्यूमरेटर्स के पास एक सीमित संपत्ति होती है जो उन्हें थोड़ा कम उपयोगी बनाती है। कल्पना कीजिए कि हम कुछ ऐसा करना चाहते हैं:

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)

रूबी के रेशे दर्ज करें

यह वही है जो फाइबर अनुमति देता है। फाइबर आपको प्रत्येक पुनः प्रविष्टि पर तर्क स्वीकार करने . की अनुमति देता है , इसलिए हम अपने रैपर को इस तरह लागू कर सकते हैं:

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.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#resume . पर पहली कॉल पर , Fiber.yield . को पहली कॉल पर निलंबित कर दिया गया , और फिर जारी रखा Fiber#resume . पर बाद में कॉल करने पर , Fiber.yield . के वापसी मूल्य के साथ resume . के लिए तर्क होने के नाते . कोड उस बिंदु से चलना जारी रखता है जहां Fiber.yield . है पिछली बार कॉल किया गया था।

यह फाइबर का एक विचित्र प्रकार है जिसमें फाइबर के लिए प्रारंभिक तर्क आपको ब्लॉक तर्क के रूप में पारित किया जाएगा, न कि Fiber.yield के वापसी मूल्य के माध्यम से। ।

इसे ध्यान में रखते हुए, हम जानते हैं कि resume . के लिए एक विशेष तर्क पारित करके , हम फाइबर के भीतर तय कर सकते हैं कि हमें रुकना चाहिए या नहीं। आइए इसे आजमाएं:

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

ऐसी कई स्थितियां हैं जहां ये सुविधाएं बहुत उपयोगी हो सकती हैं। चूंकि फाइबर में कोड का एक निलंबित ब्लॉक होता है जिसे मैन्युअल रूप से फिर से शुरू किया जा सकता है, फाइबर का उपयोग इवेंट रिएक्टरों को लागू करने और एक ही थ्रेड के भीतर समवर्ती संचालन से निपटने के लिए किया जा सकता है। वे हल्के होते हैं, इसलिए आप एक फाइबर के लिए एक क्लाइंट निर्दिष्ट करके और आवश्यकतानुसार इन फाइबर ऑब्जेक्ट्स के बीच स्विच करके फाइबर का उपयोग कर सर्वर को कार्यान्वित कर सकते हैं।

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

रूबी के पास 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

इसलिए हम कोड के उस हिस्से पर "पुश" नियंत्रण की अनुमति देते हैं जो ज़िप संग्रह बनाता है, और यह नियंत्रित करना असंभव है कि यह कितना डेटा आउटपुट करता है और कितनी बार। अगर हम अपने ज़िप को 5 एमबी के टुकड़ों में लिखना चाहते हैं - जो कि एडब्ल्यूएस एस 3 ऑब्जेक्ट स्टोरेज पर एक सीमा होगी - हमें एक कस्टम 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`

यह हमें नियंत्रित करने की अनुमति देता है कि हमारा ज़िप फ़ाइल जनरेटर किस दर पर डेटा उत्सर्जित करता है।

इसलिए एन्यूमरेटर और फाइबर एक नियंत्रण प्रवाह तंत्र . हैं "पुश" ब्लॉक को "पुल" ऑब्जेक्ट्स में बदलने के लिए जो विधि कॉल स्वीकार करते हैं।

फाइबर और एन्यूमरेटर के साथ केवल एक ही नुकसान है—यदि आपके पास ensure जैसा कुछ है आपके ब्लॉक में, या कुछ ऐसा जिसे ब्लॉक पूरा होने के बाद करने की आवश्यकता है, अब यह कॉलर पर निर्भर है कि वह आपको पर्याप्त बार कॉल करे। एक तरह से, यह जावास्क्रिप्ट में वादों का उपयोग करते समय आपके पास मौजूद बाधाओं से तुलनीय है।

निष्कर्ष

यह रूबी में प्रवाह-नियंत्रित गणनाओं में हमारी नज़र को समाप्त करता है। रास्ते में, जूलिक ने Enumerable . के बीच समानता और अंतर पर प्रकाश डाला और Fiber कक्षाएं, और उन उदाहरणों में कबूतर जहां कॉलर ने डेटा के प्रवाह को निर्धारित किया। हमने Fiber . के बारे में भी सीखा है प्रत्येक ब्लॉक रीएंट्री पर तर्क पारित करने की अनुमति देने के लिए अतिरिक्त जादू। हैप्पी फ्लो-कंट्रोलिंग!

जादू की एक स्थिर खुराक पाने के लिए, रूबी मैजिक की सदस्यता लें और हम अपना मासिक संस्करण सीधे आपके इनबॉक्स में पहुंचाएंगे।


  1. अपने कंप्यूटर को अंदर और बाहर ठीक से कैसे साफ करें

    साफ होने पर सब कुछ बेहतर काम करता है। उल्लेख नहीं है कि अव्यवस्था मारता है। इसलिए अपने कंप्यूटर को नियमित रूप से साफ करना महत्वपूर्ण है और यह वसंत उतना ही अच्छा समय है जितना कि कोई भी। जिस तरह एक बिना साफ-सुथरी हार्ड ड्राइव वाला कंप्यूटर बेहतर और तेज चलता है, उसी तरह बिना डस्ट बिल्ड-अप वाला कंप्यू

  1. रुबोकॉप के साथ लाइनिंग और ऑटो-फॉर्मेटिंग रूबी कोड

    लाइनिंग प्रोग्रामेटिक और शैलीगत त्रुटियों के लिए स्रोत कोड की स्वचालित जाँच है। यह जाँच एक स्थिर कोड विश्लेषण उपकरण द्वारा की जाती है जिसे लिंटर कहा जाता है। एक कोड फ़ॉर्मेटर, हालांकि, स्रोत कोड को स्वरूपित करने से संबंधित एक उपकरण है, ताकि यह नियमों के पूर्व-कॉन्फ़िगर किए गए सेट का सख्ती से पालन कर

  1. लॉगर और लॉगरेज के साथ रूबी में लॉगिंग

    रूबी में लॉग के साथ कार्य करना लॉगिंग उन प्राथमिक कार्यों में से एक है जिसे एक एप्लिकेशन आमतौर पर संबोधित करता है। लॉग का उपयोग तब किया जाता है जब आपको आवश्यकता होती है, उदाहरण के लिए, देखें कि आपके ऐप्स के अंदर क्या हो रहा है, उन पर नज़र रखें, या कुछ विशिष्ट डेटा के लिए मीट्रिक एकत्र करें। एक न