পাঠ ৩৪ · ৫৮-এর মধ্যে · মডিউল ৭
Home / Courses / Concepts of Programming Languages & Compiler Design / টাইপ ইনফারেন্স

টাইপ ইনফারেন্স

Type inference
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টাইপ ইনফারেন্স কী এবং এটি L32-এর স্ট্যাটিক/ডায়নামিক দ্বন্দ্বকে কীভাবে নরম করে
  • Hindley-Milner-স্টাইল অ্যালগরিদমের তিনটি ধাপ — ফ্রেশ ভেরিয়েবল, কনস্ট্রেইন্ট, ইউনিফিকেশন
  • একটি সত্যিকারের সাবস্টিটিউশন-বেসড unify() অ্যালগরিদম বাস্তবায়ন ও যাচাই
  • ইউনিফিকেশন কীভাবে সত্যিকারের টাইপ-এরর সনাক্ত করে, প্রোগ্রামার কোনো অ্যানোটেশন না লিখলেও

১ · টাইপ ইনফারেন্স কী ও কেন

টাইপ ইনফারেন্সType Inferenceপ্রোগ্রামার স্পষ্টভাবে টাইপ না লিখেই একটি এক্সপ্রেশনের টাইপ স্বয়ংক্রিয়ভাবে নির্ণয় করার প্রক্রিয়া। হলো প্রোগ্রামার স্পষ্টভাবে টাইপ না লিখেই একটি এক্সপ্রেশনের টাইপ স্বয়ংক্রিয়ভাবে নির্ণয় করার প্রক্রিয়া — সরাসরি L32-এর দ্বন্দ্বের একটি সমাধান। অনেক আধুনিক স্ট্যাটিকালি-টাইপড ভাষা (Rust, Haskell, এবং আরও অনেক ভাষার inference-সমর্থিত ফিচার) স্ট্যাটিক টাইপিং-এর নিরাপত্তা (এরর কম্পাইল-টাইমে ধরা পড়ে) ডায়নামিক টাইপিং-এর মতো লেখার সুবিধার (সর্বত্র টাইপ অ্যানোটেশন লেখার প্রয়োজন নেই) সাথে মেলায় — ইনফারেন্সের মাধ্যমে।

২ · Hindley-Milner-স্টাইল অ্যালগরিদম — তিন ধাপ

ক্লাসিক্যাল, ফাউন্ডেশনাল অ্যালগরিদম (এখানে একটি সরলীকৃত, কনসেপচুয়াল ভার্সন, পূর্ণাঙ্গ ফরমাল টাইপ সিস্টেম নয়) তিনটি ধাপে কাজ করে —

(১) ফ্রেশ টাইপ ভেরিয়েবল
যে এক্সপ্রেশনের টাইপ সরাসরি লিটারেল থেকে স্পষ্ট নয়, তাকে একটি নতুন, অজানা টাইপ ভেরিয়েবল (যেমন T) বরাদ্দ করা হয়।
(২) কনস্ট্রেইন্ট জেনারেশন
এক্সপ্রেশনগুলো একসাথে কীভাবে ব্যবহৃত হচ্ছে তা থেকে নিয়মাবলী (কনস্ট্রেইন্ট) তৈরি করা হয় — যেমন "x, int-এর সাথে সামঞ্জস্যপূর্ণ হতে হবে।"
(৩) ইউনিফিকেশন
কনস্ট্রেইন্টগুলো বারবার সমাধান/মার্জ করে টাইপ ভেরিয়েবলের জায়গায় কংক্রিট টাইপ বসানো হয় — হয় সব ভেরিয়েবল সমাধান হয় (সাফল্য), অথবা একটি সাংঘর্ষিক (contradictory) কনস্ট্রেইন্ট পাওয়া যায় (টাইপ এরর)।
এক্সপ্রেশন: let x = 5 in x + 1 ফ্রেশ ভেরিয়েবল: x → T কনস্ট্রেইন্ট: (T, int), (T, int) unify() → T = int → ফলাফল টাইপ: int
নিচের কোড সেলে এই প্রতিটি ধাপ প্রকৃত কোড দিয়ে এক্সিকিউট করা হয়েছে — কোনো ধাপই হাতে-লেখা "মনে হওয়া" ফলাফল নয়।

ফরমাল নোটেশনে, ইনফারেন্স আসলে একটি টাইপিং জাজমেন্ট $\Gamma \vdash e : \tau$ ("এনভায়রনমেন্ট $\Gamma$-এর অধীনে, এক্সপ্রেশন $e$-এর টাইপ $\tau$") খুঁজে বের করার প্রক্রিয়া, যেখানে $\tau$ শুরুতে অজানা (একটি ফ্রেশ ভেরিয়েবল) এবং ইউনিফিকেশনের শেষে একটি কংক্রিট টাইপে সমাধান হয়।

