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

এক্সপ্রেশন ও ইভালুয়েশন অর্ডার

Expressions & evaluation order
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • এক্সপ্রেশন ও স্টেটমেন্টের মধ্যে নির্ভুল পার্থক্য, এবং কেন কিছু ভাষা এই সীমারেখা মুছে দেয়
  • অপারেটর প্রিসিডেন্স ও অ্যাসোসিয়েটিভিটি — M3/L14-এর অ্যাম্বিগুইটি আলোচনার evaluation-semantics দিক
  • অপারেন্ড ইভালুয়েশন অর্ডার কেন একটি স্বতন্ত্র, প্রায়ই-উপেক্ষিত প্রশ্ন
  • বাম-থেকে-ডান ও ডান-থেকে-বাম ইভালুয়েশনের সাইড-এফেক্ট লগ সরাসরি কোডে তুলনা করা

১ · এক্সপ্রেশন বনাম স্টেটমেন্ট

একটি এক্সপ্রেশনExpressionএমন একটি কোড-খণ্ড যা ইভালুয়েট হয়ে একটি ভ্যালু প্রোডিউস করে — যেমন 3 + 4 — এবং সাধারণত একটি বড় এক্সপ্রেশনের ভেতরে নেস্ট করা যায়। ইভালুয়েট হয়ে একটি ভ্যালু প্রোডিউস করে (যেমন 3 + 4)। একটি স্টেটমেন্টStatementএমন একটি কোড-খণ্ড যা একটি অ্যাকশন/এফেক্ট ঘটায় কিন্তু নিজে ব্যবহারযোগ্য কোনো ভ্যালু দেয় না — বেশিরভাগ ইম্পারেটিভ ভাষায় assignment বা loop এর উদাহরণ। একটি অ্যাকশন/এফেক্ট ঘটায় কিন্তু নিজে ব্যবহারযোগ্য কোনো ভ্যালু দেয় না (বেশিরভাগ ইম্পারেটিভ ভাষায় একটি অ্যাসাইনমেন্ট বা একটি লুপ)। কিছু ভাষা — বিশেষত ফাংশনাল-ঝোঁকের ভাষা, M2/L08-এর সরাসরি সম্প্রসারণ — এই সীমারেখা ঝাপসা করে দিয়ে সবকিছুকেই এক্সপ্রেশন বানিয়ে দেয়। এটি একটি genuine, বাস্তব ভাষা-ডিজাইন সিদ্ধান্ত যার পরিণতি আছে composability-তে — একটি এক্সপ্রেশন একটি বড় এক্সপ্রেশনের ভেতরে নেস্ট করা যায়, কিন্তু একটি খাঁটি স্টেটমেন্ট সাধারণত পারে না।

২ · প্রিসিডেন্স ও অ্যাসোসিয়েটিভিটি

প্রিসিডেন্স ঠিক করে একাধিক অপারেটর প্যারেন্থিসিস ছাড়া একসাথে থাকলে কোনটি "বেশি শক্ত করে বাঁধে" (M3/L14-এর 2+3*4 উদাহরণের সরাসরি reuse — গুণ যোগের চেয়ে শক্ত করে বাঁধে বলেই ফলাফল 14, 20 নয়)। অ্যাসোসিয়েটিভিটি ঠিক করে একই-প্রিসিডেন্সের অপারেটর বারবার এলে কীভাবে গ্রুপ হয় — যেমন 10 - 3 - 2 কি (10-3)-2=5 (লেফট-অ্যাসোসিয়েটিভ, বিয়োগের জন্য প্রায়-সার্বজনীন বাস্তব-জগতের কনভেনশন) নাকি 10-(3-2)=9 (রাইট-অ্যাসোসিয়েটিভ)?

$$10 - 3 - 2 \;=\; (10 - 3) - 2 \;=\; 5 \quad \text{(লেফট-অ্যাসোসিয়েটিভ, প্রায় সব ভাষায় স্ট্যান্ডার্ড)}$$

