পাঠ ৪৪ · ৫৮-এর মধ্যে · মডিউল ৯
Home / Courses / Concepts of Programming Languages & Compiler Design / কোরুটিন ও কন্টিনিউয়েশন

কোরুটিন ও কন্টিনিউয়েশন

Coroutines & continuations
৮ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কোরুটিন — সংজ্ঞা এবং সাধারণ ফাংশন-কল মডেলের সাথে এর মৌলিক পার্থক্য
  • Python-এর generator/yield কীভাবে কোরুটিনের একটি বাস্তব উদাহরণ
  • কন্টিনিউয়েশন — একটি আরও সাধারণ ও তাত্ত্বিক ধারণা হিসেবে (Scheme-এর call/cc-এর সংক্ষিপ্ত উল্লেখসহ)
  • Python দিয়ে সত্যিকারের generator বাস্তবায়ন — ম্যানুয়াল next() কল এবং দুটো স্বাধীন ইন্টারলিভড ইনস্ট্যান্স

১ · কোরুটিন — পজ ও রিজিউম করতে পারা একটি ফাংশন

একটি সাধারণ ফাংশন কল/রিটার্ন মডেল কড়াভাবে "এক-দিকমুখী" ও "এক-বার" — একবার একটি ফাংশন রিটার্ন করলে, তার লোকাল স্টেট (ভ্যারিয়েবল, এক্সিকিউশনের অবস্থান) চিরতরে হারিয়ে যায় (M8/L39-এর স্ট্যাক-ডাইনামিক লাইফটাইমের সরাসরি প্রতিফলন — ফাংশন রিটার্নের সাথে সাথে তার অ্যাক্টিভেশন রেকর্ড বিলুপ্ত হয়ে যায়)। কোরুটিনCoroutineএকটি ফাংশন যা এক্সিকিউশন মাঝপথে পজ করে (সম্পূর্ণ লোকাল স্টেট ধরে রেখে) এবং পরে ঠিক সেই বিন্দু থেকে রিজিউম হতে পারে, একাধিকবার। এই মডেলের একটি সত্যিকারের জেনারেলাইজেশন — একটি কোরুটিন কলারের কাছে বারবার নিয়ন্ত্রণ ফিরিয়ে দিতে (yield) পারে, এবং প্রতিবার পরবর্তী রিজাম্পশনের জন্য তার স্টেট সংরক্ষিত থাকে। বাস্তব, প্রত্যক্ষ প্রাসঙ্গিকতা — Python-এর generator ফাংশন (yield ব্যবহার করে) এই ঠিক ধারণারই একটি প্রচলিত, বাস্তবিক রূপ, যা বেশিরভাগ Python প্রোগ্রামার ইতিমধ্যেই ব্যবহার করেছেন।

কলার next() কল করে generator যেখানে ছিল সেখান থেকে চলা শুরু পরের yield পর্যন্ত এক্সিকিউট, মান ফেরত, স্টেট সেভ পরের next() ফাংশন শেষ হলে StopIteration — কোরুটিন সম্পূর্ণ শেষ
প্রতিটি next() কল কোরুটিনকে ঠিক আগের পজ-পয়েন্ট থেকে চালিয়ে পরের yield পর্যন্ত নিয়ে যায় — স্টেট প্রতিবারই সংরক্ষিত থাকে।

২ · কন্টিনিউয়েশন — বাকি কম্পিউটেশনের একটি প্রথম-শ্রেণির প্রতিরূপ