৩ · কোড দিয়ে যাচাই — একটি প্রকৃত unify() অ্যালগরিদম

নিচের কোড সেলে একটি সত্যিকারের, সাবস্টিটিউশন-বেসড unify() ফাংশন বাস্তবায়ন করা হয়েছে — টাইপ হয় ('con', name) (কংক্রিট টাইপ, যেমন int) অথবা ('var', id) (টাইপ ভেরিয়েবল)। প্রথমে এটি let x = 5 in x + 1-এর উপর চালানো হয়েছে (সাফল্য কেস), তারপর একই টাইপ ভেরিয়েবলকে int ও string উভয়ের সাথে মেলাতে বাধ্য করা একটি ইচ্ছাকৃত ব্যর্থতা-কেস দেখানো হয়েছে।

Python
# টাইপ রিপ্রেজেন্টেশন: ('con', name) কংক্রিট টাইপ, অথবা ('var', name) টাইপ ভেরিয়েবল

def resolve(t, subst):
    """t যদি একটি ভেরিয়েবল হয় যা subst-এ ইতিমধ্যে সমাধান করা আছে, তাহলে সেই সমাধানে অনুসরণ করে চূড়ান্ত রূপ ফেরত দেয়।"""
    while t[0] == 'var' and t[1] in subst:
        t = subst[t[1]]
    return t


def occurs_in(var_name, t, subst):
    """occurs check -- t (resolve করার পর) কি ঠিক সেই var_name ভেরিয়েবলটিই?"""
    t = resolve(t, subst)
    return t[0] == 'var' and t[1] == var_name


def unify_one(t1, t2, subst):
    """একজোড়া টাইপকে ইউনিফাই করে, একটি নতুন (সম্প্রসারিত) সাবস্টিটিউশন ফেরত দেয় -- ব্যর্থ হলে TypeError।"""
    t1 = resolve(t1, subst)
    t2 = resolve(t2, subst)

    if t1 == t2:
        return subst  # ইতিমধ্যেই একই -- কিছু করার নেই

    if t1[0] == 'var':
        if occurs_in(t1[1], t2, subst):
            raise TypeError(f"occurs check ব্যর্থ: {t1} নিজেই {t2}-এর ভেতরে আছে")
        new_subst = dict(subst)
        new_subst[t1[1]] = t2
        return new_subst

    if t2[0] == 'var':
        return unify_one(t2, t1, subst)  # সিমেট্রিক -- ঘুরিয়ে আবার চেষ্টা করো

    # দুটোই কংক্রিট টাইপ, কিন্তু ভিন্ন -- সত্যিকারের সংঘর্ষ
    raise TypeError(f"ইউনিফিকেশন ব্যর্থ: {t1} এর সাথে {t2} মেলানো যায় না")


def unify(constraints):
    """কনস্ট্রেইন্টের একটি তালিকা (t1, t2) জোড়া করে ধারাবাহিকভাবে ইউনিফাই করে -- চূড়ান্ত সাবস্টিটিউশন ফেরত দেয়।"""
    subst = {}
    for (a, b) in constraints:
        subst = unify_one(a, b, subst)
    return subst


CON_INT = ('con', 'int')
T = ('var', 'T')  # x-এর টাইপ, শুরুতে অজানা

print("=== worked example: let x = 5 in x + 1 ===")
print("ধাপ ১ (ফ্রেশ ভেরিয়েবল): x -> T\n")

# ধাপ ২ (কনস্ট্রেইন্ট জেনারেশন):
#   'let x = 5' -- 5 : int, তাই x-এর টাইপ int-এর সাথে ইউনিফাই হতেই হবে
#   'x + 1'     -- 1 : int, আর + উভয় পাশে int দাবি করে, তাই আবার T int-এর সাথে ইউনিফাই হতে হবে
constraints = [
    (T, CON_INT),   # from: let x = 5
    (T, CON_INT),   # from: x + 1  (x এর ব্যবহার int-এর সাথে সামঞ্জস্যপূর্ণ হতে হবে)
]
print("ধাপ ২ (কনস্ট্রেইন্ট):", constraints)

# ধাপ ৩ (ইউনিফিকেশন):
subst = unify(constraints)
final_type = resolve(T, subst)
print("ধাপ ৩ (unify() চালানোর পর সাবস্টিটিউশন):", subst)
print(f"চূড়ান্ত ইনফার্ড টাইপ: T = {final_type}  (এক্সপ্রেশনের সামগ্রিক টাইপও int, কারণ + এর ফলাফল int)")

