Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ৩/১৫কঠিন৫০ মিনিট

Type System — static, dynamic, strong, weak আসলে কী গ্যারান্টি দেয়

Type Systems: Static vs Dynamic, Strong vs Weak

টাইপ মানে values-এর set আর legal operations; static/dynamic ঠিক করে কখন check হয়, strong/weak ঠিক করে ভাঙলে কী হয় — আর dynamic typing আসলে প্রতিটা value-তে গায়ে-লাগানো tag-এর physical implementation।

এই লেসন শেষে আপনি পারবেন

  • টাইপকে "values-এর set আর তাদের উপর legal operations-এর তালিকা" হিসেবে define করে বলতে পারবেন একটি টাইপ declaration ঠিক কী promise করে
  • Static/dynamic (কখন check হয়) আর strong/weak (কতটা কড়াভাবে মানে) দুইটা **স্বাধীন** axis-এ অন্তত ছয়টা ভাষা বসাতে পারবেন, justification সহ
  • Dynamic typing-এর physical implementation ব্যাখ্যা করতে পারবেন — tagged value, type descriptor, runtime dispatch — আর CPython-এর object layout-এ সেটা চিনতে পারবেন
  • C-এর union punning আর implicit conversion কীভাবে bits নীরবে reinterpret করে, আর Python/JS একই situation-এ ভিন্নভাবে কেন আচরণ করে — উদাহরণ trace করে দেখাতে পারবেন
  • Type safety-র informal theorem — progress + preservation — আগের লেসনের stuck state-এর সাথে যুক্ত করে বলতে পারবেন well-typed program কী গ্যারান্টি দেয় আর কী দেয় না

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

Module-এর driving question-এ ফিরি: Python আর Rust — দুটোই programming language, নিচে পার্থক্য কোথায়? আগের দুই লেসনে আমরা দুই স্তর খুলেছি: syntax বনাম semantics, আর semantics-এর গাণিতিক লেখা। তৃতীয় স্তরটা সবচেয়ে practical, আর সেখানেই Python-Rust পার্থক্য সবচেয়ে জোরালো: type system।

শুরুতেই একটা বিভ্রান্তি ঝাড়তে হবে, কারণ industry-তে চারটা শব্দ — static, dynamic, strong, weak — এমনভাবে এলোমেলো ব্যবহৃত হয় যে দুজন senior engineer-ও একই বাক্যে দুই মানে বুঝে ফেলে। আসল কথাটা সরল: দুইটা সম্পূর্ণ স্বাধীন প্রশ্ন আছে। প্রথমটা timing: type check হয় run-এর আগে (static) না চলার মধ্যে (dynamic)? দ্বিতীয়টা strictness: check ভাঙলে কি চিৎকার হয় (strong) না নীরবে bits reinterpret হয়ে যায় (weak)? এক প্রশ্নের উত্তর অন্যটা আগায় না — Python dynamic হয়েও strong, C static হয়েও weak।

আর এই দুই axis-এর নিচে একটা physical সত্য বসে আছে, যেটাই এই লেসনের under-the-hood মূল অংশ: dynamic typing আসলে কোনো জাদু না — প্রতিটা runtime value-র গায়ে লাগানো একটা tag, আর computer- representation module-এ আপনি যে interpretation-idea দেখেছেন (bit pattern-এর অর্থ আসে interpreter/tag থেকে) — ঠিক সেটাই, hardware-এর বদলে runtime-এ।

আজকের প্রশ্ন তিনটা: (১) টাইপ declaration ঠিক কী promise করে, আর কোন প্রশ্নের উত্তর দেয় না? (২) dynamic typing physically implement হয় কীভাবে, আর তার প্রতিটা operation-এর খরচ কোথায়? (৩) “well-typed programs cannot go wrong” — এই গ্যারান্টির সীমা ঠিক কোথায়?

মূল ধারণা

টাইপ মানে কী — দুই অংশের একটা contract

Cardelli-Wegner-র ধ্রুপদি definition: type হলো values-এর একটা set আর সেই values-গুলোর উপর legal operations-এর একটা তালিকা।

int টাইপ বলছে: এই ঘরে থাকবে পূর্ণসংখ্যা (set), আর এদের উপর চলবে যোগ-বিয়োগ-গুণ-ভাগ, তুলনা (operations)। str বলছে: character sequence, আর তাদের উপর concatenation, slicing, search। এই দুই অংশের ভাঙনটা ধরে রাখুন, কারণ প্রতিটা type-error আসলে এর এক-একটা ভঙ্গ:

  • Set ভঙ্গ: age = "thirty" যেখানে age-এর টাইপ int চাই — ভুল value, সঠিক family।
  • Operations ভঙ্গ: "5" + 5 — value-দুটোই বৈধ, কিন্তু +-এর এই জুটির জন্য কোনো legal rule নেই (Python-এ)।