কন্টিনিউয়েশনContinuationপ্রোগ্রামের এক্সিকিউশনের যেকোনো বিন্দুতে "বাকি যা কিছু ঘটার কথা" তার একটি বিমূর্ত প্রতিরূপ — এমন একটি মান যা সেভ, পাস, এবং পরে (একাধিকবারও) ইনভোক করা যায়। একটি আরও সাধারণ, বেশি তাত্ত্বিক ধারণা — প্রোগ্রামের এক্সিকিউশনের যেকোনো নির্দিষ্ট বিন্দুতে "বাকি কম্পিউটেশন" ঠিক কী হবে, তার একটি বিমূর্ত প্রতিরূপ। যেসব ভাষা এই কনসেপ্ট সাপোর্ট করে (উল্লেখমাত্র, যেমন Scheme-এর call/cc — বাস্তবায়ন করার দরকার নেই এখানে), সেখানে একটি কন্টিনিউয়েশনকে একটি প্রথম-শ্রেণির মান হিসেবে সেভ করা, অন্য জায়গায় পাস করা, এমনকি পরে (একাধিকবারও, স্বাভাবিক সিকোয়েনশিয়াল অর্ডারের বাইরে গিয়েও) ইনভোক করা যায়। সততার সাথে বলা দরকার — এটি কোরুটিনের চেয়েও বেশি উন্নত ও তুলনামূলক বিরল বাস্তব-ব্যবহারের একটি ফিচার — কন্ট্রোল-ফ্লো মডিউলের ধারণাগত সম্পূর্ণতার জন্যই এখানে উল্লেখ করা হলো, প্রতিদিনের প্রোগ্রামিং টুল হিসেবে নয়।

কোরুটিন বনাম কন্টিনিউয়েশন

দুটোই "কন্ট্রোল ফ্লো"-কে সরল রৈখিক কল/রিটার্নের বাইরে নিয়ে যায় — কিন্তু কোরুটিন একটি নির্দিষ্ট, সীমিত প্যাটার্নে (পজ, তারপর ঠিক সেখান থেকে রিজিউম) কাজ করে, যেখানে কন্টিনিউয়েশন একটি সম্পূর্ণ সাধারণ মেকানিজম — "বাকি সবকিছু" নিজেই একটি মান, যা দিয়ে তাত্ত্বিকভাবে কোরুটিন-সহ কন্ট্রোল ফ্লোর প্রায় যেকোনো প্যাটার্নই বানানো সম্ভব। বাস্তবে বেশিরভাগ ভাষা সরাসরি কন্টিনিউয়েশন এক্সপোজ না করে শুধু কোরুটিন (বা তার মতো সীমিত ফিচার) দেয় — কারণ এটাই বেশিরভাগ বাস্তব ব্যবহারের জন্য যথেষ্ট এবং বোঝা/ডিবাগ করা সহজ।

৩ · কোড: real generator দিয়ে পজ/রিজিউম, ও দুটো স্বাধীন ইনস্ট্যান্স

নিচের কোড সেলে একটি real Python generator ফাংশন — countdown_coroutine(n) — যা n, n-1, ..., 1 yield করে, প্রতিটি yield-এর পরে পজ করে। প্রথমে এটাকে ম্যানুয়াল next() কল দিয়ে (একটি for লুপ ব্যবহার না করে, যাতে প্রতিটি আলাদা পজ/রিজিউম ধাপ দৃশ্যমান থাকে) চালানো হয়েছে। তারপর একই ফাংশনের দুটো সম্পূর্ণ আলাদা ইনস্ট্যান্স তৈরি করে তাদের next() কল একে অপরের সাথে ইন্টারলিভ (পালাক্রমে) করে দেখানো হয়েছে — প্রতিটি নিজের স্বাধীন পজড স্টেট বজায় রাখে।

Python
def countdown_coroutine(n):
    print(f"  [কোরুটিন শুরু, n={n}]")
    while n > 0:
        yield n
        print(f"  [রিজিউম হলো, পরের মান তৈরি হবে]")
        n -= 1
    print("  [কোরুটিন শেষ]")

print("=== একক জেনারেটর, ম্যানুয়াল next() কল ===")
gen = countdown_coroutine(3)
print("next() কল ১:", next(gen))
print("next() কল ২:", next(gen))
print("next() কল ৩:", next(gen))
try:
    next(gen)
except StopIteration:
    print("next() কল ৪: StopIteration -- জেনারেটর সম্পূর্ণ শেষ")

