रूबी में पुनरावृत्ति करने के विभिन्न तरीके हैं — लूप्स, ब्लॉक्स और एन्यूमरेटर्स। अधिकांश रूबी प्रोग्रामर कम से कम लूप्स और ब्लॉक्स से परिचित हैं लेकिन Enumerator और Fiber अक्सर अंधेरे में रहते हैं। रूबी मैजिक के इस संस्करण में, अतिथि लेखक जूलिक Enumerable और Fiber पर प्रकाश डालते हैं ताकि फ्लो कंट्रोलिंग एन्यूमरेबल्स और ब्लॉक्स को अंदर से बाहर करने की व्याख्या कर सकें।
ब्लॉक्स को निलंबित करना और चेनड इटरेशन
हमने रूबी मैजिक के पिछले संस्करण में Enumerator पर चर्चा की है, जहां हमने बताया कि अपने स्वयं के #each विधि से Enumerator कैसे लौटाया जाए और इसका उपयोग क्या हो सकता है। Enumerator और Fiber के लिए एक और भी व्यापक उपयोग केस यह है कि वे मिड-फ्लाइट में "एक ब्लॉक को निलंबित" कर सकते हैं। न केवल #each या संपूर्ण #each कॉल को दिए गए ब्लॉक, बल्कि कोई भी ब्लॉक!
यह एक बहुत शक्तिशाली निर्माण है, जिसका उपयोग उन विधियों के लिए शिम्स को लागू करने के लिए किया जा सकता है जो कॉलर्स को ब्लॉक का उपयोग करके ब्रिज के रूप में काम करते हैं जो क्रमिक कॉल्स की अपेक्षा करते हैं बजाय ब्लॉक लेने के। उदाहरण के लिए, कल्पना करें कि हम एक डेटाबेस हैंडल खोलना चाहते हैं और प्रत्येक आइटम को पढ़ना चाहते हैं जिसे हमने पुनर्प्राप्त किया है:
db.with_each_row_of_result(sql_stmt) do |row| yield row endब्लॉक API बहुत अच्छा है क्योंकि यह संभावित रूप से हमारे लिए सभी प्रकार की सफाई करेगा जब ब्लॉक समाप्त होता है। हालांकि, कुछ उपभोक्ता डेटाबेस के साथ इस तरह से काम करना चाह सकते हैं:
@cursor = cursor # बाद में: row = @cursor.next_row send_row_to_event_stream(row)अभ्यास में, इसका मतलब है कि हम ब्लॉक के निष्पादन को "बस अभी के लिए" निलंबित करना चाहते हैं और ब्लॉक के भीतर बाद में जारी रखना चाहते हैं। इस प्रकार, कॉलर फ्लो कंट्रोल लेता है बजाय इसके कि यह कैली (ब्लॉक करने वाली विधि) के हाथों में हो।
इटरेटर्स को चेन करना
इस पैटर्न के सबसे आम उपयोगों में से एक कई इटरेटर्स को एक साथ चेन करना है। जब हम ऐसा करते हैं, तो जिन विधियों का हम पुनरावृत्ति के लिए उपयोग करते हैं (जैसे #each), एक Enumerator ऑब्जेक्ट लौटाते हैं, जिसका उपयोग हम 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 # ब्लॉक समाप्त हो गया है और कर्सर खाली है, सफाई हो चुकी है end endयदि हम इसे स्वयं लागू करते, तो यह इस तरह से होने की संभावना है:
cursor = Enumerator.new do |yielder| db.with_each_row_of_result(sql_stmt) do |row| yielder.yield row end endब्लॉक्स को अंदर से बाहर करना
रेल्स हमें रिस्पॉन्स बॉडी को भी एक Enumerator होने की अनुमति देता है। यह हमारे द्वारा रिस्पॉन्स बॉडी के रूप में असाइन किए गए Enumerator पर next को कॉल करेगा और लौटाए गए मान के स्ट्रिंग होने की उम्मीद करेगा — जिसे Rack रिस्पॉन्स में लिखा जाएगा। उदाहरण के लिए, हम रेंज की #each विधि को कॉल को रेल्स रिस्पॉन्स बॉडी के रूप में लौटा सकते हैं:
class MyController < ApplicationController def index response.body = ('a'..'z').each end endयह वही है जिसे मैं ब्लॉक को अंदर से बाहर करना कहता हूं। संक्षेप में, यह एक कंट्रोल फ्लो हेल्पर है जो हमें एक ब्लॉक (या लूप, जो रूबी में एक ब्लॉक भी है) में "समय को फ्रीज" करने की अनुमति देता है।
हालांकि, एन्यूमरेटर्स में एक सीमित संपत्ति है जो उन्हें थोड़ा कम उपयोगी बनाती है। कल्पना करें कि हम ऐसा कुछ करना चाहते हैं:
File.open('output.tmp', 'wb') do |f| # लिखने के लिए फाइल यील्ड करें, लगातार लूप { 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" # ...बाद में writable << "Some more data" writable.finishआदर्श रूप से, हम अपने विधि कॉल को किसी ऐसी संरचना में रैप करेंगे जो हमें निम्नलिखित ट्रिक की अनुमति दे:
write_file_through_encryptor(file_name) do |writable| loop do yield_and_wait_for_next_call(writable) # फिर हम किसी तरह इस लूप से बाहर निकलते हैं ताकि ब्लॉक पूरा हो सके 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| # यहां हम फाइबर के ब्लॉक संदर्भ में प्रवेश करते हैं, पुनर्प्रवेश इस ब्लॉक की शुरुआत में होगा loop do # जब हम Fiber.yield को कॉल करते हैं तो हमारा फाइबर निलंबित हो जाएगा — हम # "data_to_write_or_termination = " असाइनमेंट तक नहीं पहुंचेंगे इससे पहले कि हमारा फाइबर फिर से शुरू हो data_to_write_or_termination = Fiber.yield end end endयह इस तरह काम करता है: जब आप पहली बार अपने deferred_writable पर .resume कॉल करते हैं, तो यह फाइबर में प्रवेश करता है और पहले Fiber.yield स्टेटमेंट या आउटमोस्ट फाइबर ब्लॉक के अंत तक जाता है, जो भी पहले आता है। जब आप Fiber.yield को कॉल करते हैं, तो यह आपको वापस नियंत्रण देता है। Enumerator याद रखें? ब्लॉक को निलंबित किया जा रहा है, और अगली बार जब आप .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 # लूप से बाहर निकलें, या... write_to_output(data_to_write) # ...आउटपुट पर लिखें data_to_write = Fiber.yield # खुद को निलंबित करें और अगले `resume` की प्रतीक्षा करें end # हम यहां पहुंचते हैं यदि हम ऊपर लूप से बाहर निकलते हैं। कोई Fiber.yield नहीं है # स्टेटमेंट कहीं भी, इसलिए फाइबर समाप्त हो जाएगा और "मृत" हो जाएगा। 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 # कॉलर को वापस नियंत्रण दें और इसे हम पर 'resume' कॉल करने के लिए प्रतीक्षा करें end end client_fibers << client_fiber # और फिर अपने मुख्य वेबसर्वर लूप में client_fibers.each do |client_fiber| client_fiber.resume # यदि कोई हो तो क्लाइंट से डेटा प्राप्त करें, और इसे एक 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इसलिए हम ZIP आर्काइव बनाने वाले कोड के हिस्से पर "पुश" कंट्रोल की अनुमति देते हैं, और यह नियंत्रित करना असंभव है कि यह कितना डेटा आउटपुट करता है और कितनी बार। यदि हम अपना ZIP 5 MB के चंक्स में लिखना चाहते हैं — जो 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 # इस बिंदु पर अभी तक कुछ भी उत्पन्न या लिखा नहीं गया है enum = output_enum.each # एक Enumerator बनाएं bin_str = enum.next # ब्लॉक को कुछ बाइनरी डेटा उत्पन्न करने दें और फिर इसे निलंबित करें output.write(bin_str) # हमारा ब्लॉक निलंबित है और `next` के अगले आह्वान की प्रतीक्षा कर रहा हैयह हमें नियंत्रित करने की अनुमति देता है कि हमारा ZIP फाइल जेनरेटर किस दर पर डेटा उत्सर्जित करता है।
एन्यूमरेटर और फाइबर, इसलिए, "पुश" ब्लॉक्स को "पुल" ऑब्जेक्ट्स में बदलने के लिए एक कंट्रोल फ्लो मैकेनिज्म हैं जो मेथड कॉल्स स्वीकार करते हैं।
फाइबर्स और एन्यूमरेटर्स के साथ केवल एक नुकसान है — यदि आपके ब्लॉक में ensure जैसा कुछ है, या ऐसा कुछ है जो ब्लॉक पूरा होने के बाद किया जाना चाहिए, तो अब यह कॉलर पर निर्भर करता है कि वह आपको पर्याप्त बार कॉल करे। एक तरह से, यह जावास्क्रिप्ट में प्रॉमिसेज का उपयोग करते समय आपके सामने आने वाली बाधाओं के बराबर है।
निष्कर्ष
यह रूबी में फ्लो-कंट्रोल्ड एन्यूमरेबल्स में हमारे लुक को समाप्त करता है। रास्ते में, जूलिक ने Enumerable और Fiber क्लासेस के बीच समानताओं और अंतरों पर प्रकाश डाला, और उन उदाहरणों में गोता लगाया जहां कॉलर डेटा के फ्लो को निर्धारित करता है। हमने Fiber के अतिरिक्त जादू के बारे में भी सीखा जो प्रत्येक ब्लॉक पुनर्प्रवेश पर आर्गुमेंट्स पास करने की अनुमति देता है। हैप्पी फ्लो-कंट्रोलिंग!
जादू की एक स्थिर खुराक पाने के लिए, रूबी मैजिक की सदस्यता लें और हम अपना मासिक संस्करण सीधे आपके इनबॉक्स में पहुंचाएंगे।