আর টাইপ কী দেয় না সেটাও contract-এর অংশ: int-এর কোনো promise নেই যে মানটা positive, range-এ আছে (overflow!), বা business logic মেনে চলে। Type system শুধু family-membership আর operation-legality নিয়ে কথা বলে — বাকি সব assertion/test-এর জগৎ।

দুইটা স্বাধীন axis

Axisপ্রশ্নউত্তর কোথায়
Static vs DynamicType check হয় কখন?Run-এর আগে (compiler) না চলার-মধ্যে (runtime)
Strong vs WeakCheck ভাঙলে কী হয়?Loud failure না silent reinterpretation/coercion

চার কোণে চার বাস্তব ভাষা:

StrongWeak
StaticJava, Rust, Haskell, SwiftC, C++
DynamicPython, Ruby, Clojure, ErlangJavaScript, PHP, Perl

Prevented বনাম detected — একই ভুলের দুই ভাগ্য

আগের লেসনের stuck state মনে করুন: small-step machine True + 3 পেলে আটকে যায়। প্রশ্ন হলো সেই আটকানো ধরা পড়বে কখন:

  • Static: compiler AST-হাঁটার সময়ই প্রমাণ করে সব possible execution-এ প্রতিটা operation-এর operand টাইপমতো — না-হলে compile fail। ভুলটা prevented: কোনো input, কোনো path, কোনো দিনেও ঘটবে না।
  • Dynamic: runtime-এ প্রতিটা operation execute-এর মুহূর্তে operand-দুটোর tag দেখে — মিললে চলে, না-মিললে exception। ভুলটা detected: ঘটতে পারে, তবে ঘটার মুহূর্তেই চিৎকার। যে path চলেনি, তার ভুল আজও hidden।

Trade-off দুই দিকেই খরচে: static-এ flexibility হারায় (duck-typed code compile হয় না), dynamic-এ coverage হারায় (untested branch = hidden bomb)। mypy/pyright/TypeScript-এর পুরো product-ই dynamic ভাষায় static check যোগ করার চেষ্টা — best of both, তার দাম annotation-এর শ্রম।

Strong/Weak — ভাঙার পরে কী হয়

Check ভাঙলে (wrong-type operation) ভাষা দুই রকম সাড়া দিতে পারে:

  • Strong: চিৎকার — exception, compile error। Value-টার bits অক্ষত, অর্থ অপরিবর্তিত।
  • Weak: নীরবে মিটমাট — bits reinterpret, implicit conversion, বা raw memory হিসেবে access। Programmer-এর intention-এর বদলে machine-এর convenience।

C-এর ক্লাসিক অস্ত্রভাণ্ডার: (char*) cast যেকোনো pointer যেকোনো টাইপে ঢুকিয়ে দেয়, union একই bytes দুই টাইপের চোখে দেখায় (union punning), arithmetic implicit conversion-এর সিঁড়ি (integer promotion) নীরবে টাইপ পালটায়। এগুলো কিছু ক্ষেত্রে feature (system programming-এ raw bytes-এ access অমূল্য), বেশিরভাগ ক্ষেত্রে CVE-র জন্মপত্র।

JavaScript অন্য পথে weak: memory-unsafe না, কিন্তু coercion rule-book এত উদার যে প্রায় সব জুটি চলে যায়:

JS expressionফলনীরব rule
1 + "1""11"+ string-পক্ষী: number→string
"5" - 23- number-পক্ষী: string→number
[] + []""দুটোই ToPrimitive→""
[] + {}"[object Object]"object→string default
null == undefinedtrueবিশেষ equality clause
NaN === NaNfalseIEEE-754-এর নিয়ম — NaN নিজের সাথেই অসম

Python একই expression-গুলোতে TypeError ছাড়ে — dynamic হয়েও। পার্থক্যটা timing-এ না, strictness-এ: দুই ভাষাই runtime-এ tag দেখে, কিন্তু একটা mismatch মানলে আরেকটা নীরবে convert করে নেয়।

ভেতরে কী ঘটছে

Dynamic typing-এর physical সত্য: প্রতিটা value একটা self-describing box

Dynamic language-এ runtime-কে টাইপ জানাতেই হয় — কারণ সে-ই checker। জানানোর উপায় একটাই: value-র সাথে tag বহন করা। তিনটা প্রচলিত কৌশল:

  1. Boxed tagged object — প্রতিটা value heap-object, যার header-এ type descriptor pointer (CPython, Ruby, JVM objects)।
  2. Word-tagging — 64-bit word-এর ৩-৪ bit টাইপ-tag হিসেবে, বাকিটা data (Erlang/OTP, OCaml, Lisp Machines)। ছোট integer-এর জন্য allocation-ই লাগে না।
  3. Uniform representation — সব value একই shape, tag implicit (early Lisp: সব cell সমান)।

CPython-এর case টা খুলে দেখি — এটাই আপনি প্রতিদিন চালান। Python-এ x = 65 লিখলে x নামের ঘরে integer-টা ঢোকে না — ঢোকে heap-এ থাকা একটা object-এর pointer:

names dict                heap
┌─────────┐          ┌─────────────────────┐
│ "x" ────┼────────▶ │ PyLongObject        │
└─────────┘          │  ob_refcnt: 1       │
                     │  ob_type: ──────────┼──▶ int-এর type descriptor
                     │  ob_size: 1         │    (যার ভেতরে add/sub/...
                     │  ob_digit: [65]     │     slot-দের function table)
                     └─────────────────────┘

ob_type pointer-টাই the tag। x + y চালালে VM-এর কাজ:

BINARY_OP '+':
  ১. x-এর pointer follow করে ob_type পড়ো
  ২. y-এর pointer follow করে ob_type পড়ো
  ৩. int-descriptor-এর numeric-slot-এ দুটোই match করে কি না দেখো
  ৪. match হলে slot-function call (দুই payload-এর real addition)
     না মিললে NotImplemented → TypeError

প্রতিটা a + b, প্রতিটা comparison, প্রতিটা attribute-access — tag-read + dispatch + check — তিন ধাপের overhead। এটাই dynamic typing-এর runtime বিল, আর এটাই CPython-এর C-এর চেয়ে ধীর হওয়ার বড় কারণগুলোর একটা। JIT-রা (PyPy, Python 3.13’s JIT) ঠিক এই বিল কমায়: বারবার প্রমাণিত tag-pattern cache করে guard-check-এ নামিয়ে আনে।

Type safety — গ্যারান্টিটা formal ভাষায়

আগের লেসনে stuck state দেখেছেন: এমন term যার কোনো rule নেই। Type system-এর লক্ষ্য সেই state-কে অসম্ভব করা, আর প্রমাণ হয় দুই lemma দিয়ে (informally):

  • Preservation: যদি e : T (e-র টাইপ T) এবং e এক step-এ e′-তে যায়, তবে e′ : T-ও। অর্থাৎ চলতে-চলতে টাইপ ভাঙে না — যে-টাইপ দিয়ে শুরু, সেই পরিবারেই থাকে।
  • Progress: যদি e : T, তবে e হয় value, নয়তো e এক step নিতে পারে। অর্থাৎ stuck হওয়ার third option-টাই নেই।

দুটো একসাথে induction দিয়ে দাঁড় করালেই Milner-এর famous claim: well-typed programs cannot go wrong — runtime type error অসম্ভব। খেয়াল করুন গ্যারান্টির সীমাটা: এটা বলে না program সঠিক, terminate করবে, বা null হবে না (null pointer bug-রা টাইপ-safe Java- তেও বাস করে — তবে লেসন ৫-এ দেখব optional/Option টাইপ কীভাবে null- কেও টাইপের ভেতরে ঢুকিয়ে দেয়)। এটা বলে ঠিক একটা: operation-family mismatch ঘটবে না।

উদাহরণ

একই ইচ্ছা, চার ভাষা, চার ভাগ্য

উদ্দেশ্য: একটা 32-bit pattern-কে দুই চোখে দেখা — একবার unsigned integer, একবার float। System programmer-রা এটা প্রতিদিন করে (hash function, RNG, fast inverse sqrt!)।

C — নীরবে সম্ভব, দুই পথে:

#include \<stdio.h>
#include \<string.h>

union pun { unsigned int u; float f; };

int main(void) {
    /* পথ ১: union punning */
    union pun p;
    p.f = 1.0f;
    printf("%08X\n", p.u);        // 3F800000 — নীরব, কোনো অনুমতি-পত্র নেই

    /* পথ ২: memcpy */
    int i = 1065353216;
    float g;
    memcpy(&g, &i, sizeof g);
    printf("%f\n", g);            // 1.000000
}

Python — চিৎকার, তবে দরজা আছে:

>>> "5" + 5
TypeError: can only concatenate str (not "int") to str
>>> import struct
>>> struct.unpack('<I', struct.pack('<f', 1.0))[0]
1065353216

Java — compile-time চিৎকার:

float g = Float.intBitsToFloat(1065353216);   // অফিসিয়াল দরজা
// (float) i লিখলে হবে VALUE conversion — bits না, মান 1065353216.0f

Assembly — কোনো প্রশ্নই নেই:

movd  xmm0, eax      ; bits move — টাইপ বলার কেউই নেই

চার ভাষার message চারটা: C বলে “memory তোমার”; Python বলে “tag- মিললেই চলবে, নইলে চিৎকার”; Java বলে “দরজা আছে, তবে named door”; assembly বলে “bits-ই সব”। Strong/weak-এর spectrum-টা এখানেই দৃশ্যমান — capability সবার কাছেই আছে, পার্থক্য permission-এর দামে।

একটা nuance যা interview-তে প্রায়ই পড়ে: Python-এ True + True দেয় 2 — TypeError না! কারণ bool আসলে int-এর subclass (isinstance(True, int) → True), তাই tag-check মিলে যায়। Strong typing মানে “no silent surprises” absolute না — মানে surprises-গুলো documented rule-এ ঘটে (subclass hierarchy এখানে rule)।