৩ · অপারেন্ড ইভালুয়েশন অর্ডার — একটি স্বতন্ত্র প্রশ্ন

প্রিসিডেন্স ও অ্যাসোসিয়েটিভিটি ঠিক করে এক্সপ্রেশনটি কীভাবে গ্রুপ হবে — কিন্তু একটি সম্পূর্ণ ভিন্ন, প্রায়ই-উপেক্ষিত প্রশ্ন থেকে যায়: f() + g() ইভালুয়েট করার সময়, f() নাকি g() আগে কল হয়? বেশিরভাগ ভাষা একটি নির্দিষ্ট (deterministic) অর্ডার ফিক্স করে — সাধারণত বাম-থেকে-ডান — কিন্তু যদি f() ও g()-এর সাইড-এফেক্ট থাকে (যেমন কিছু প্রিন্ট করা, বা শেয়ার্ড স্টেট পরিবর্তন করা), তাহলে ইভালুয়েশন অর্ডার সত্যিকারের observable আউটপুট/আচরণ বদলে দেয় — শুধু একটি internal ইমপ্লিমেন্টেশন খুঁটিনাটি নয়, বরং একটি real, মাঝেমধ্যে বিস্ময়কর bug-এর উৎস, যদি একটি ভাষার ইভালুয়েশন অর্ডার ঠিকমতো বোঝা না থাকে।

৪ · কোড: একই মান, ভিন্ন সাইড-এফেক্ট লগ

নিচের কোড সেলে আমরা একটি ছোট এক্সপ্রেশন গাছ বানাব — (a * b) + c — যার প্রতিটি লিফ নোডে একটি সাইড-এফেক্ট আছে (একটি শেয়ার্ড লগ লিস্টে নিজের নাম যোগ করা, তারপর তার ভ্যালু রিটার্ন করা)। এরপর দুটো আলাদা ট্রি-ইভালুয়েটর চালাব — একটি সবসময় প্রতিটি অপারেটরের বাম অপারেন্ড আগে ইভালুয়েট করে, আরেকটি সবসময় ডান অপারেন্ড আগে — এবং দেখব চূড়ান্ত মান অভিন্ন থাকলেও লগের ক্রম সম্পূর্ণ ভিন্ন হয়ে যায়।

Python
# এক্সপ্রেশন গাছ (নেস্টেড টাপল) -- (a * b) + c
# leaf নোড: ("leaf", নাম, ভ্যালু)
# অপারেটর নোড: (অপারেটর, বাম-সাবট্রি, ডান-সাবট্রি)

def leaf(name, value):
    return ("leaf", name, value)

expr_tree = ("+", ("*", leaf("a", 2), leaf("b", 3)), leaf("c", 4))
# আশানুরূপ মান: (2 * 3) + 4 = 10


def apply_op(op, left_value, right_value):
    if op == "+":
        return left_value + right_value
    if op == "*":
        return left_value * right_value
    raise ValueError(f"অজানা অপারেটর: {op!r}")


def evaluate_left_to_right(node, log):
    if node[0] == "leaf":
        _, name, value = node
        log.append(name)  # -- সাইড এফেক্ট: এই লিফটি কখন ইভালুয়েট হলো তা লগ করা
        return value
    op, left, right = node
    left_value = evaluate_left_to_right(left, log)   # বাম আগে
    right_value = evaluate_left_to_right(right, log)  # ডান পরে
    return apply_op(op, left_value, right_value)


def evaluate_right_to_left(node, log):
    if node[0] == "leaf":
        _, name, value = node
        log.append(name)
        return value
    op, left, right = node
    right_value = evaluate_right_to_left(right, log)  # ডান আগে
    left_value = evaluate_right_to_left(left, log)     # বাম পরে
    return apply_op(op, left_value, right_value)


log_ltr = []
value_ltr = evaluate_left_to_right(expr_tree, log_ltr)