assert final_type == CON_INT, "প্রত্যাশিত ছিল int, কিন্তু ভিন্ন কিছু পাওয়া গেল!"
print("assertion পাস -- ইনফার্ড টাইপ সত্যিই int।\n")

print("=== ইচ্ছাকৃত ব্যর্থতা: একই ভেরিয়েবলকে int ও string উভয়ের সাথে ইউনিফাই করার চেষ্টা ===")
bad_constraints = [
    (T, CON_INT),               # T = int
    (T, ('con', 'string')),     # কিন্তু এখানে T = string -ও দাবি করা হচ্ছে -- সংঘর্ষ!
]
print("কনস্ট্রেইন্ট:", bad_constraints)
try:
    unify(bad_constraints)
    print("(এটা প্রিন্ট হওয়ার কথা না -- ইউনিফিকেশন ব্যর্থ হওয়া উচিত ছিল!)")
except TypeError as e:
    print(f"সঠিকভাবে ধরা পড়েছে -> টাইপ এরর: {e}")

    
লক্ষ্য করুন ব্যর্থতা-কেসে কী ঘটে ধাপে ধাপে — প্রথম কনস্ট্রেইন্ট (T, int) প্রসেস হওয়ার পর subst = {'T': int}। দ্বিতীয় কনস্ট্রেইন্ট (T, string) প্রসেস করার সময় resolve(T, subst) ইতিমধ্যে int-এ সমাধান হয়ে যায় — তাই এখন প্রকৃতপক্ষে int ও string-কে সরাসরি তুলনা করা হচ্ছে, দুটোই কংক্রিট টাইপ, দুটোই ভিন্ন — তাই unify_one-এর একেবারে শেষ শাখাটি (সত্যিকারের সংঘর্ষ) ট্রিগার হয়। এটিই ঠিক সেই মুহূর্ত যেখানে "প্রোগ্রামার কোনো টাইপ না লিখলেও" একটি প্রকৃত টাইপ-এরর স্বয়ংক্রিয়ভাবে ধরা পড়ে।
মূল কথা · Key takeaway

টাইপ ইনফারেন্স জাদু নয় — এটি ঠিক L30-এর টাইপ-চেকিং নিয়মগুলোই প্রয়োগ করে, শুধু টাইপগুলো লিটারেলের বদলে ভেরিয়েবল দিয়ে শুরু হয়, এবং ইউনিফিকেশন ধীরে ধীরে সেগুলোকে কংক্রিট করে তোলে। যখন কোনো সমাধান সম্ভব হয় না (যেমন একই ভেরিয়েবলকে দুটো ভিন্ন কংক্রিট টাইপের সাথে মেলানোর দাবি), সেটিই একটি প্রকৃত টাইপ-এরর — L35-এ দেখা যাবে ঠিক এই একই ইনফারেন্স মেকানিজম কীভাবে প্যারামেট্রিক পলিমরফিজমকেও স্বাভাবিকভাবে সাপোর্ট করে।

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

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

প্র ০১ উপরের কোডে resolve() ফাংশনটি কেন প্রয়োজন — শুধু subst[t[1]] দিয়ে সরাসরি লুকআপ করলে সমস্যা কোথায়?

সাবস্টিটিউশন চেইন হতে পারে — যেমন যদি প্রথমে T1-কে T2-এর সাথে ইউনিফাই করা হয় (subst = {'T1': T2}), তারপর পরে T2-কে int-এর সাথে ইউনিফাই করা হয় (subst = {'T1': T2, 'T2': int}) — তাহলে T1-এর প্রকৃত চূড়ান্ত টাইপ জানতে একটি লুকআপ যথেষ্ট নয়, T1 → T2 → int — পুরো চেইন অনুসরণ করতে হয়। resolve()-এর while লুপ ঠিক এই চেইন-ফলোয়িং করে, যতক্ষণ না একটি কংক্রিট টাইপ বা একটি এখনো-অসমাধানকৃত ভেরিয়েবলে পৌঁছায়।

প্র ০২ L32-এর ভাষায় বলতে গেলে, টাইপ ইনফারেন্স-সহ একটি ভাষা কি স্ট্যাটিকালি টাইপড, নাকি ডায়নামিকালি টাইপড?