ছবিতে

PYTHON (dynamic):  a + b                      C (static):  a + b
      │                                              │
      ▼                                              ▼
┌───────────────┐                            ┌──────────────┐
│ deref a → tag │  (pointer chase,           │ add eax, ebx │
├───────────────┤   cache miss সম্ভব)        │              │
│ deref b → tag │                            │ ১ instruction│
├───────────────┤                            │ ~1 cycle     │
│ tag match? ───┼──না──▶ TypeError           └──────────────┘
├───────────────┤
│ slot lookup   │  (function-table index)
├───────────────┤
│ indirect call │  (branch predictor-এর কাজ)
├───────────────┤
│ real add      │
└───────────────┘
 ~৪ ধাপ + দুই pointer-chase + indirect branch
Dynamic dispatch-এর খরচের anatomy — একটা `a + b`-র পেছনে dynamic ভাষায় যা যা ঘটে, আর static-compiled কোডে যা ঘটে।

এই gap-টাই dynamic-to-native compilers (PyPy JIT, LuaJIT, V8) বন্ধ করার যুদ্ধক্ষেত্র: profiling দেখে “এখানে সবসময় দুটো int” জানলে guard + fast-path emit করে — অর্থাৎ runtime-এ পাওয়া তথ্য দিয়ে static optimization-এর নকল। Guard fail হলে deopt — dynamic-এর flexibility রেখে static-এর গতির চেষ্টা, প্রতিটা JIT-এর পুরো engineering-এই trade-off।

নিজে চালিয়ে দেখুন

EXPERIMENT

Bits-এর দুই চোখ — C union punning বনাম Python-এর tagged world

gcc বা clang + Python 3 (macOS/Linux)· ২০ মিনিট

অংশ ১ — C-এর নীরব reinterpretation:

/* punning.c */
#include \<stdio.h>
#include \<string.h>

union pun {
    unsigned int u;
    float        f;
};

int main(void) {
    union pun p;
    p.f = 1.0f;
    printf("1.0f-এর bits          = %08X\n", p.u);

    p.u += 1;                              // শেষ bit +১ — float-এর জগতে এটা
    printf("+1 bit হিসেবে ফের float = %.9g\n", p.f);   // পরের representable float!

    int i = 1065353216;                    // ঠিক 1.0f-এর bit pattern
    float by_memcpy, by_cast;
    memcpy(&by_memcpy, &i, sizeof by_memcpy);  // bits copy — conversion নয়
    by_cast = (float)i;                        // VALUE conversion
    printf("memcpy (bits)          = %f\n", by_memcpy);
    printf("cast   (value)         = %.1f\n", by_cast);
    return 0;
}
gcc -O2 punning.c -o punning && ./punning

প্রত্যাশিত output:

1.0f-এর bits          = 3F800000
+1 bit হিসেবে ফের float = 1.00000012
memcpy (bits)          = 1.000000
cast   (value)         = 1065353216.0

মধ্যের দুই লাইনই লেসন: একই 32-bit source, দুই ভিন্ন semantic — memcpy bits নকল করে (float 1.0 পাওয়া গেল), cast মান convert করে (বিলিয়ন পাওয়া গেল)। C দুটোই নীরবে করল — weak typing-এর দুই মুখ। আর +1 bit-এ পেলেন float-এর next representable value — IEEE-754-এর gap-structure নিজের চোখে।

অংশ ২ — Python-এর চিৎকার আর tag-inspection:

# inspect_tags.py
import struct, sys

# ১. একই কাজ — কিন্তু explicit, আর ভুল হলে চিৎকার
print(struct.pack('<f', 1.0).hex())                      # 0000803f
print(struct.unpack('<I', struct.pack('<f', 1.0))[0])    # 1065353216
try:
    "5" + 5
except TypeError as e:
    print("চিৎকার:", e)

# ২. প্রতিটা value-র গায়ে সত্যিই tag আছে
x = 65
print(type(x).__name__, id(x), sys.getsizeof(x))         # int <address> 28

a, b = 1000, 1000
print(id(a) == id(b))                                    # False — দুইটা আলাদা box!

s = ""
print(sys.getsizeof(s))                                  # 49 — খালি str-ও box খায়
print(sys.getsizeof(True))                               # 28
print(isinstance(True, int), True + True)                # True 2 — subclass nuance

প্রত্যাশিত পর্যবেক্ষণ:

  1. sys.getsizeof(65) = 28 bytes — 8-byte integer-এর জন্য ২৮! কারণ box-টা refcnt + type pointer + size + digit ধরে। Tag- বহনের physical দাম এটাই।
  2. দুইটা আলাদা 1000-এর id() আলাদা — প্রতিটা assignment নতুন box (small-int cache −5..256 বাদে — id(5) == id(5) চেক করুন, True পাবেন: implementation detail যা interview-তে fun fact)।
  3. "5" + 5 চিৎকার করেছে — JS-এর সাথে Python-এর পার্থক্য এই এক লাইনে: একই dynamic, ভিন্ন strictness।
