কম্পিউটার

রুবিতে ফাইবার এবং গণনাকারী - ভিতরে বাঁকানো ব্লক

রুবির পুনরাবৃত্তি করার বিভিন্ন উপায় রয়েছে—লুপ, ব্লক এবং গণনাকারী। বেশিরভাগ রুবি প্রোগ্রামার অন্তত লুপ এবং ব্লকের সাথে পরিচিত কিন্তু Enumerator এবং Fiber প্রায়ই অন্ধকারে থাকুন। রুবি ম্যাজিকের এই সংস্করণে, অতিথি লেখক জুলিক Enumerable-এ আলোকপাত করেছেন এবং Fiber প্রবাহ নিয়ন্ত্রক গণনাযোগ্য এবং ভিতরে বাঁক ব্লক ব্যাখ্যা করতে.

সাসপেন্ডিং ব্লক এবং চেইনড ইটারেশন

আমরা রুবি ম্যাজিকের পূর্ববর্তী সংস্করণে গণনাকারী নিয়ে আলোচনা করেছি, যেখানে আমরা বর্ণনা করেছি কিভাবে একটি Enumerator ফেরত দিতে হয়। আপনার নিজের #each থেকে পদ্ধতি এবং এটি কি জন্য ব্যবহার করা যেতে পারে। 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 ), পরিবর্তে একটি গণনাকারী অবজেক্ট ফেরত দিন, যেটি আমরা 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

এমন অনেকগুলি পরিস্থিতি রয়েছে যেখানে এই সুবিধাগুলি খুব কার্যকর হতে পারে। যেহেতু একটি ফাইবারে কোডের একটি স্থগিত ব্লক রয়েছে যা ম্যানুয়ালি পুনরায় চালু করা যেতে পারে, তাই ফাইবারগুলি ইভেন্ট রিঅ্যাক্টরগুলি বাস্তবায়নের জন্য এবং একক থ্রেডের মধ্যে সমসাময়িক ক্রিয়াকলাপগুলির সাথে ডিল করার জন্য ব্যবহার করা যেতে পারে। এগুলি হালকা, তাই আপনি একটি একক ফাইবারে একটি একক ক্লায়েন্ট বরাদ্দ করে এবং প্রয়োজনীয় হিসাবে এই ফাইবার বস্তুগুলির মধ্যে স্যুইচ করে Fibers ব্যবহার করে একটি সার্ভার বাস্তবায়ন করতে পারেন৷

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

এটি আমাদের জিপ ফাইল জেনারেটর কোন হারে ডেটা নির্গত করে তা নিয়ন্ত্রণ করতে দেয়।

গণনাকারী এবং ফাইবার, অতএব, একটি নিয়ন্ত্রণ প্রবাহ প্রক্রিয়া "পুশ" ব্লকগুলিকে "টান" অবজেক্টে পরিণত করার জন্য যা পদ্ধতি কলগুলি গ্রহণ করে।

ফাইবার এবং গণনাকারীদের সাথে শুধুমাত্র একটি সমস্যা আছে—যদি আপনার কাছে ensure এর মত কিছু থাকে আপনার ব্লকে, বা ব্লক সম্পূর্ণ হওয়ার পরে কিছু করতে হবে, এটি এখন কলারের উপর নির্ভর করে যে আপনাকে পর্যাপ্ত সময় কল করবে। একটি উপায়ে, এটি জাভাস্ক্রিপ্টে প্রতিশ্রুতি ব্যবহার করার সময় আপনার সীমাবদ্ধতার সাথে তুলনীয়৷

উপসংহার

এটি রুবিতে প্রবাহ-নিয়ন্ত্রিত গণনাগুলির দিকে আমাদের দৃষ্টিভঙ্গি শেষ করে। পথ ধরে, জুলিক Enumerable-এর মধ্যে মিল এবং পার্থক্যের উপর আলোকপাত করেছেন এবং Fiber ক্লাস, এবং ঘুঘু উদাহরণ যেখানে কলকারী ডেটা প্রবাহ নির্ধারণ করে। আমরা Fiber সম্পর্কেও শিখেছি প্রতিটি ব্লক পুনঃপ্রবেশের যুক্তি পাস করার অনুমতি দিতে এর অতিরিক্ত জাদু। শুভ প্রবাহ-নিয়ন্ত্রণ!

ম্যাজিকের একটি স্থির ডোজ পেতে, রুবি ম্যাজিকের সদস্যতা নিন এবং আমরা আমাদের মাসিক সংস্করণ সরাসরি আপনার ইনবক্সে পৌঁছে দেব।


  1. কীভাবে আপনার কম্পিউটার সঠিকভাবে, ভিতরে এবং বাইরে পরিষ্কার করবেন

  2. RuboCop সহ রুবি কোড লিন্টিং এবং স্বয়ংক্রিয় ফর্ম্যাটিং

  3. Logger এবং Lograge সঙ্গে রুবি লগ ইন করুন

  4. Ruby's redo, retry and next keywords