print()
print("=== দুটো স্বাধীন জেনারেটর ইনস্ট্যান্স, ইন্টারলিভড ===")
gen_a = countdown_coroutine(3)
gen_b = countdown_coroutine(2)

print("gen_a ->", next(gen_a))
print("gen_b ->", next(gen_b))
print("gen_a ->", next(gen_a))
print("gen_b ->", next(gen_b))
print("gen_a ->", next(gen_a))

    
লক্ষ্য করুন প্রথম অংশে — প্রতিটি next() কল ঠিক এক ধাপ এগিয়ে যায়: মান রিটার্ন করে, তারপর থেমে যায়, পরের next() কল না আসা পর্যন্ত। চতুর্থ কলে n শূন্যে পৌঁছে যাওয়ায় ফাংশনটি সম্পূর্ণ শেষ হয়ে যায় এবং StopIteration রেইজ করে — ঠিক যেমন উপরের ফ্লো-ডায়াগ্রামে দেখানো হয়েছে। দ্বিতীয় অংশে gen_a ও gen_b-এর next() কল পালাক্রমে চললেও, প্রতিটি জেনারেটর তার নিজের n-এর মান স্বাধীনভাবে মনে রাখে — gen_b-এর কল কখনো gen_a-এর অবস্থান পরিবর্তন করে না, এবং উল্টোটাও নয়।
মূল কথা · Key takeaway

কোরুটিন প্রমাণ করে যে "ফাংশন একবার কল হয়ে একবার রিটার্ন করবে" — এটা কোনো মৌলিক সীমাবদ্ধতা নয়, বরং একটি সাধারণ ডিজাইন চয়েস, যা কোরুটিনের মতো কনস্ট্রাক্ট দিয়ে সাধারণীকরণ করা যায়। Python-এর generator এই ধারণাকে দৈনন্দিন, ব্যবহারযোগ্য একটি টুলে রূপান্তরিত করেছে; কন্টিনিউয়েশন একই দিকের একটি আরও গভীর, আরও সাধারণ তাত্ত্বিক সম্প্রসারণ।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ একটি সাধারণ ফাংশনের সাথে একটি কোরুটিনের মৌলিক পার্থক্য কী, ঠিক কোন জিনিসটা "সংরক্ষিত" থাকে?

একটি সাধারণ ফাংশন রিটার্ন করার সাথে সাথে তার সম্পূর্ণ লোকাল স্টেট (ভ্যারিয়েবল, এক্সিকিউশনের ঠিক কোন লাইনে ছিল) হারিয়ে যায় — পরের বার কল করলে সবকিছু নতুন করে শুরু হয়। একটি কোরুটিন yield করলে তার স্টেট হারায় না — লোকাল ভ্যারিয়েবলের মান, এবং yield স্টেটমেন্টের ঠিক পরের বিন্দু পর্যন্ত এক্সিকিউশন পজিশন — দুটোই সংরক্ষিত থাকে। উপরের কোড সেলে countdown_coroutine-এর n ভ্যারিয়েবল প্রতিটি next() কলের মাঝে ঠিক আগের মানই মনে রাখে — এটাই সেই সংরক্ষণের প্রমাণ।

প্র ০২ দ্বিতীয় ডেমোতে gen_a ও gen_b-এর next() কল ইন্টারলিভ করা সত্ত্বেও কেন একে অপরের স্টেটে হস্তক্ষেপ করে না?

কারণ countdown_coroutine(3) ও countdown_coroutine(2) — প্রতিটি কল আসলে একটি সম্পূর্ণ নতুন, স্বাধীন জেনারেটর অবজেক্ট তৈরি করে, প্রতিটির নিজস্ব আলাদা মেমরিতে n-এর নিজস্ব কপি সহ। gen_a ও gen_b — এই দুটো ভ্যারিয়েবল দুটো সম্পূর্ণ ভিন্ন অবজেক্টকে নির্দেশ করে, যদিও দুটোই একই ফাংশন-বডি থেকে তৈরি। তাই next(gen_a) শুধু gen_a-এর নিজের সংরক্ষিত অবস্থানকে এগিয়ে নেয় — gen_b-এর অবস্থান সম্পূর্ণ অপরিবর্তিত থাকে, কারণ সেটি একটি সম্পূর্ণ ভিন্ন স্টেট।

