রুবিতে ইটেরেশন করার বিভিন্ন উপায় আছে — লুপ, ব্লক এবং এনামারেটর। অধিকাংশ রুবি প্রোগ্রামার কমপক্ষে লুপ এবং ব্লক সম্পর্কে জানেন কিন্তু 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 # later: 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]]
এটি খুব উপকারী হতে পারে, বিশেষ করে যদি আপনার সিস্টেম ইভেন্ট ব্যবহার করে। রুবি একটি বিল্ট-ইন মেথড প্রদান করে যেকোনো মেথডকে Enumerator জেনারেটর দিয়ে র্যাপ করার জন্য, যা আমাদের ঠিক এই কাজ করতে দেয়। কল্পনা করি আমরা "পুল" করতে চাই রো একটা একটা করে আমাদের with_each_row_of_result- থেকে, বদলে মেথডটি আমাদের কাছে yield করবে।
@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-এ এবং এক্সপেক্ট করবে রিটার্নড ভ্যালু একটি স্ট্রিং — যেটি র্যাক রেসপন্সে লিখে ফেলা হবে। উদাহরণস্বরূপ, আমরা একটি রেঞ্জের #each মেথড কলকে রেইলস রেসপন্স বডি হিসেবে রিটার্ন করতে পারি:
class MyController < ApplicationController def index response.body = ('a'..'z').each end end
এটিই আমি বলি একটি ব্লককে ভেতর বের করা। সারসংক্ষেপে, এটি একটি কন্ট্রোল ফ্লো হেল্পার যা আমাদের অনুমতি দেয় "টাইম ফ্রিজ" করার জন্য একটি ব্লকে (বা একটি লুপ, যেটিও রুবিতে একটি ব্লক) মিড-ফ্লাইট।
তবে, Enumerators-এ একটি লিমিটিং প্রোপার্টি আছে যা সেগুলো কিছুটা কম উপকারী করে। কল্পনা করি আমরা এমন কিছু করতে চাই:
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)
রুবির 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.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 # 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 একটি সাসপেন্ডেড ব্লক কোড ধারণ করে যেটি ম্যানুয়ালি রিজিউম করা যেতে পারে, Fibers ব্যবহার করা যেতে পারে ইভেন্ট রিয়েক্টর ইমপ্লিমেন্ট করতে এবং কনকারেন্ট অপারেশন ডील করতে একটি সিঙ্গেল থ্রেডের ভিতরে। সেগুলো লাইটওয়েট, তাই আপনি একটি সার্ভার ইমপ্লিমেন্ট করতে পারেন Fibers ব্যবহার করে একটি সিঙ্গেল ক্লায়েন্টকে একটি সিঙ্গেল 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
রুবির একটি অডিশনাল স্ট্যান্ডার্ড লাইব্রেরি আছে 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 আর্কাইভ তৈরি করে, এবং কন্ট্রোল করা অসম্ভব কত ডেটা আউটপুট করছে এবং কত gyakran। যদি আমরা আমাদের ZIP চানকস likে, মনে করুন, ৫ 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 # 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 হলো, তাই, একটি কন্ট্রোল ফ্লো মেকানিজম "পুশ" ব্লককে "পুল" অবজেক্টে রূপান্তর করার জন্য যেগুলো মেথড কল এক্সেপ্ট করে।
Fibers এবং Enumerators-এ শুধু একটি পিটফল আছে — আপনার ব্লকে যদি ensure থাকে, বা কিছু কিছু যা ব্লক কমপ্লিট হওয়ার পর করতে হবে, এখন সেটি কলারের ওপর নির্ভর করে আপনাকে যথেষ্টবার কল করতে। একভাবে, এটি কম্পেয়ারেবল কনস্ট্রেইন্টস সাথে যেগুলো আপনার থাকে যখন জাভাস্ক্রিপ্টে Promises ব্যবহার করেন।
উপসংহার
এটি সমাপ্তি করে রুবিতে ফ্লো-কন্ট্রোলড এনামারেবলসে আমাদের লুক। পথে, জুলিক আলোকপাত করেছেন Enumerable এবং Fiber ক্লাসের সাদৃশ্য এবং পার্থক্য, এবং ডুব করেছেন উদাহরণগুলোর মধ্যে যেখানে কলার ডেটার ফ্লো ডিটারমাইন করল। আমরা Fiber-এর অতিরিক্ত ম্যাজিকও শিখেছি প্রতিটি ব্লক রি-এন্ট্রিতে আর্গুমেন্ট পাস করার অনুমতি দিয়ে। হ্যাপি ফ্লো-কন্ট্রোলিং!
ম্যাজিকের একটি স্থির ডোজ পেতে, Ruby Magic সাবস্ক্রাইব করুন এবং আমরা আমাদের মাসিক সংস্করণ সরাসরি আপনার ইনবক্সে ডেলিভার করব।