এটা কী প্রমাণ করে

C একই 32 bits-কে নীরবে দুই টাইপের value হিসেবে পড়ে (weak/static); Python একই কাজে টাইপ-mismatch-এ চিৎকার করে (strong/dynamic), আর Python-এর প্রতিটা object-এর ভেতরে সত্যিই tag (ob_type) বাস করে — id()/type()/sys.getsizeof দিয়ে ছুঁয়ে দেখা যায়।

নিজে বানান

BUILD IT

TaggedValue Interpreter — dynamic typing নিজের হাতে implement করুন

Python · ●●○○○
  1. Tagged class লিখুন — tag আর payload ধরে
  2. Constructor helper বানান — t_int, t_bool, t_str
  3. প্রতিটা operation-এ explicit tag-check লিখুন — mismatch-এ DynTypeError
  4. একটা WEAK flag যোগ করুন যেটা on করলে JS-style coercion করবে
  5. CHECKS counter বসিয়ে মাপুন tag-check-এর runtime বিল
  6. t_if দিয়ে দেখান condition-এও tag-enforcement চলে

লক্ষ্য: Python দিয়ে dynamic typing-এর engine বানানো — যাতে দেখা যায় “dynamic” মানে কোনো absence না, একটা সুনির্দিষ্ট mechanical protocol।

class DynTypeError(TypeError):
    pass

class Tagged:
    """একটা runtime value — tag + payload। CPython-এর PyLongObject-
    এর শিক্ষামূলক নকল।"""
    __slots__ = ('tag', 'payload')

    def __init__(self, tag, payload):
        self.tag = tag
        self.payload = payload

    def __repr__(self):
        return f"[{self.tag}: {self.payload!r}]"

def t_int(n):   return Tagged('int', n)
def t_bool(b):  return Tagged('bool', b)
def t_str(s):   return Tagged('str', s)

WEAK = False          # True = JS-মোড, False = Python-মোড
CHECKS = 0            # কতবার tag পড়লাম — dynamic-এর runtime বিল

def note_check():
    global CHECKS
    CHECKS += 1

def t_add(a, b):
    note_check()
    if a.tag == 'int' and b.tag == 'int':
        return t_int(a.payload + b.payload)
    if a.tag == 'str' and b.tag == 'str':
        return t_str(a.payload + b.payload)
    if WEAK:                                   # JS-মোড: নীরবে string-এ ফেলে দাও
        def flat(v):
            return v.payload if v.tag == 'str' else repr(v.payload)
        return t_str(flat(a) + flat(b))
    raise DynTypeError(
        f"cannot add {a.tag} and {b.tag}")     # Python-মোড: চিৎকার

def t_eq(a, b):
    note_check()
    return t_bool(False) if a.tag != b.tag \
        else t_bool(a.payload == b.payload)

def t_if(cond, then_v, else_v):
    """আমাদের mini-language-এ condition হতেই হবে bool — Python-এর
    truthiness এখানে নেই (লক্ষ্য করুন: dynamic ≠ permissive)"""
    note_check()
    if cond.tag != 'bool':
        raise DynTypeError(f"condition must be bool, got {cond.tag}")
    return then_v if cond.payload else else_v

# ── Demo ─────────────────────────────────────────────────────
n1, n2 = t_int(40), t_int(2)
print(t_add(n1, n2))                 # [int: 42]

try:
    t_add(t_str("5"), t_int(5))
except DynTypeError as e:
    print("strong মোড:", e)          # চিৎকার

WEAK = True
print(t_add(t_str("5"), t_int(5)))   # [str: '55'] — JS-এর জগৎ
WEAK = False

try:
    t_if(t_int(1), t_str("yes"), t_str("no"))
except DynTypeError as e:
    print("strict condition:", e)    # 1 truthy হওয়া সত্ত্বেও — ট্যাগ-ই সব

# বিল-মাপা: ১০০০ বার add — প্রতিটায় কয়টা check?
start_checks = CHECKS
acc = t_int(0)
for _ in range(1000):
    acc = t_add(acc, t_int(1))
print(f"{CHECKS - start_checks} tag-checks "
      f"for 1000 additions ({(CHECKS - start_checks) / 10:.0f}% overhead)")

প্রত্যাশিত output:

[int: 42]
strong মোড: cannot add str and int
[str: '55']
strict condition: condition must be bool, got int
1000 tag-checks for 1000 additions (100% overhead)