log_rtl = []
value_rtl = evaluate_right_to_left(expr_tree, log_rtl)

print(f"বাম-থেকে-ডান  -> চূড়ান্ত মান = {value_ltr}, সাইড-এফেক্ট লগ = {log_ltr}")
print(f"ডান-থেকে-বাম  -> চূড়ান্ত মান = {value_rtl}, সাইড-এফেক্ট লগ = {log_rtl}")

assert value_ltr == value_rtl == 10, "চূড়ান্ত গণনাকৃত মান অর্ডার নির্বিশেষে অভিন্ন থাকার কথা"
assert log_ltr == ["a", "b", "c"], "বাম-থেকে-ডান ইভালুয়েশনে a, তারপর b, তারপর c লগ হওয়ার কথা"
assert log_rtl == ["c", "b", "a"], "ডান-থেকে-বাম ইভালুয়েশনে c, তারপর b, তারপর a লগ হওয়ার কথা"
assert log_ltr != log_rtl, "একই মান হলেও লগের ক্রম ভিন্ন হওয়ার কথা"

print("\nযাচাই সফল -- চূড়ান্ত মান অভিন্ন (10), কিন্তু সাইড-এফেক্ট লগের ক্রম genuinely ভিন্ন।")

    
লক্ষ্য করুন apply_op ফাংশনটি দুই ক্ষেত্রেই ঠিক একই যুক্তিতে কল হয় — apply_op(op, left_value, right_value), প্যারামিটারের ক্রম কখনো বদলায় না — তাই চূড়ান্ত গণনা সবসময় একই থাকে। যা বদলায় তা হলো কোন লিফটি কখন লগে যোগ হয় — কারণ দুটো ইভালুয়েটর ফাংশন শুধু কোন সাবট্রি আগে recursively ভিজিট করা হবে তার ক্রম উল্টে দিয়েছে। এই বিচ্ছিন্নতাই — "কী গণনা হচ্ছে" বনাম "কোন ক্রমে সাইড-এফেক্ট ঘটছে" — এই পুরো পাঠের মূল বিষয়।
মূল কথা · Key takeaway

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

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

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

প্র ০১ উপরের কোডে value_ltr ও value_rtl কেন সবসময় সমান থাকে, যেখানে লগের ক্রম আলাদা?

কারণ apply_op(op, left_value, right_value) কলটি দুই ইভালুয়েটরেই অভিন্ন — প্যারামিটারের অবস্থান (কোনটি "বাম", কোনটি "ডান") কখনো অদলবদল হয় না, শুধু সেগুলো কোন ক্রমে গণনা করে বসানো হচ্ছে তা বদলায়। গণিতের দৃষ্টিকোণ থেকে + ও * উভয়ই commutative (যোগ/গুণের ফলাফল অপারেন্ডের গণনা-ক্রমের উপর নির্ভর করে না), তাই চূড়ান্ত সংখ্যাটি অপরিবর্তিত থাকে — শুধু কখন প্রতিটি লিফ ভিজিট হলো, সেটাই আলাদা।

প্র ০২ যদি leaf-এর সাইড এফেক্ট শুধু লগ করা না হয়ে বরং একটি শেয়ার্ড কাউন্টার ভ্যারিয়েবল বাড়ানো হতো, তাহলে কি চূড়ান্ত মানও পরিবর্তনশীল হয়ে যেতে পারত?

হ্যাঁ, সত্যিই হতে পারত — যদি কোনো একটি leaf-এর রিটার্ন-ভ্যালু নিজেই সেই শেয়ার্ড কাউন্টারের বর্তমান অবস্থার উপর নির্ভর করত (যেমন leaf রিটার্ন করত কাউন্টারের বর্তমান মান, স্থির মান নয়), তাহলে ইভালুয়েশন অর্ডার বদলালে কোন leaf কোন কাউন্টার-মান দেখতে পাচ্ছে তাও বদলে যেত — এবং তখন শুধু সাইড-এফেক্ট লগ নয়, চূড়ান্ত গণনাকৃত মানও ভিন্ন হতে পারত। এটিই বাস্তব জগতে ইভালুয়েশন অর্ডার-নির্ভর bug-এর একটি সাধারণ উৎস — যখন একটি অপারেন্ডের ভ্যালু নিজেই অন্য একটি অপারেন্ডের সাইড-এফেক্টের উপর নির্ভরশীল হয়ে পড়ে।

