স্কোপ — স্ট্যাটিক (লেক্সিক্যাল) বনাম ডায়নামিক স্কোপিং
এই পাঠে যা শিখবেন
- স্ট্যাটিক (লেক্সিক্যাল) স্কোপিং কীভাবে সোর্স-টেক্সটের নেস্টিং দিয়ে নাম রেজলভ করে
- ডায়নামিক স্কোপিং কীভাবে রানটাইম কল স্ট্যাক দিয়ে নাম রেজলভ করে
- একটি সুনির্দিষ্ট উদাহরণে দুটো পদ্ধতির genuinely ভিন্ন ফলাফল হাতে-কলমে যাচাই করা
- কেন স্ট্যাটিক স্কোপিং আধুনিক ভাষা-ডিজাইনে প্রায় সার্বজনীন ডিফল্ট হয়ে উঠেছে
১ · এই পাঠের লেন্স — M6/L29 থেকে ভাষা-ডিজাইন প্রশ্নে
M6/L29 দেখিয়েছিল একটি কম্পাইলার কীভাবে স্কোপ রেজোলিউশন বাস্তবায়ন করে — সিম্বল টেবিলের একটি স্ট্যাক ব্যবহার করে। এই পাঠ সেই বাস্তবায়ন-বিস্তারিত পুনরাবৃত্তি করবে না; বরং প্রশ্ন করবে — একজন ভাষা-ডিজাইনার স্কোপিং রুল নিয়ে ঠিক কী সিদ্ধান্ত নিতে পারেন, এবং সেই সিদ্ধান্তের বাস্তব পরিণতি কী। L37-এর সাধারণ বাইন্ডিং-টাইম ভাষায় বললে — এই পাঠ প্রশ্ন করছে: "একটি নেম রেফারেন্স কোন ডিক্লারেশন-অ্যাট্রিবিউটে বাইন্ড হয়, এবং সেই বাইন্ডিং স্ট্যাটিক নাকি ডায়নামিক?"
২ · স্ট্যাটিক (লেক্সিক্যাল) স্কোপিং
স্ট্যাটিক স্কোপিংStatic / Lexical Scopingএকটি নেম রেফারেন্স কোন ডিক্লারেশনকে বোঝায় তা সোর্স টেক্সটে সেই রেফারেন্সটি কোন কোন স্কোপ দ্বারা টেক্সচুয়ালি ঘেরা (enclosed) তা দেখে ঠিক হয় — সম্পূর্ণ কম্পাইল টাইমে নির্ধারণযোগ্য। -এ একটি নেম রেফারেন্স কোন ডিক্লারেশনকে বোঝায় তা নির্ধারিত হয় নেমটির সোর্স টেক্সটে অবস্থান দিয়ে — নির্দিষ্টভাবে, কোড যেভাবে লেখা হয়েছে তার নেস্টিং স্ট্রাকচার অনুসরণ করে কোন কোন স্কোপ সেই রেফারেন্সকে টেক্সচুয়ালি ঘিরে (enclose) আছে তা দেখে। এটি সম্পূর্ণভাবে কম্পাইল টাইমেই নির্ধারণ করা সম্ভব, প্রোগ্রামটি আসলে রান না করেই (সরাসরি M6/L29-এর কম্পাইল-টাইম স্ট্যাক-বেসড রেজোলিউশনের সাথে সংযুক্ত)।
৩ · ডায়নামিক স্কোপিং
ডায়নামিক স্কোপিংDynamic Scopingএকটি নেম কোন ডিক্লারেশনকে বোঝায় তা রানটাইমে বর্তমান কল স্ট্যাকে থাকা কলারদের চেইন খুঁজে ঠিক হয় — সোর্স টেক্সটের নেস্টিং দিয়ে নয়। -এ একটি নেম রেফারেন্স কোন ডিক্লারেশনকে বোঝায় তা নির্ধারিত হয় actual runtime কল সিকোয়েন্স দিয়ে — নির্দিষ্টভাবে, সেই মুহূর্তে কল স্ট্যাকে থাকা কলারদের চেইন খুঁজে, টেক্সটের লেক্সিক্যাল নেস্টিং দিয়ে নয়। কম প্রচলিত এই পদ্ধতি — কিন্তু এখনকার কিছু ভাষায় (যেমন কিছু শেল স্ক্রিপ্টিং ভাষা, Emacs Lisp-এর কিছু ভ্যারিয়েবল) এখনো ব্যবহৃত হয়।
f() ফাংশন — কিন্তু স্কোপিং রুল ভিন্ন হলে x-এর রেজলভড ভ্যালু ভিন্ন হয়ে যায়।৪ · ক্রুশিয়াল উদাহরণ — একই কোড, দুটো ভিন্ন উত্তর
নিচের উদাহরণটি এই পুরো পাঠের মূল বিষয়। ধরুন —
- একটি গ্লোবাল ভ্যারিয়েবল
x = 1আছে। - একটি ফাংশন
f()আছে যাxপড়ে ও প্রিন্ট করে —f()টেক্সটে টপ-লেভেলে ডিফাইন করা,g()-এর ভেতরে নেস্টেড নয়। - একটি ফাংশন
g()আছে যেটি একটি লোকালx = 2ডিক্লেয়ার করে, এরপরf()কল করে।
স্ট্যাটিক স্কোপিং-এ: f()-এর x রেফারেন্স গ্লোবাল x-কে
বোঝায় — কারণ f টেক্সচুয়ালি টপ-লেভেলে ডিফাইন করা, g-এর ভেতরে নেস্টেড নয় — তাই
f() প্রিন্ট করবে 1, কে f()-কে কল করছে তা যাই হোক না কেন।
ডায়নামিক স্কোপিং-এ: f()-এর x রেফারেন্স g()-এর লোকাল
x-কে খুঁজে পাবে — কারণ f() রান হওয়ার মুহূর্তে g() হলো বর্তমান
অ্যাক্টিভ কলার — তাই প্রিন্ট করবে 2।
# toy প্রোগ্রাম রিপ্রেজেন্টেশন -- স্কোপ = নেম -> ভ্যালু ডিকশনারি
global_scope = {"x": 1}
f_scope = {} # f-এর নিজস্ব লোকাল স্কোপ -- কোনো লোকাল x নেই
g_scope = {"x": 2} # g-এর নিজস্ব লোকাল স্কোপ -- একটি লোকাল x আছে
def resolve_static(name, lexical_scope_chain):
"""স্ট্যাটিক স্কোপিং: সোর্স-টেক্সট নেস্টিং অনুসারে ফিক্সড চেইন খুঁজে বের করে।"""
for scope in lexical_scope_chain:
if name in scope:
return scope[name]
raise NameError(f"{name!r} কোনো স্কোপে পাওয়া যায়নি")
def resolve_dynamic(name, call_stack):
"""ডায়নামিক স্কোপিং: রানটাইম কল-স্ট্যাক, সবচেয়ে সাম্প্রতিক কলার থেকে শুরু করে খুঁজে বের করে।"""
for frame in reversed(call_stack):
if name in frame:
return frame[name]
raise NameError(f"{name!r} কোনো ফ্রেমে পাওয়া যায়নি")
# --- স্ট্যাটিক রেজোলিউশন ---
# f() টেক্সচুয়ালি টপ-লেভেলে ডিফাইন করা -- তাই তার lexical chain
# সবসময় [f_scope, global_scope], কে কল করছে তার উপর নির্ভর করে না
f_lexical_chain = [f_scope, global_scope]
static_result = resolve_static("x", f_lexical_chain)
# --- ডায়নামিক রেজোলিউশন ---
# g() থেকে f() কল হচ্ছে -- runtime কল স্ট্যাক: global -> g -> f
call_stack_when_f_runs = [global_scope, g_scope, f_scope]
dynamic_result = resolve_dynamic("x", call_stack_when_f_runs)
print(f"স্ট্যাটিক স্কোপিং-এ f()-এর x = {static_result}")
print(f"ডায়নামিক স্কোপিং-এ f()-এর x = {dynamic_result}")
assert static_result == 1, "স্ট্যাটিক স্কোপিং-এ f()-এর x গ্লোবাল x (=1) হওয়ার কথা"
assert dynamic_result == 2, "ডায়নামিক স্কোপিং-এ f()-এর x g()-এর লোকাল x (=2) হওয়ার কথা"
print("\nযাচাই সফল -- একই f() ফাংশন, একই x রেফারেন্স, কিন্তু দুটো ভিন্ন স্কোপিং রুলে দুটো ভিন্ন উত্তর।")
resolve_static শুধুমাত্র f_lexical_chain ব্যবহার করে — যা f
কোথায় লেখা হয়েছে তার উপর নির্ভর করে, প্রোগ্রাম কীভাবে চলছে তার উপর একদমই নয়। অন্যদিকে
resolve_dynamic শুধুমাত্র call_stack_when_f_runs ব্যবহার করে — যা এই মুহূর্তে
কে কাকে কল করেছে তার উপর নির্ভর করে, f কোথায় লেখা হয়েছে তা একদমই বিবেচনায় নেয় না।
এই দুটো সম্পূর্ণ ভিন্ন ইনপুট নিয়েই দুটো পদ্ধতি ভিন্ন ফলাফল দেয়।
৫ · কেন স্ট্যাটিক স্কোপিং জিতেছে
প্রায় সব আধুনিক ভাষা (C, Python, JavaScript, Java, Rust — সবাই) স্ট্যাটিক স্কোপিং ব্যবহার করে। এর একটি স্পষ্ট, ব্যবহারিক কারণ আছে — স্ট্যাটিক স্কোপিং একজন প্রোগ্রামারকে শুধু কোডের টেক্সচুয়াল গঠন পড়েই বুঝতে দেয় একটি নেম কী বোঝায়, প্রোগ্রামের প্রতিটি সম্ভাব্য কল-পাথ ট্রেস করার দরকার হয় না — একটি গুরুত্বপূর্ণ readability ও reasoning-about-code সুবিধা। ডায়নামিক স্কোপিং একটি ফাংশনের আচরণকে নির্ভরশীল করে তোলে কে তাকে কল করছে তার উপর — বড় প্রোগ্রামে এটি সত্যিকারের বিভ্রান্তিকর একটি বৈশিষ্ট্য, কারণ একই ফাংশন কল প্রোগ্রামের বিভিন্ন জায়গা থেকে ভিন্ন ভিন্ন ফলাফল দিতে পারে।
স্ট্যাটিক স্কোপিং নাম রেজলভ করে সোর্স-টেক্সটের নেস্টিং দিয়ে (কম্পাইল টাইমে নির্ধারণযোগ্য); ডায়নামিক
স্কোপিং রেজলভ করে রানটাইম কল স্ট্যাক দিয়ে। একই f()/g()/গ্লোবাল-x
কাঠামোর জন্য দুটো পদ্ধতি সত্যিকারের ভিন্ন উত্তর দেয় (1 বনাম 2) — এটি একটি বিমূর্ত পার্থক্য নয়, বরং
প্রোগ্রামের observable আচরণকেই বদলে দেয়। এই কারণেই আধুনিক ভাষা-ডিজাইন প্রায় সর্বসম্মতভাবে স্ট্যাটিক
স্কোপিং বেছে নিয়েছে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
যদি g() নিজে f()-কে কল না করে বরং অন্য কোনো ফাংশন h() সরাসরি টপ-লেভেল থেকে f()-কে কল করত, তাহলে ডায়নামিক স্কোপিং-এ f()-এর x কী হতো?
এক্ষেত্রে রানটাইম কল স্ট্যাক হতো [global, h_frame, f_frame] — h()-এর নিজস্ব
কোনো লোকাল x না থাকলে, resolve_dynamic h_frame-এও কিছু না পেয়ে
পরবর্তীতে global_scope-এ পৌঁছে গ্লোবাল x = 1 খুঁজে পেত। এটিই ডায়নামিক
স্কোপিং-এর মূল সমস্যা প্রদর্শন করে — f()-এর ফলাফল কে তাকে কল করছে তার উপর
সম্পূর্ণ নির্ভরশীল, যেখানে স্ট্যাটিক স্কোপিং-এ ফলাফল সবসময় 1-ই হতো, কলার যে-ই হোক না কেন।
প্র ০২
উপরের কোডে resolve_static ফাংশনটি call_stack প্যারামিটার নেয় না, শুধু lexical_scope_chain নেয় — এটি কেন গুরুত্বপূর্ণ একটি ডিজাইন সিদ্ধান্ত?
কারণ এটিই স্ট্যাটিক স্কোপিং-এর মূল সংজ্ঞাগত বৈশিষ্ট্য প্রতিফলিত করে — একটি নেম কী বোঝায় তা নির্ধারণ
করতে রানটাইম তথ্যের (কে কল করছে) কোনো প্রয়োজনই নেই। resolve_static-এর সিগনেচার নিজেই এই
দাবিটি এনফোর্স করে — এটি রানটাইম কল স্ট্যাক অ্যাক্সেসই করতে পারে না, কারণ প্যারামিটার হিসেবে সেটি
দেওয়াই হয়নি। এটি ঠিক কেন স্ট্যাটিক স্কোপিং সম্পূর্ণ কম্পাইল টাইমে নির্ধারণযোগ্য তার একটি কোড-স্তরের
প্রতিফলন।
প্র ০৩ ডায়নামিক স্কোপিং যদি এত বিভ্রান্তিকর হয়, তাহলে কেন কিছু ভাষা তবুও এটি ব্যবহার করে?
ডায়নামিক স্কোপিং-এর একটি বাস্তব সুবিধা আছে নির্দিষ্ট ব্যবহারের ক্ষেত্রে — এটি একটি ফাংশনকে তার কলারের কনটেক্সট থেকে কনফিগারেশন/সেটিংস "উত্তরাধিকারসূত্রে" পেতে দেয় প্যারামিটার হিসেবে explicitly পাস না করেই (যেমন Emacs Lisp-এ কিছু এডিটর-সেটিংস ভ্যারিয়েবল)। কিন্তু এই সুবিধা একটি বড় খরচে আসে — কোডের আচরণ বোঝার জন্য সম্পূর্ণ কল-হায়ারার্কি জানতে হয়, যা বড় প্রোগ্রামে ব্যবহারিকভাবে অসম্ভব হয়ে ওঠে — এই কারণেই এটি একটি বিরল, নিশ-কেস ফিচার হয়ে থেকে গেছে, ডিফল্ট নয়।
অনুশীলন
-
চিন্তা করুন: Python, JavaScript, C, Java — এই ভাষাগুলো কি স্ট্যাটিক নাকি ডায়নামিক স্কোপিং ব্যবহার করে? আপনার পরিচিত কোনো ভাষায় কি একটি ফাংশনের ভেতরের ভ্যারিয়েবল-রেফারেন্স কখনো "কে কল করছে" তার উপর নির্ভর করতে দেখেছেন?
Python, JavaScript, C, Java — এই চারটি ভাষাই স্ট্যাটিক (লেক্সিক্যাল) স্কোপিং ব্যবহার করে। কার্যত সব মূলধারার আধুনিক ভাষা একই — তাই বাস্তবে "কে কল করছে তার উপর ভ্যারিয়েবল-রেফারেন্স নির্ভর করা" খুব কম প্রোগ্রামারই সরাসরি দেখেছেন; এটি মূলত একটি ঐতিহাসিক/নিশ ফিচার (পুরনো Lisp ডায়ালেক্ট, কিছু শেল স্ক্রিপ্ট কনভেনশন)।
-
পরীক্ষা করুন: উপরের কোড সেলে
g_scope-এর মান{"x": 2}থেকে{"y": 99}-এ বদলান (অর্থাৎg()-এর লোকাল ভ্যারিয়েবলের নাম আরxনয়) এবং Run চেপে দেখুনresolve_dynamic("x", ...)-এর ফলাফল কী হয়।g_scope-এ আর কোনোxকী না থাকায়,resolve_dynamicf_scope-এ (খালি) কিছু না পেয়ে, তারপরg_scope-এও (এখন শুধুyআছে) কিছু না পেয়ে, শেষেglobal_scope-এ পৌঁছে গ্লোবালx = 1খুঁজে পাবে — অর্থাৎ এখন ডায়নামিক ও স্ট্যাটিক দুটো পদ্ধতিই 1 রিটার্ন করবে, কারণ কল-স্ট্যাকে আর কোনো "shadowing"xঅবশিষ্ট নেই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরের পাঠ — লাইফটাইম ও স্টোরেজ অ্যালোকেশন — স্কোপ (কোথায় নাম দেখা যায়) থেকে লাইফটাইম (কখন স্টোরেজ টিকে থাকে) প্রশ্নে যায়।
- লাইফটাইম ও স্টোরেজ অ্যালোকেশন পাঠ ৩৯ স্কোপ ও লাইফটাইম প্রায়ই একসাথে ঘটে কিন্তু ধারণাগতভাবে ভিন্ন — এই পাঠ সেই পার্থক্য স্পষ্ট করে।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স স্কোপ রেজোলিউশনের কম্পাইলার-বাস্তবায়ন (M6/L29) সরাসরি একটি স্ট্যাক ডেটা স্ট্রাকচার ব্যবহার করে — এই কোর্সে সেই ভিত্তি শেখানো হয়েছে।