তিনটা সিদ্ধান্ত নোট করুন:

  1. WEAK flag-ই পুরো strong/weak axis — একই interpreter, এক boolean পালটালেই JS হয়ে যায়। Weak typing কোনো ক্ষমতার ব্যাপার না (দুই মোডেই সব কাজ সম্ভব), policy-র ব্যাপার।
  2. প্রতিটা operation-এ অন্তত এক tag-read — 100% overhead মাপা গেল। এটাই hood section-এর C-vs-Python diagram-এর সংখ্যাগত রূপ।
  3. t_if-এর strictness ইচ্ছাকৃত — dynamic ভাষা মানেই সব জায়গায় উদার না; কোন ট্যাগ কোথায় বাধ্য তা প্রতিটা ভাষার নিজস্ব সিদ্ধান্ত।

নিজে বাড়ান:

  1. t_mul আর t_len যোগ করুন (t_len শুধু str-এ চলবে)।
  2. Small-int cache নকল করুন: −5..256 range-এর t_int-এর জন্য shared instance ফেরত দিন — তারপর t_eq(t_int(5), t_int(5)).payload আর id()-ভিত্তিক তুলনার পার্থক্য দেখুন। CPython-এর interned ints-এর রহস্য খুলে যাবে।
  3. চ্যালেঞ্জ: word-tagging মোড যোগ করুন — payload ৬১-bit হিসেবে রেখে top-3 bit tag (Erlang-এর মতো)। ছোট int-এ কোনো box-allocation লাগে না — মেমরি-খরচের পার্থক্য মেপে দেখুন।

বাস্তব সিস্টেমে

Runtime tags আর type discipline যেখানে বাস্তবে দেখা যায়

CPython-এর সবকিছু। আপনার প্রতিদিনের interpreter — প্রতিটা value boxed, প্রতিটা operation tag-dispatched। sys.getsizeof-এর সংখ্যাগুলো (int 28, empty list 56, empty dict 64) tag-বহনের সরাসরি হিসাব।

Erlang/Elixir BEAM। Word-tagging-এর শিল্পস্তরের মাস্টার: প্রতি 64-bit word-এ 4-bit tag, ছোট integer ও atom-এ zero allocation — telecom-grade soft-realtime-এর ভিত্তি।

OCaml/Haskell-এর hybrid। Compile-time টাইপ জানা থাকায় runtime tag প্রায় দরকার নেই — OCaml int-এ ১ bit tag (odd = pointer), polymorphic comparison-এর সময় ছাড়া কোনো tag-check নেই। Static typing-এর performance-dividend এর চেয়ে concrete উদাহরণ বোধহয় নেই।

JVM। Primitive (int — raw 32 bit, no tag) বনাম reference (tagged object) দুই জগৎ; generics-এ boxing যেখানে পড়ে সেখানেই tag-বহনের বিল (ArrayList<Integer>-এর প্রতিটা element heap-boxed)। Project Valhalla ঠিক এই বিল কমানোর multi-year effort।

Lisp Machines (1980s)। Hardware-এই tag ছিল — word-এর dedicated tag-bits, ALU-র ভেতরে tag-check circuit। Dynamic typing-এর খরচ silicon দিয়ে শূন্য-প্রায় করার ঐতিহাসিক চেষ্টা; আজ CPU-র SIMD/ tagged-pointer trick-গুলো তারই নাতি-নাতনি।

WASM GC proposal। WebAssembly-তে managed types আনার সময় tagged-representation choice-টা spec-level debate ছিল — engines (V8, SpiderMonkey)-এর ভেতরের object-layout এখন ঠিক এই লেসনের trade-off-গুলোর উপর দাঁড়িয়ে।

TypeScript-এর erasure। Runtime-এ TypeScript-এর কোনো অস্তিত্ব নেই — সব tag-check JS-এর নিজের। Compile-time-only type system-এর সবচেয়ে সফল production deployment: গ্যারান্টি compile-এ, খরচ runtime- এ শূন্য — তবে runtime-data-র সাথে trust-boundary-তে validation (zod ইত্যাদি) আলাদা করে লাগতেই হয়, কারণ compile-time টাইপ wire-এর bytes রক্ষা করে না।

যে ভুলগুলো সবাই করে

“Strong মানে static, dynamic মানে weak — চারটা শব্দ একই কথার চার রূপ।”

সবচেয়ে প্রচলিত বিভ্রান্তি, আর সবচেয়ে সহজে ভাঙা যায়: Python dynamic-strong ("5"+5 চিৎকার), C static-weak ((char*) নীরব)। প্রথম axis সময়ের, দ্বিতীয়টা strictness-এর — orthogonal। এই দুই confusion-কে আলাদা রাখতে পারলে আপনি ইতিমধ্যে বাজারের ৯০% type-system discussion-এর চেয়ে precise।

“Dynamic language-এ variables-এর টাইপ হয় না — যা খুশি ঢোকান।”

Variables-এর না হলেও values-এর টাইপ সবসময় থাকে — এবং সেটা runtime-এ জোর করে পড়া হয়। x = 5; x = "hello" বৈধ মানে variable-টা untyped না — variable শুধু নাম-board-এর entry, যে pointer-ই ধরুক ধরতে পারে; box-গুলো কঠোরভাবে typed। তাই dynamic language-এও x.foo এক টাইপে চলে অন্য টাইপে AttributeError — tag-ই সেই সিদ্ধান্ত দেয়।