প্র ০৩ একটি ভাষা যদি অপারেন্ড ইভালুয়েশন অর্ডার "unspecified" (আনস্পেসিফাইড, অর্থাৎ যেকোনো ইমপ্লিমেন্টেশন যেকোনো অর্ডার বেছে নিতে পারে) রাখে, তাহলে এটি একজন প্রোগ্রামারের জন্য কী ঝুঁকি তৈরি করে?

যদি অর্ডার আনস্পেসিফাইড থাকে এবং প্রোগ্রামার সাইড-এফেক্ট সহ অপারেন্ড লেখেন (যেমন f() + g(), যেখানে দুটোই কিছু প্রিন্ট করে বা শেয়ার্ড স্টেট বদলায়), তাহলে সেই একই কোড ভিন্ন কম্পাইলার/ইন্টারপ্রেটার ভার্সনে, বা এমনকি ভিন্ন অপ্টিমাইজেশন লেভেলে, ভিন্ন observable আউটপুট দিতে পারে — একটি সত্যিকারের, reproduce করা কঠিন bug। এই কারণেই বেশিরভাগ ভাষা deliberately একটি নির্দিষ্ট (সাধারণত বাম-থেকে-ডান) ইভালুয়েশন অর্ডার commit করে, শুধু predictability-র জন্য।

অনুশীলন

  1. চিন্তা করুন: Python-এ f() + g() লিখলে কোন অর্ডারে f() ও g() কল হয়? আপনার পরিচিত কোনো ভাষায় কখনো এমন একটি bug দেখেছেন যেখানে ইভালুয়েশন অর্ডার প্রত্যাশা থেকে ভিন্ন হওয়ায় সমস্যা হয়েছে?

    Python একটি নির্দিষ্ট বাম-থেকে-ডান অপারেন্ড ইভালুয়েশন অর্ডার গ্যারান্টি দেয় — তাই f() + g()-এ সবসময় f() আগে, g() পরে কল হবে। বাস্তবে এই ধরনের bug সাধারণত দেখা দেয় যখন একটি ফাংশন কল-এর আর্গুমেন্টের মধ্যে সাইড-এফেক্ট থাকে এবং প্রোগ্রামার ধরে নেন একটি নির্দিষ্ট অর্ডার হবে, কিন্তু ভাষাটি (বা একটি নির্দিষ্ট অপারেটর, যেমন C-এর কিছু ক্ষেত্রে) সেই গ্যারান্টি দেয় না।

  2. পরীক্ষা করুন: উপরের কোড সেলে expr_tree-কে ("*", leaf("p", 5), ("+", leaf("q", 2), leaf("r", 3)))-এ বদলান এবং Run চেপে দেখুন নতুন চূড়ান্ত মান ও দুটো ভিন্ন লগ কী হয়।

    নতুন গাছটি গণনা করে 5 * (2 + 3) = 25 — তাই value_ltr ও value_rtl দুটোই 25 হবে (মান অপরিবর্তিত)। বাম-থেকে-ডান লগ হবে ['p', 'q', 'r'] (বাম সাবট্রি p আগে, তারপর ডান সাবট্রির ভেতরের বাম-থেকে-ডান ক্রমে q, r); ডান-থেকে-বাম লগ হবে ['r', 'q', 'p'] — সম্পূর্ণ উল্টো ক্রম, ঠিক আগের উদাহরণের প্যাটার্নের মতোই।

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

আগের পাঠ
ক্লোজার ও লেক্সিক্যাল এনভায়রনমেন্ট