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।
আগে এটা বুঝি
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 Dynamic | Type check হয় কখন? | Run-এর আগে (compiler) না চলার-মধ্যে (runtime) |
| Strong vs Weak | Check ভাঙলে কী হয়? | Loud failure না silent reinterpretation/coercion |
চার কোণে চার বাস্তব ভাষা:
| Strong | Weak | |
|---|---|---|
| Static | Java, Rust, Haskell, Swift | C, C++ |
| Dynamic | Python, Ruby, Clojure, Erlang | JavaScript, 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" - 2 | 3 | - number-পক্ষী: string→number |
[] + [] | "" | দুটোই ToPrimitive→"" |
[] + {} | "[object Object]" | object→string default |
null == undefined | true | বিশেষ equality clause |
NaN === NaN | false | IEEE-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 বহন করা। তিনটা প্রচলিত কৌশল:
- Boxed tagged object — প্রতিটা value heap-object, যার header-এ type descriptor pointer (CPython, Ruby, JVM objects)।
- Word-tagging — 64-bit word-এর ৩-৪ bit টাইপ-tag হিসেবে, বাকিটা data (Erlang/OTP, OCaml, Lisp Machines)। ছোট integer-এর জন্য allocation-ই লাগে না।
- 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]
1065353216Java — compile-time চিৎকার:
float g = Float.intBitsToFloat(1065353216); // অফিসিয়াল দরজা
// (float) i লিখলে হবে VALUE conversion — bits না, মান 1065353216.0fAssembly — কোনো প্রশ্নই নেই:
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এই 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।
নিজে চালিয়ে দেখুন
Bits-এর দুই চোখ — C union punning বনাম Python-এর tagged world
অংশ ১ — 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প্রত্যাশিত পর্যবেক্ষণ:
sys.getsizeof(65)= 28 bytes — 8-byte integer-এর জন্য ২৮! কারণ box-টা refcnt + type pointer + size + digit ধরে। Tag- বহনের physical দাম এটাই।- দুইটা আলাদা
1000-এরid()আলাদা — প্রতিটা assignment নতুন box (small-int cache −5..256 বাদে —id(5) == id(5)চেক করুন, True পাবেন: implementation detail যা interview-তে fun fact)। "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 দিয়ে ছুঁয়ে দেখা যায়।
নিজে বানান
TaggedValue Interpreter — dynamic typing নিজের হাতে implement করুন
- Tagged class লিখুন — tag আর payload ধরে
- Constructor helper বানান — t_int, t_bool, t_str
- প্রতিটা operation-এ explicit tag-check লিখুন — mismatch-এ DynTypeError
- একটা WEAK flag যোগ করুন যেটা on করলে JS-style coercion করবে
- CHECKS counter বসিয়ে মাপুন tag-check-এর runtime বিল
- 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)তিনটা সিদ্ধান্ত নোট করুন:
WEAKflag-ই পুরো strong/weak axis — একই interpreter, এক boolean পালটালেই JS হয়ে যায়। Weak typing কোনো ক্ষমতার ব্যাপার না (দুই মোডেই সব কাজ সম্ভব), policy-র ব্যাপার।- প্রতিটা operation-এ অন্তত এক tag-read — 100% overhead মাপা গেল। এটাই hood section-এর C-vs-Python diagram-এর সংখ্যাগত রূপ।
- t_if-এর strictness ইচ্ছাকৃত — dynamic ভাষা মানেই সব জায়গায় উদার না; কোন ট্যাগ কোথায় বাধ্য তা প্রতিটা ভাষার নিজস্ব সিদ্ধান্ত।
নিজে বাড়ান:
t_mulআরt_lenযোগ করুন (t_lenশুধু str-এ চলবে)।- Small-int cache নকল করুন: −5..256 range-এর t_int-এর জন্য shared
instance ফেরত দিন — তারপর
t_eq(t_int(5), t_int(5)).payloadআরid()-ভিত্তিক তুলনার পার্থক্য দেখুন। CPython-এর interned ints-এর রহস্য খুলে যাবে। - চ্যালেঞ্জ: 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 নীরবে মিটিয়ে দেয়।
2CPython-এ sys.getsizeof(10**18) কত হবে — 28, নাকি বেশি? কেন?
আর sys.getsizeof(10**100) তখন?
প্রয়োগ
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 সহ।
3Preservation + 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।
5Build section-এর interpreter-এ WEAK=True করলে [str: '55'] পাওয়া
গেল। এবার ধরুন t_eq(t_int(1), t_str("1")) — JS এ কী দেয়, Python-এ
কী? আপনার WEAK মোডে কী দেবে এখন, এবং সেটা কি consistent? (JS-এর
== বনাম ===-এর কথা মাথায় রাখুন।)
প্রয়োগ
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 কারণ এটা।
6OCaml-এ 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 উদাহরণ