“Weak typing মানে খারাপ ডিজাইন — strong সবসময় superior।”

Weak tools নির্দিষ্ট domain-এর ইচ্ছাকৃত অস্ত্র: OS kernel-এ raw bytes-এ access (device register, network packet) অপরিহার্য, আর C- এর casts সেই দরজা। খারাপ হওয়াটা ব্যবহার-প্রসঙ্গে: application logic- এ union-punning CVE-র উৎস। সঠিক framing হলো blast-radius management — kernel-এ এক লাইনের unsafe block, application-এ পুরো codebase safe default। Rust-এর unsafe {} block-এর দর্শন এটাই: capability রেখে দাও, কিন্তু জায়গা মাপা রাখো।

“Type-safe মানে bug-free — well-typed প্রোগ্রাম তো সঠিকই হবে।”

Milner-এর theorem-এর সীমা মনে রাখুন: guarantee শুধু “no stuck states” — operation-family mismatch নেই। Infinite loop, wrong algorithm, off-by-one, null-dereference (nullable-টাইপ থাকলে), race condition — সব perfectly well-typed। Type system-কে ভুলে যাওয়া ভুলগুলোর একটা নির্দিষ্ট শ্রেণি মুছে দেওয়ার যন্ত্র ভাবুন — শ্রেণিটা বড় ও দামি (production incidents-এর বড় অংশ), তাই investment সার্থক; কিন্তু “correctness” শব্দটা তার vocabulary-তেই নেই।

বুঝেছেন কি না দেখুন

1

চারটা ভাষা — C, Python, Java, JavaScript — প্রতিটাকে দুই axis-এ (static/dynamic, strong/weak) বসান, প্রতিটার জন্য একটা করে এক-লাইনের evidence দিন।

স্মরণ

C — static, weak: টাইপ compile-এ যাচাই, তবে (char*) cast আর union punning নীরবে bits reinterpret করে।

Python — dynamic, strong: টাইপ runtime-এ tag থেকে, আর "5"+5 TypeError — কোনো silent coercion নেই।

Java — static, strong: generic/array টাইপ compile-এ enforce, implicit narrowing (long→int) পর্যন্ত compile error চায় explicit cast।

JavaScript — dynamic, weak: টাইপ runtime-এ, কিন্তু 1+"1"→"11" জাতীয় coercion rule-book প্রায় সব mismatch নীরবে মিটিয়ে দেয়।

2

CPython-এ sys.getsizeof(10**18) কত হবে — 28, নাকি বেশি? কেন? আর sys.getsizeof(10**100) তখন?

প্রয়োগ

10**18 64-bit-এর মধ্যে (≈9.2×10¹⁸ max signed) — এক-digit PyLong (30-bit limb ×1… আসলে 60-bit দরকার, ২ limb) — 28 বা 32 bytes; CPython 3.12-এ medium ints 28-32। মূল শিক্ষা: size fixed না, payload বড় হলে box বড় হয়। 10**100 ≈ 34 limb দরকার — getsizeof প্রায় 72 bytes। Arbitrary-precision int-এর দামটাই এখানে দেখা যায়: variable-length box, প্রতিটা arithmetic overflow-check সহ।

3

Preservation + progress একসাথে কীভাবে “well-typed programs cannot go wrong” দেয় — নিজের ভাষায় তিন-বাক্যে ব্যাখ্যা করুন, আর বলুন এই theorem কোন ধরনের bug ধরে না।

যুক্তি

Preservation বলে টাইপ চলতে-চলতে টিকে থাকে (প্রতিটা step-এর পরেও well-typed); progress বলে well-typed term হয় value নয়তো step নিতে পারে — stuck-এর তৃতীয় option নেই। Induction-এ দুটো জোড়া দিলে: প্রতিটা reachable state well-typed, আর প্রতিটা well-typed state এগোতে পারে — তাই কোনোদিও কোনো state-এ পৌঁছানো অসম্ভব যেখানে operation তার operand-family-র বাইরে চলে। ধরে না: wrong answer, nontermination, null-deref (nullable থাকলে), concurrency races — এসবের টাইপ-legal হওয়া সম্ভব।

4

আপনার team-এর embedded firmware C-তে, যেখানে device registers memory-mapped। Senior একজন বলছেন “সব union-punning বাদ দিয়ে memcpy-ভিত্তিক serialization-এ যাই”। Formal-semantics lens দিয়ে evaluate করুন: প্রস্তাবটার পক্ষে-বিপক্ষে কী, আর মাঝামাঝি solution কী হতে পারে?

ডিজাইন