প্র ০৩ কন্টিনিউয়েশনকে "কোরুটিনের চেয়ে বেশি সাধারণ" বলা হলো কেন — একটি স্বজ্ঞাত ব্যাখ্যা দিন।

একটি কোরুটিন শুধু একটি নির্দিষ্ট প্যাটার্নে পজ/রিজিউম করতে পারে — এটি নিয়ন্ত্রণ ফিরিয়ে দেয় (yield) এবং প্রত্যাশা করে যে পরবর্তী next() কলটি ঠিক সেই বিন্দু থেকেই আবার শুরু করবে, স্বাভাবিক সিকোয়েনশিয়াল অর্ডারে। একটি কন্টিনিউয়েশন আরও নমনীয় — এটি "বাকি পুরো কম্পিউটেশন"-কে একটি সাধারণ মান হিসেবে ধরে রাখে, যা প্রয়োজনে একাধিকবার ইনভোক করা যায় (একটি "সেভ পয়েন্টে" বারবার ফিরে যাওয়ার মতো), অথবা সম্পূর্ণ ভিন্ন জায়গা থেকেও কল করা যায় — শুধু ঠিক পরের ধাপেই সীমাবদ্ধ নয়। এই বাড়তি নমনীয়তার কারণেই এটি তাত্ত্বিকভাবে কোরুটিন-সহ আরও অনেক কন্ট্রোল-ফ্লো প্যাটার্ন প্রকাশ করতে সক্ষম, যদিও সেই ক্ষমতা বাস্তবে খুব কম ভাষায় সরাসরি এক্সপোজ করা হয়।

অনুশীলন

  1. চিন্তা করুন: উপরের কোড সেলে countdown_coroutine(3)-এর বদলে countdown_coroutine(1) দিয়ে একক জেনারেটর অংশটি আবার ট্রেস করুন। মোট কয়টি সফল next() কল হবে, তারপর কোনটিতে StopIteration আসবে?

    n=1 দিয়ে শুরু হলে লুপ কন্ডিশন n > 0 একবারই সত্য হয় — তাই একটিমাত্র সফল next() কল হবে, যা 1 রিটার্ন করবে। দ্বিতীয় next() কলে n ইতিমধ্যে 0-এ নেমে যাওয়ায় লুপ শেষ হয়ে যাবে এবং ফাংশন সম্পূর্ণ শেষ হয়ে StopIteration রেইজ করবে — অর্থাৎ মোট ১টি সফল কল, তারপরের কলেই StopIteration।

  2. পরীক্ষা করুন: দ্বিতীয় ডেমোতে gen_b = countdown_coroutine(2)-এর বদলে gen_b = countdown_coroutine(5) করে Run চেপে দেখুন — শেষ তিনটি next() কলের (gen_a, gen_b, gen_a) আউটপুট কী হবে।

    gen_a = countdown_coroutine(3) অপরিবর্তিত থাকায় তার আউটপুট আগের মতোই — next(gen_a) ধারাবাহিকভাবে 3, 2, 1 দেবে। gen_b = countdown_coroutine(5) হওয়ায় প্রথম next(gen_b) কল 5 দেবে (আগে 2 দিত)। মূল বিষয়টি অপরিবর্তিত থাকে — gen_a ও gen_b সম্পূর্ণ স্বাধীন, তাই gen_b-এর শুরুর মান বদলে দিলেও gen_a-এর ধারাবাহিকতা (3, তারপর 2, তারপর 1) একেবারেই প্রভাবিত হয় না।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
এক্সেপশন হ্যান্ডলিং মেকানিজম