স্ট্যাটিকালি টাইপড — টাইপ চেকিং (এই ক্ষেত্রে, ইউনিফিকেশন) এখনো কম্পাইল-টাইমেই ঘটে, প্রোগ্রাম চলার আগেই, এবং একটি টাইপ-মিসম্যাচ (যেমন উপরের ব্যর্থতা-কেস) প্রোগ্রাম চলার আগেই বাতিল হয়ে যায় — L32-এর স্ট্যাটিক টাইপিং-এর সংজ্ঞাটিই এখানে হুবহু প্রযোজ্য। পার্থক্য শুধু এই — প্রোগ্রামারকে টাইপগুলো নিজে হাতে লিখতে হয় না, কম্পাইলার নিজেই সেগুলো বের করে নেয়। তাই টাইপ ইনফারেন্স "নতুন তৃতীয় ধরনের টাইপিং" নয় — এটি স্ট্যাটিক টাইপিংয়েরই একটি সুবিধাজনক বাস্তবায়ন কৌশল।

প্র ০৩ যদি worked example-এ শুধু একটি কনস্ট্রেইন্ট থাকত ((T, int), শুধু let x = 5 থেকে, x + 1 অংশটুকু বাদ দিয়ে), তাহলে কি ফলাফল পাল্টাত?

না, চূড়ান্ত ফলাফল একই থাকত — T = int। দ্বিতীয় কনস্ট্রেইন্ট (T, int) unify_one-এর t1 == t2 শাখায় গিয়ে সাথে সাথে সন্তুষ্ট হয় (কারণ resolve(T, {'T': int}) ইতিমধ্যে int), তাই এটি সাবস্টিটিউশনে নতুন কিছু যোগ করে না — এটি শুধু একটি নিশ্চিতকরণ (confirmation), নতুন তথ্য নয়। এটি দেখায় কনস্ট্রেইন্ট সিস্টেম রিডানডেন্ট (অপ্রয়োজনীয় কিন্তু সাংঘর্ষিক-নয়) কনস্ট্রেইন্টকে স্বাভাবিকভাবেই নিরাপদে হ্যান্ডেল করে।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোড সেলে ব্যর্থতা-কেসের কনস্ট্রেইন্ট তালিকাকে [(T, CON_INT), (('var', 'U'), ('con', 'bool')), (T, ('var', 'U'))]-এ বদলে দিন (এখন T ও U দুটো আলাদা ভেরিয়েবল, কিন্তু পরোক্ষভাবে সংঘর্ষে জড়িয়ে) এবং Run চাপুন — কী ঘটে?

    প্রথম কনস্ট্রেইন্টের পর subst = {'T': int}। দ্বিতীয়টির পর subst = {'T': int, 'U': bool}। তৃতীয় কনস্ট্রেইন্ট (T, U) প্রসেস করার সময় resolve(T, subst) = int এবং resolve(U, subst) = bool — দুটো ভিন্ন কংক্রিট টাইপ, ফলে unify_one-এর সংঘর্ষ-শাখাটি ট্রিগার হয় এবং একটি TypeError ছোঁড়ে — এমনকি যদিও মূল কনস্ট্রেইন্ট দুটোতে সরাসরি int ও bool-এর মধ্যে কোনো সরাসরি তুলনা লেখা ছিল না, T ও U-এর মধ্য দিয়ে পরোক্ষভাবে সংঘর্ষ ধরা পড়ল — ইউনিফিকেশনের একটি বাস্তবসম্মত শক্তি।

  2. চিন্তা করুন: Python-এ একটি ফাংশনের প্যারামিটারে টাইপ অ্যানোটেশন না লিখলে সেটি কি "টাইপ ইনফারেন্স"? কেন বা কেন না?

    না — Python মূলত L32-এর ডায়নামিক টাইপিং ব্যবহার করে, এই পাঠের ইনফারেন্স নয়। পার্থক্যটি মৌলিক: এখানে বর্ণিত টাইপ ইনফারেন্স কম্পাইল-টাইমে প্রমাণ করে একটি নির্দিষ্ট টাইপ, প্রোগ্রাম চলার আগেই, এবং ভুল হলে প্রোগ্রামটি চলতেই দেয় না। Python একটি ভ্যারিয়েবলের জন্য কোনো টাইপই আগেভাগে নির্ণয় বা যাচাই করে না — এটি শুধু রান-টাইমে বর্তমান ভ্যালুর টাইপ দেখে অপারেশন চালানোর চেষ্টা করে, ব্যর্থ হলে তখনই এরর দেয় (কিছু আধুনিক Python টুল যেমন mypy আলাদাভাবে, ঐচ্ছিকভাবে, টাইপ ইনফারেন্স যোগ করে — কিন্তু ভাষার নিজস্ব রানটাইম আচরণ নয়)।

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

আগের পাঠ
L33 · টাইপ ইকুইভ্যালেন্স ও চেকিং রুল