পক্ষে: union-punning technically UB-adjacent territory-তে পড়ে (C-তে type-punning through union common extension, কিন্তু strict- aliasing rules active-optimize করলে ভাঙে); memcpy একমাত্র standard- sanctioned bit-copy — CompCert-যুগের verified toolchain-এও নিরাপদ।

বিপক্ষে: hot path-এ memcpy overhead, আর firmware-এ register access মানেই ইতিমধ্যে volatile-cast territory যেখানে aliasing অনিবার্য।

মাঝামাঝি: punning আলাদা unsafe-marked module-এ isolate (Rust-এর দর্শন), boundary-তে explicit serialization functions (uint32 read/write helpers), আর CI-তে UBSan/-fstrict-aliasing দুই মোডে test। Blast-radius কমানো, elimination না — এটাই realistic engineering answer।

5

Build section-এর interpreter-এ WEAK=True করলে [str: '55'] পাওয়া গেল। এবার ধরুন t_eq(t_int(1), t_str("1")) — JS এ কী দেয়, Python-এ কী? আপনার WEAK মোডে কী দেবে এখন, এবং সেটা কি consistent? (JS-এর == বনাম ===-এর কথা মাথায় রাখুন।)

প্রয়োগ

JS ==: 1 == "1" → true (number-coercion rule); ===: false — strict equality কোনো coercion করে না। Python: 1 == "1" → False (equality টাইপ-mismatch-এ exception ছাড়ে না, শুধু unequal বলে — comparison আলাদা policy than +)। আমাদের WEAK মোডের current t_eq: tag ভিন্ন হলেই False — অর্থাৎ WEAK-তেও ===-সদৃশ। Consistency-র জন্য দুই function দরকার: loose_eq (WEAK-তে coerce-then-compare) আর strict_eq — ঠিক JS-এর দুই operator। শিক্ষা: equality-র নিজের semantic policy আছে, এবং weak ভাষারা দুই policy একসাথে রাখার বোঝা user-এর কাঁধে ফেলে — JS-এ ==-কে lint-নিষিদ্ধ করার পেছনের formal কারণ এটা।

6

OCaml-এ runtime tag প্রায় নেই, CPython-এ প্রতিটা value-তে। দুই ভাষাই কি “typed”? Static-typed ভাষায় runtime tag না থাকলে কী হারায় — এমন একটা operation-এর উদাহরণ দিন যা Python-এ চলে কিন্তু tagless representation-এ অসম্ভব।

যুক্তি

দুটোই typed — তবে টাইপ-তথ্যের বাসস্থান ভিন্ন: OCaml compile-time proof-এ, Python runtime tag-এ। Tagless হলে হারায় runtime reflection আর dynamic type inquiry: Python-এ type(x).__name__, isinstance(x, str) runtime-এ tag পড়ে; OCaml-এ এমন generic “type-of-any-value” function লেখা যায় না (System F-এর parametricity — লেসন ৫-এ দেখব)। আরেকটা: Python-এর repr() যেকোনো value ছাপে — tagless জগতে printer-কে টাইপ আগে জানাতে হয়। Trade-off পরিষ্কার: tag = runtime introspection-এর দাম, no-tag = speed আর memory-র লাভ, introspection হারানো।

এরপর কী

এরপর কী

আজ দেখলাম টাইপ check কখন হয় (static/dynamic) আর ভাঙলে কী হয় (strong/weak), আর dynamic-এর পেছনের যন্ত্রটা — tag + dispatch — নিজের হাতে বানিয়ে মেপে দেখলাম। কিন্তু একটা অসুবিধা চোখে পড়েছে: Java-তে List<String> লিখতে হয়, Python-এ duck typing-এর দৌড়ে typo এক মাস পরে ধরা পড়ে। প্রশ্নটা তখন: compiler কি annotation- এর বোঝাটা কমিয়ে টাইপগুলো নিজেই বের করতে পারে?

পরের লেসনে — Hindley-Milner Type Inference — দেখব পারা যায়, এবং সেটাও গণিত দিয়ে: unification মানে টাইপ-equation সমাধান (puzzle-solving-এর মতো), Algorithm W কীভাবে ML/Rust/F# জাতীয় ভাষায় let id = fn x => x থেকে α → α বের করে — annotation একটাও ছাড়া। আর দেখব কেন একই কাজ Java/C# কয়েক দশক পারে না করতে পেরেছিল।

আরও পড়ুন

  • Types and Programming Languages, Chapter 8 — Simply Typed Lambda-Calculus — Benjamin C. Pierce · Progress ও preservation theorem-এর মূল পাঠ — type safety-র formal proof template
  • "On Understanding Types, Data Abstraction, and Polymorphism" — Luca Cardelli, Peter Wegner · 1985-এর এই survey-ই type = set of values দৃষ্টিভঙ্গি আর polymorphism classification-এর canonical উৎস
  • The Java Virtual Machine Specification, §2 — The Structure of the Java Virtual Machine — Tim Lindholm et al. · Tagged representation আর runtime type information-এর industrial উদাহরণ