এই পর্বে যা শিখবেন
- Alphabet, string, দৈর্ঘ্য, Σ*, language আর তাদের অপারেশন, সঙ্গে গণনা আর ছোট উপপাদ্যের প্রমাণ
- Phrase-structure grammar, derivation, leftmost derivation আর BNF; বাজারঘরের অর্ডার-কমান্ডের একটি ছোট ভাষা
- Parse tree, ambiguity, dangling else, আর একই language-এর ambiguous ও unambiguous grammar-এর সমতার প্রমাণ
- “এই grammar ঠিক এই language বানায়” প্রমাণ করা: balanced parentheses-এ দুই দিকের induction
- Chomsky hierarchy-র চার স্তর, প্রতিটির উদাহরণ, আর countability দিয়ে grammar-হীন language-এর অস্তিত্ব
১ · Alphabet, string আর language — ভাষা মানে string-এর সেট
বাজারঘরের কুপন কোড ধরুন EID-25 বা BOI-10: তিনটি বড় হাতের অক্ষর, একটি হাইফেন, দুটি অঙ্ক। কেউ eid25 লিখলে সিস্টেম বলে “ভুল কোড”। এখানে সিস্টেমের কাছে “ভাষা” মানে বৈধ string-গুলোর একটি সেট, আর প্রতিটি ইনপুটের প্রশ্ন একটাই: string-টি সেটে আছে কি না। বাংলা বা ইংরেজির মতো স্বাভাবিক ভাষাতেও একই প্রশ্ন ওঠে: কোন শব্দের সারি ব্যাকরণসম্মত বাক্য? এই পর্বে সেই প্রশ্নের গাণিতিক রূপ দেখবেন।
পর্ব ১২-এ alphabet আর Σ* recursive সংজ্ঞায় এসেছিল, আর পর্ব ১৮-এ দেখেছিলেন Σ* countable. এখানে সংজ্ঞাগুলো এক জায়গায় গুছিয়ে নিয়ে language-এর ওপর অপারেশনে যাব।
Alphabet Σ হলো চিহ্নের একটি সসীম, অশূন্য সেট। Σ-এর ওপর একটি string w = x1x2…xn হলো চিহ্নের একটি সসীম ক্রম; n = |w| তার দৈর্ঘ্য। দৈর্ঘ্য 0-এর string হলো empty string ε (পর্ব ১২-এ λ)। Σn = দৈর্ঘ্য n-এর সব string, Σ* = Σ0 ∪ Σ1 ∪ Σ2 ∪ ⋯ (Kleene star), আর Σ+ = Σ* − {ε}. দুটি string পরপর বসানো হলো concatenation: u = ab, v = ba হলে uv = abba, |uv| = |u| + |v|, আর εw = wε = w.
Concatenation সাধারণত commutative নয় (ab · ba = abba, কিন্তু ba · ab = baab), তবে associative: (uv)w = u(vw). আর ε হলো তার identity. পর্ব ৭০-এর ভাষায় (Σ*, ·) একটি monoid, group নয়, কারণ কোনো অশূন্য string-এর inverse নেই।
|Σ| = k ≥ 2 হলে |Σn| = kn, আর দৈর্ঘ্য n পর্যন্ত মোট string-সংখ্যা |Σ≤n| = 1 + k + ⋯ + kn = (kn+1 − 1)/(k − 1).
- দৈর্ঘ্য n-এর string-এ n-টি ঘর, প্রতিটিতে k-টি স্বাধীন পছন্দ, তাই product rule অনুযায়ী knটি। — পর্ব ৩১-এর গুণের নিয়ম
- আলাদা দৈর্ঘ্যের string আলাদা, তাই Σ0, …, Σn disjoint; যোগ করলে জ্যামিতিক ধারা। — sum rule আর পর্ব ১৭-এর সূত্র ∎
Σ = {a, b} হলে দৈর্ঘ্য 3 পর্যন্ত (ε-সহ) 1 + 2 + 4 + 8 = 15টি string; Σ = {a, b, c} হলে দৈর্ঘ্য 4 পর্যন্ত (35 − 1)/2 = 121টি।
Σ-এর ওপর একটি formal language হলো Σ*-এর যেকোনো subset L. L আর M দুটি language হলে union L ∪ M, intersection L ∩ M আর complement Σ* − L সাধারণ সেটের মতোই (পর্ব ১৫)। নতুন অপারেশন:
- Concatenation: LM = {uv : u ∈ L, v ∈ M}.
- ঘাত: L0 = {ε}, Ln+1 = LnL.
- Kleene star: L* = L0 ∪ L1 ∪ L2 ∪ ⋯, অর্থাৎ L-এর শব্দ যতবার খুশি (শূন্যবারও) জুড়ে পাওয়া সব string; L+ = LL*.
ছোট উদাহরণ নিন। L = {a, ab}, M = {b, ε} হলে LM = {ab, a, abb, ab} = {a, ab, abb}: চারটি জোড়া থেকে মাত্র তিনটি আলাদা string, কারণ a · b আর ab · ε একই। তাই |LM| ≤ |L| · |M|, সমতা সবসময় নয়। আরও কয়েকটি তথ্য, যা পরীক্ষায় প্রায়ই আসে: ∅L = ∅ (জোড়ার জন্য কোনো u নেই), {ε}L = L, আর ∅* = {ε}, কারণ L0 = {ε} সবসময় থাকে। খেয়াল করুন, ∅ আর {ε} আলাদা: প্রথমটিতে কোনো string নেই, দ্বিতীয়টিতে একটি string আছে, যার দৈর্ঘ্য শূন্য।
যেকোনো language L, M, N-এর জন্য: (ক) L(M ∪ N) = LM ∪ LN; (খ) L(M ∩ N) ⊆ LM ∩ LN, আর এখানে সমতা না-ও হতে পারে; (গ) (L*)* = L*.
- (ক) w ∈ L(M ∪ N) ⇔ w = uv, u ∈ L, আর v ∈ M বা v ∈ N ⇔ w ∈ LM বা w ∈ LN. — “বা” দুই দিকেই ভাগ হয়
- (খ) w = uv, u ∈ L, v ∈ M ∩ N হলে একই ভাঙনে w ∈ LM আর w ∈ LN. উল্টো দিক ভাঙে: L = {a, ab}, M = {bc}, N = {c} নিন। M ∩ N = ∅, তাই বাঁ দিক ∅; কিন্তু abc = a · bc ∈ LM আর abc = ab · c ∈ LN. সমস্যা হলো, দুই দিকে আলাদা ভাঙন কাজ করেছে। — পাল্টা-উদাহরণ
- (গ) L* ⊆ (L*)* স্পষ্ট (ঘাত 1). উল্টো দিকে, (L*)*-এর একটি string হলো কয়েকটি L*-string-এর জোড়া, আর প্রতিটি L*-string নিজে কয়েকটি L-শব্দের জোড়া; সব মিলিয়ে সসীম সংখ্যক L-শব্দের জোড়া, তাই L*-এ। — দুই inclusion ∎
একটি গণনা দিয়ে শেষ করি। L = {a, ab} হলে L*-এ দৈর্ঘ্য n-এর কয়টি string? L*-এর প্রতিটি string অনন্যভাবে ভাঙে, কারণ b সবসময় ঠিক আগের a-এর সঙ্গে “ab” হিসেবে আসে। শেষ টুকরো a হলে বাকিটা দৈর্ঘ্য n − 1-এর, ab হলে n − 2-এর। তাই f(n) = f(n − 1) + f(n − 2), f(0) = f(1) = 1: সংখ্যাগুলো 1, 1, 2, 3, 5, 8, … , Fibonacci (পর্ব ৪৪)। অনন্য ভাঙন না থাকলে এই হিসাব ভুল হতো; যেমন {a, aa}*-এ aaa তিনভাবে ভাঙে, কিন্তু string একটিই।
২ · Phrase-structure grammar — নিয়ম দিয়ে ভাষা বানানো
{aⁿbⁿ : n ≥ 0} বা সব balanced বন্ধনীর মতো অসীম language তালিকা করে লেখা যায় না। দরকার একটি সসীম বর্ণনা। একটি উপায় হলো ছক আর প্রতিস্থাপনের নিয়ম, যেমন স্কুলে শেখা “বাক্য → কর্তা + কর্ম + ক্রিয়া, কর্তা → আমি | রহিম, …”। Grammar এই ধারণার গাণিতিক রূপ।
একটি phrase-structure grammar G = (V, T, S, P), যেখানে V একটি সসীম variable-সেট (nonterminal), T একটি সসীম terminal-সেট (V ∩ T = ∅; T হলো ভাষার alphabet), S ∈ V হলো start symbol, আর P হলো সসীম সংখ্যক production α → β-এর সেট, যেখানে α, β ∈ (V ∪ T)*, আর α-তে অন্তত একটি variable থাকে। একই বাঁ দিকের কয়েকটি নিয়ম একসঙ্গে লেখা হয়: A → x | y.
u, v ∈ (V ∪ T)*-এর জন্য u ⇒ v (u থেকে এক ধাপে v), যদি u = xαy আর v = xβy, যেখানে α → β ∈ P. ⇒-এর reflexive-transitive closure (পর্ব ৫৪) হলো ⇒*: শূন্য বা তার বেশি ধাপে। S থেকে ⇒*-এ পাওয়া প্রতিটি string একটি sentential form, আর derivation-এর শেষে যদি কোনো variable না থাকে, তবে সেটি ভাষার একটি বাক্য। G-এর language L(G) = {w ∈ T* : S ⇒* w}.
উদাহরণ: V = {S}, T = {a, b}, নিয়ম S → aSb | ε. তাহলে
S ⇒ aSb ⇒ aaSbb ⇒ aaaSbbb ⇒ aaabbb.
প্রতিটি ধাপে বাঁয়ে একটি a আর ডানে একটি b যোগ হয়, শেষে S মুছে যায়। তাই L(G) = {aⁿbⁿ : n ≥ 0} মনে হয়; এর পূর্ণ প্রমাণ অনুশীলন ১-এ। একটি sentential form-এ একাধিক variable থাকলে কোনটি আগে বদলাবেন, সেই স্বাধীনতা থাকে। প্রতিবার সবচেয়ে বাঁয়ের variable বদলালে তাকে বলে leftmost derivation; §৪-এ দেখবেন এটি গাছের সঙ্গে এক-এক মেলে।
Grammar আসলে একটি recursive সংজ্ঞা (পর্ব ১২): নিয়মগুলো recursive step, আর L(G)-এর “আর কিছু নয়” অংশটি আসে “শুধু derivation-এ যা পাওয়া যায়” থেকে। তাই grammar-এর দাবি প্রমাণের প্রধান অস্ত্র structural induction বা derivation-এর দৈর্ঘ্যের ওপর induction.
নিচের ব্লক একটি grammar থেকে দৈর্ঘ্য n পর্যন্ত সব বাক্য বানায়: শুধু leftmost variable বদলায়, আর কোনো sentential form-এ n-এর বেশি terminal হয়ে গেলে সেই পথ ছেড়ে দেয় (terminal কখনো মোছে না, তাই এটি নিরাপদ)। তারপর ফলাফল সব string-এর মধ্য থেকে একটি সদস্যতা-পরীক্ষা (predicate) দিয়ে বাছাই করা সেটের সঙ্গে মেলায়। তৃতীয় grammar-টি ইচ্ছা করে ত্রুটিপূর্ণ। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে।
from itertools import product
def generate(rules, n, start="S"): # all terminal strings of length <= n
seen, todo, out = {start}, [start], set()
while todo:
form = todo.pop()
i = next((k for k, ch in enumerate(form) if ch in rules), None)
if i is None: # no variable left: a sentence
out.add(form)
continue
for rhs in rules[form[i]]: # expand the leftmost variable
new = form[:i] + rhs + form[i + 1:]
if sum(ch not in rules for ch in new) <= n and new not in seen:
seen.add(new)
todo.append(new)
return out
def balanced(w): # counter never negative, ends at 0
d = 0
for ch in w:
d += 1 if ch == "(" else -1
if d < 0:
return False
return d == 0
anbn = lambda w: w == "a" * (len(w) // 2) + "b" * (len(w) // 2)
N = 10
tests = [("S->(S)S|e", {"S": ["(S)S", ""]}, "()", balanced),
("S->aSb|e", {"S": ["aSb", ""]}, "ab", anbn),
("S->(S)|()S|e", {"S": ["(S)", "()S", ""]}, "()", balanced)]
for name, G, sigma, pred in tests:
gen = generate(G, N)
want = {"".join(t) for L in range(N + 1)
for t in product(sigma, repeat=L) if pred("".join(t))}
print(f"{name}: lengths 0,2,..,{N}:", [sum(len(w) == L for w in gen) for L in range(0, N + 1, 2)])
miss = sorted(want - gen, key=lambda w: (len(w), w))
print(" same as the predicate:", gen == want, "| missing:", miss[:2])আসল আউটপুট (ব্রাউজারের Python):
S->(S)S|e: lengths 0,2,..,10: [1, 1, 2, 5, 14, 42]
same as the predicate: True | missing: []
S->aSb|e: lengths 0,2,..,10: [1, 1, 1, 1, 1, 1]
same as the predicate: True | missing: []
S->(S)|()S|e: lengths 0,2,..,10: [1, 1, 2, 4, 8, 16]
same as the predicate: False | missing: ['(())()', '((())())']
S → (S)S | ε দৈর্ঘ্য 0, 2, …, 10-এ 1, 1, 2, 5, 14, 42টি string দেয়, আর predicate-এর সেটের সঙ্গে হুবহু মেলে; এগুলো Catalan সংখ্যা (পর্ব ৫০), আর §৬-এর প্রমাণ দেখাবে কেন। S → aSb | ε প্রতিটি জোড় দৈর্ঘ্যে ঠিক একটি string দেয়, aⁿbⁿ. তৃতীয় grammar প্রতিটি string balanced বানায় (তাই “missing” আছে, কিন্তু বাড়তি কিছু নেই), অথচ (())() আর ((())()) বানাতে পারে না: (S) দিয়ে শুরু হলে পরে আর কিছু বসানো যায় না। সতর্কতা: দৈর্ঘ্য 10 পর্যন্ত মিল একটি প্রমাণ নয়, শুধু প্রমাণে আস্থা বাড়ায়, বা ত্রুটিপূর্ণ grammar-এর মতো সরাসরি পাল্টা-উদাহরণ খুঁজে দেয়।
৩ · BNF — programming language আর বাজারঘরের অর্ডার-কমান্ড
Programming language-এর manual খুললে syntax লেখা থাকে এমন রূপে: <statement> ::= <assignment> | <if-statement>. এটি BNF (Backus–Naur Form): context-free grammar-এর (§৭) একটি লেখার রীতি। কোণ-বন্ধনীতে নাম মানে variable, উদ্ধৃতিতে লেখা মানে terminal, ::= মানে →, আর | মানে “অথবা”। John Backus ১৯৫৯ সালে ALGOL-এর syntax বর্ণনায় এই ধরনের রীতি প্রস্তাব করেন, আর Peter Naur-এর সম্পাদনায় ALGOL 60-এর রিপোর্টে তা ব্যবহৃত হয় (ইতিহাস ভাবনার প্রশ্ন ১-এ)। পরে EBNF আর ABNF-এর মতো রূপে ঐচ্ছিক অংশ ([ ]) আর পুনরাবৃত্তি ({ }) লেখার সংক্ষেপ যোগ হয়েছে; এদের প্রকাশ-ক্ষমতা একই।
বাজারঘরের গুদামের কর্মীরা একটি ছোট টার্মিনালে লেখা কমান্ড দিয়ে অর্ডার দেন বা বাতিল করেন। পুরো ভাষাটির BNF (সব নাম আর কোড কাল্পনিক):
<command> ::= <order> | <cancel>
<order> ::= "ORDER" <items> "TO" <district>
<items> ::= <item> | <item> "," <items>
<item> ::= <qty> "x" <code>
<qty> ::= <nz> | <nz> <digit>
<code> ::= "P-" <digit> <digit> <digit> <digit>
<district> ::= "DHAKA" | "KHULNA" | "SYLHET" | "RAJSHAHI"
<cancel> ::= "CANCEL" "BG" <digit><digit><digit><digit><digit><digit>
<nz> ::= "1" | "2" | … | "9"
<digit> ::= "0" | <nz>
<items>-এর নিয়মটি recursive: একটি item, অথবা একটি item, কমা, আর আবার <items>. তাই যত খুশি item লেখা যায়, অথচ নিয়ম মাত্র দুটি। <qty> বলে পরিমাণ 1 থেকে 99, আর শুরুতে 0 চলবে না। এই grammar থেকে পরীক্ষাযোগ্য একটি প্রোগ্রাম বানানোর সরল কৌশল হলো recursive descent: প্রতিটি variable-এর জন্য একটি ফাংশন, যা নিয়মের ডান দিক বাঁ থেকে ডানে মেলায়, আর অন্য variable এলে সেই ফাংশনকে ডাকে। Recursive নিয়ম তখন recursive ফাংশন হয়ে যায়; call stack-এর ভূমিকা DSA কোর্সের পর্ব ১৩-এ। নিচের ব্লক ঠিক এভাবে একটি recogniser লেখে, যা বলে কমান্ডটি ভাষায় আছে কি না, আর না থাকলে কোন token-এ আটকাল।
DIGITS, DISTRICTS = "0123456789", {"DHAKA", "KHULNA", "SYLHET", "RAJSHAHI"}
def qty(t): # <qty> ::= <nz> | <nz> <digit>
return 1 <= len(t) <= 2 and t[0] in "123456789" and all(c in DIGITS for c in t)
def code(t): # <code> ::= "P-" <digit> <digit> <digit> <digit>
return len(t) == 6 and t[:2] == "P-" and all(c in DIGITS for c in t[2:])
def items(toks, i): # <items> ::= <item> | <item> "," <items>; returns next index
if i + 2 >= len(toks) or not (qty(toks[i]) and toks[i + 1] == "x" and code(toks[i + 2])):
raise SyntaxError(f"token {i + 1}: expected <item>")
if i + 3 < len(toks) and toks[i + 3] == ",":
return items(toks, i + 4)
return i + 3
def command(line):
toks = line.replace(",", " , ").split()
if toks[:1] == ["CANCEL"]: # <cancel> ::= "CANCEL" "BG" <digit>^6
t = toks[1] if len(toks) == 2 else ""
if not (len(t) == 8 and t[:2] == "BG" and all(c in DIGITS for c in t[2:])):
raise SyntaxError("expected CANCEL <orderid>")
return "cancel"
if toks[:1] != ["ORDER"]:
raise SyntaxError("token 1: expected ORDER or CANCEL")
i = items(toks, 1) # <order> ::= "ORDER" <items> "TO" <district>
if toks[i:i + 1] != ["TO"] or len(toks) != i + 2 or toks[i + 1] not in DISTRICTS:
raise SyntaxError(f"token {i + 1}: expected TO <district> and end")
return f"order, {i // 4} item(s)"
for line in ["ORDER 2 x P-1042 TO KHULNA",
"ORDER 1 x P-0007, 12 x P-3301 TO SYLHET",
"CANCEL BG202611",
"ORDER 0 x P-1042 TO DHAKA",
"ORDER 2 x P-1042, TO DHAKA",
"ORDER 3 x P-55 TO RAJSHAHI",
"ORDER 2 x P-1042 TO COMILLA"]:
try:
print("OK ", line, "->", command(line))
except SyntaxError as e:
print("FAIL", line, "->", e)আসল আউটপুট (ব্রাউজারের Python):
OK ORDER 2 x P-1042 TO KHULNA -> order, 1 item(s)
OK ORDER 1 x P-0007, 12 x P-3301 TO SYLHET -> order, 2 item(s)
OK CANCEL BG202611 -> cancel
FAIL ORDER 0 x P-1042 TO DHAKA -> token 2: expected <item>
FAIL ORDER 2 x P-1042, TO DHAKA -> token 6: expected <item>
FAIL ORDER 3 x P-55 TO RAJSHAHI -> token 2: expected <item>
FAIL ORDER 2 x P-1042 TO COMILLA -> token 5: expected TO <district> and end
প্রথম তিনটি কমান্ড grammar থেকে derive করা যায়। দ্বিতীয়টিতে items ফাংশন নিজেকে একবার ডেকেছে, কমার পরের অংশের জন্য। চতুর্থটি আটকায় token 2-এ, কারণ <qty> 0 দিয়ে শুরু হতে পারে না; পঞ্চমটিতে কমার পরে নতুন item নেই; ষষ্ঠটিতে কোডে চারটির বদলে দুটি অঙ্ক; শেষটিতে COMILLA <district>-এর তালিকায় নেই। একটি সৎ মন্তব্য: এই ভাষায় কোনো বন্ধনী বা অন্য কোনো “ভেতরে ভেতরে” গঠন নেই, তাই §৭-এর হিসাবে এটি আসলে regular, আর পর্ব ৭৪ “Finite automaton আর regular expression-এর পরিচয়”-এর regular expression দিয়েই লেখা যেত (Python-এর regex Python কোর্সের পর্ব ৩০-এ)। কমান্ডে বান্ডেল-এর ভেতরে বান্ডেল, যেমন (2 x P-1042, (1 x P-0007)), যোগ করলেই grammar-এর আসল শক্তি লাগবে।
৪ · Derivation tree — বাক্যের গঠন গাছে
Derivation একটি ধাপের তালিকা, কিন্তু কোন অংশ কোন নিয়ম থেকে এসেছে, তা গাছে সবচেয়ে স্পষ্ট দেখা যায়। নিচের সংজ্ঞা context-free grammar-এর জন্য, যেখানে প্রতিটি নিয়মের বাঁ দিকে ঠিক একটি variable (§৭-এর type 2).
G-এর একটি parse tree (derivation tree) হলো একটি ordered rooted tree (পর্ব ৬৫), যার root-এ S, প্রতিটি ভেতরের node-এ একটি variable A, আর A-এর সন্তানরা বাঁ থেকে ডানে X1, …, Xk হলে A → X1…Xk একটি নিয়ম (ε-নিয়মে একটি ε-পাতা)। পাতাগুলো বাঁ থেকে ডানে পড়লে পাওয়া string হলো গাছের yield.
Context-free grammar G আর w ∈ T*-এর জন্য: w ∈ L(G) ⇔ yield w-এর একটি parse tree আছে। আরও নির্দিষ্টভাবে, w-এর parse tree আর w-এর leftmost derivation-এর মধ্যে একটি bijection আছে।
- গাছ থেকে derivation: গাছের ভেতরের node-গুলো preorder-এ (পর্ব ৬৬) ঘুরুন, আর প্রতিটি node-এ তার নিয়মটি খাটান। Preorder সবসময় এখনো-না-খোলা সবচেয়ে বাঁয়ের variable-কেই আগে পায়, তাই derivation-টি leftmost. — preorder-এর সংজ্ঞা
- Derivation থেকে গাছ: S দিয়ে শুরু করে প্রতিটি ধাপে যে variable বদলাল, গাছে তার নিচে নিয়মের ডান দিকের চিহ্নগুলো সন্তান হিসেবে বসান। Context-free নিয়ম আশপাশের চিহ্নের ওপর নির্ভর করে না, তাই প্রতিটি ধাপ ঠিক একটি node খোলে। — derivation-এর দৈর্ঘ্যে induction
- দুটি নির্মাণ একে অপরের inverse: একটি leftmost derivation থেকে গাছ বানিয়ে আবার preorder-এ পড়লে একই ধাপ একই ক্রমে আসে, আর উল্টোটাও। তাই এক-এক মিল। ∎ — দুই দিকের composition = identity
এখন গাণিতিক রাশির একটি grammar নিন, যাকে বলব grammar U:
E → E + T | T, T → T * F | F, F → ( E ) | a
E মানে expression (যোগফল), T মানে term (গুণফল), F মানে factor (একটি a বা বন্ধনীর ভেতরে একটি পূর্ণ রাশি)। a যেকোনো সংখ্যা বা চলকের প্রতিনিধি। নিচের ছবিতে a + a * a-এর একমাত্র parse tree.
Grammar U-তে a + a * a-এর parse tree
নীল বৃত্ত variable, কমলা বাক্স terminal. পাতাগুলো বাঁ থেকে ডানে পড়লে a + a * a. গুণটি T-এর নিচে, যোগটি root-এ: তাই প্রথমে গুণ, পরে যোগ।
নিচের stepper এই গাছের leftmost derivation দেখায়: প্রতিটি ধাপে সবচেয়ে বাঁয়ের variable বদলায়, আর বাঁয়ের তালিকায় সেই ধাপের নিয়মটি উজ্জ্বল হয়।
1E → E + T2E → T3T → T * F4T → F5F → ( E )6F → a
| এখন | |
| বদলাল | |
| ধাপ |
- শুরু। Sentential form শুধু E. লক্ষ্য a + a * a; এতে একটি + আছে, যা শুধু E → E + T দিতে পারে।
- যোগ। E → E + T: পাই E + T. যোগটি এখন গাছের root-এ বাঁধা পড়ল।
- বাঁ অংশ। সবচেয়ে বাঁয়ের variable E; + এর বাঁয়ে শুধু “a”, তাই E → T: পাই T + T.
- Term থেকে factor. বাঁয়ের T-এ * নেই, তাই T → F: পাই F + T.
- প্রথম a. F → a: পাই a + T. বাঁ অংশ শেষ, এখন সবচেয়ে বাঁয়ের variable হলো ডানের T.
- গুণ। ডানের অংশ a * a, তাই T → T * F: পাই a + T * F. গুণটি T-এর ভেতরে, যোগের নিচে।
- গুণের বাঁ অংশ। সবচেয়ে বাঁয়ের variable T, আর T → F: পাই a + F * F.
- দ্বিতীয় a. সবচেয়ে বাঁয়ের F → a: পাই a + a * F.
- শেষ। F → a: পাই a + a * a. মোট ৮টি ধাপ, কোনো variable বাকি নেই। প্রতিটি ধাপে পছন্দ ছিল একটিই, যা §৫-এর unambiguity-র ইঙ্গিত।
Parse tree থেকে E → T → F-এর মতো একক-সন্তানের শিকল আর বন্ধনী ছেঁটে ফেললে পাওয়া যায় পর্ব ৬৬-এর expression tree, যাকে compiler-এর ভাষায় abstract syntax tree বলে। তাই grammar-এর গঠনই ঠিক করে, রাশির মান কোন ক্রমে হিসাব হবে।
৫ · Ambiguity — একই string, দুটি গাছ
Context-free grammar G ambiguous, যদি L(G)-এর অন্তত একটি string-এর দুটি আলাদা parse tree থাকে (উপপাদ্য ৪.২ অনুযায়ী সমতুল্যভাবে, দুটি আলাদা leftmost derivation). নইলে G unambiguous.
সরল দেখতে একটি grammar, যাকে বলব grammar A: E → E + E | E * E | ( E ) | a. এটি “যোগ বা গুণ করে দুটি রাশি জোড়া যায়” কথাটি সরাসরি লেখে, কিন্তু কোন অপারেশন আগে, তা বলে না। নিচের stepper-এ a + a * a-এর দুটি গাছ দেখুন, আর তিনটি a-তে ক্রমে 2, 3, 4 বসিয়ে মান হিসাব করুন।
| গাছ | |
| root-এর নিয়ম | |
| মান |
- একটি string. পাতাগুলো a + a * a, নিচে প্রতিটি a-এর মান 2, 3, 4. Grammar A-তে এখনো কোনো গাছ বানানো হয়নি।
- গাছ ১, প্রথম নিয়ম। Root-এ E → E + E. বাঁয়ের E পাবে “a”, ডানের E পাবে “a * a”.
- গাছ ১ সম্পূর্ণ। ডানের E → E * E, আর তিনটি E → a. গুণটি নিচে, তাই আগে হিসাব হয়: 2 + 3 × 4 = 14. Leftmost derivation: E ⇒ E + E ⇒ a + E ⇒ a + E * E ⇒ a + a * E ⇒ a + a * a.
- গাছ ২, প্রথম নিয়ম। এবার root-এ E → E * E. বাঁয়ের E পাবে “a + a”, ডানের E পাবে “a”. একই string, অন্য ভাগ।
- গাছ ২ সম্পূর্ণ। বাঁয়ের E → E + E. যোগটি নিচে, তাই আগে হয়: (2 + 3) × 4 = 20. Leftmost derivation: E ⇒ E * E ⇒ E + E * E ⇒ a + E * E ⇒ a + a * E ⇒ a + a * a. দ্বিতীয় ধাপেই প্রথম derivation থেকে আলাদা।
- সিদ্ধান্ত। একই string-এর দুটি গাছ, তাই grammar A ambiguous, আর “মান” নির্দিষ্ট নয়। Grammar U-তে শুধু গাছ ১-এর মতো গঠন সম্ভব (§৪-এর stepper), তাই সেখানে মান সবসময় 14, যা সাধারণ গণিতের নিয়ম।
Grammar U দুটি কৌশলে ambiguity দূর করে: আলাদা স্তর (E, T, F) দিয়ে precedence, অর্থাৎ গুণ যোগের চেয়ে শক্ত বাঁধন; আর E → E + T-এর মতো বাঁ-দিকে-recursive নিয়ম দিয়ে বাঁ থেকে ডানে জোড়া (a + a + a মানে (a + a) + a). কিন্তু দুই grammar-এর language কি সত্যিই এক? না হলে ambiguity দূর করতে গিয়ে কিছু রাশি হারিয়ে ফেলতাম।
Grammar A আর grammar U ঠিক একই string-গুলো তৈরি করে।
- (L(U) ⊆ L(A)) U-এর যেকোনো derivation-এ T আর F-কে E নামে ডাকুন। তাহলে E → E + T হয় E → E + E, T → T * F হয় E → E * E, F → ( E ) আর F → a A-তেও আছে, আর E → T, T → F হয়ে যায় E → E, যা শুধু বাদ দিলেই চলে। ফলে প্রতিটি U-derivation একটি A-derivation হয়ে যায়, একই বাক্যে শেষ হয়। — নিয়মের অনুবাদ
- (উল্টো দিকের প্রস্তুতি) U-এর নিয়ম E → E + T | T থেকে, derivation-এর দৈর্ঘ্যে induction করে, E থেকে পাওয়া বাক্য ঠিক t1 + t2 + ⋯ + tk আকারের (k ≥ 1), যেখানে প্রতিটি ti T থেকে পাওয়া; একইভাবে T-এর বাক্য f1 * ⋯ * fj, যেখানে প্রতিটি fi F থেকে পাওয়া। — বাঁ-recursive নিয়মের শিকল
- L(U) তিনটি অপারেশনে বদ্ধ: x, y ∈ L(U) হলে x + y ∈ L(U) (দুটি term-এর যোগফলের তালিকা জোড়া দিলেই হয়), (x) ∈ L(U) (F → ( E )), আর x * y ∈ L(U): x = t1 + ⋯ + tk, y = s1 + ⋯ + sl হলে x * y = t1 + ⋯ + tk−1 + (tk * s1) + s2 + ⋯ + sl, আর tk * s1 দুটি গুণফলের জোড়া, তাই নিজেই একটি term (এখানে বাইরের বন্ধনীগুলো শুধু পড়ার সুবিধার জন্য, string-এর অংশ নয়)। — ধাপ ২
- (L(A) ⊆ L(U)) A-এর প্রতিটি বাক্য a থেকে শুরু করে x + y, x * y আর (x) দিয়ে বানানো, আর a ∈ L(U). ধাপ ৩ অনুযায়ী প্রতিটি নির্মাণ L(U)-এর ভেতরে থাকে, তাই A-এর parse tree-র ওপর structural induction-এ L(A) ⊆ L(U). ∎ — structural induction
নিচের ব্লক দুই grammar-এ একটি string-এর parse tree গোনে (প্রতিটি variable আর প্রতিটি substring-এর জন্য গাছের সংখ্যা একবার হিসাব করে মনে রেখে, অর্থাৎ dynamic programming), তারপর a, +, *, (, ) দিয়ে দৈর্ঘ্য 6 পর্যন্ত সব string পরীক্ষা করে দুই language মেলায়।
from functools import lru_cache
from itertools import product
def trees(w): # parse trees of w in A: E -> E+E | E*E | (E) | a
@lru_cache(None)
def E(i, j):
n = (j - i == 1 and w[i] == "a")
n += sum(E(i, k) * E(k + 1, j) for k in range(i + 1, j - 1) if w[k] in "+*")
if j - i >= 3 and w[i] == "(" and w[j - 1] == ")":
n += E(i + 1, j - 1)
return n
return E(0, len(w))
def trees_u(w): # U: E -> E+T | T, T -> T*F | F, F -> (E) | a
@lru_cache(None)
def F(i, j):
if j - i == 1:
return int(w[i] == "a")
return E(i + 1, j - 1) if w[i] == "(" and w[j - 1] == ")" and j - i >= 3 else 0
@lru_cache(None)
def T(i, j):
return F(i, j) + sum(T(i, k) * F(k + 1, j) for k in range(i + 1, j - 1) if w[k] == "*")
@lru_cache(None)
def E(i, j):
return T(i, j) + sum(E(i, k) * T(k + 1, j) for k in range(i + 1, j - 1) if w[k] == "+")
return E(0, len(w))
for w in ["a+a*a", "a+a+a", "a+a+a+a", "a*a+a*a+a", "(a+a)*a", "a+a+a+a+a+a"]:
print(f"{w:12} grammar A: {trees(w):3} tree(s) grammar U: {trees_u(w)} tree(s)")
N = 6
words = ["".join(t) for L in range(1, N + 1) for t in product("a+*()", repeat=L)]
LA = {w for w in words if trees(w)}
LU = {w for w in words if trees_u(w)}
print(f"length <= {N}: checked {len(words)} strings; |L(A)| = {len(LA)}, |L(U)| = {len(LU)}, equal: {LA == LU}")
print("max trees per string in U:", max(trees_u(w) for w in LU))আসল আউটপুট (ব্রাউজারের Python):
a+a*a grammar A: 2 tree(s) grammar U: 1 tree(s)
a+a+a grammar A: 2 tree(s) grammar U: 1 tree(s)
a+a+a+a grammar A: 5 tree(s) grammar U: 1 tree(s)
a*a+a*a+a grammar A: 14 tree(s) grammar U: 1 tree(s)
(a+a)*a grammar A: 1 tree(s) grammar U: 1 tree(s)
a+a+a+a+a+a grammar A: 42 tree(s) grammar U: 1 tree(s)
length <= 6: checked 19530 strings; |L(A)| = 15, |L(U)| = 15, equal: True
max trees per string in U: 1
a + a * a-এ grammar A দুটি গাছ দেয়, stepper-এর দুটিই। ২, ৩, ৪ আর ৫টি অপারেটরের রাশিতে A-এর গাছের সংখ্যা 2, 5, 14, 42: আবার Catalan সংখ্যা Cm (m = অপারেটর-সংখ্যা), কারণ প্রতিটি গাছ মানে বন্ধনী বসানোর একটি পূর্ণ উপায়, আর সব অপারেটরই A-তে সমান। U প্রতিটিতে ঠিক একটি গাছ দেয়। দৈর্ঘ্য 6 পর্যন্ত 19,530টি string-এর মধ্যে দুই language-এ ঠিক একই 15টি; সবগুলোর দৈর্ঘ্য বিজোড় (1, 3, 5), কারণ প্রতিটি নিয়ম দৈর্ঘ্যে জোড় সংখ্যক চিহ্ন যোগ করে। শেষ লাইন দেখায় পরীক্ষিত string-গুলোতে U-এর গাছ কখনো একের বেশি নয়। এটি উপপাদ্য ৫.২-এর ছোট ক্ষেত্রে যাচাই, আর U-এর unambiguity-র সাক্ষ্য, প্রমাণ নয়; সাধারণ প্রমাণ পরিকল্পিত Theory of Computation কোর্সের বিষয়।
Dangling else
Programming language-এ সবচেয়ে বিখ্যাত ambiguity: S → if b then S | if b then S else S | x. বাক্য if b then if b then x else x-এ else-টি কোন if-এর? ভেতরের if-এর সঙ্গে জুড়লে একটি গাছ, বাইরের if-এর সঙ্গে জুড়লে আরেকটি; দুটোই বৈধ, অর্থ আলাদা। (তিনটি if আর দুটি else থাকলে গাছ হয় 3টি; scratch-এ গোনা।) C আর Java নিয়ম করে দিয়েছে, else সবচেয়ে কাছের মেলানো-হয়নি এমন if-এর সঙ্গে যাবে; Python indentation দিয়ে, আর অনেক নতুন ভাষা বাধ্যতামূলক বন্ধনী দিয়ে প্রশ্নটিই এড়ায় (ভাবনার প্রশ্ন ৩)।
দুটি গভীর তথ্য বক্তব্য হিসেবে, প্রমাণ ছাড়া। প্রথমত, কিছু context-free language inherently ambiguous: তাদের প্রতিটি grammar-ই ambiguous. প্রচলিত উদাহরণ {aibjck : i = j অথবা j = k}, যেখানে aⁿbⁿcⁿ-এর মতো string “দুই কারণে” ভাষায় থাকে; এই ধরনের ফলের কৃতিত্ব সাধারণত Rohit Parikh-কে দেওয়া হয় (১৯৬০-এর দশক)। দ্বিতীয়ত, “এই context-free grammar কি ambiguous?” প্রশ্নের উত্তর দেওয়ার এমন কোনো সাধারণ algorithm নেই, যা সব grammar-এ কাজ করে; এটি undecidable বলে ১৯৬০-এর দশকের গোড়ায় কয়েকজন গবেষক দেখান। Undecidable মানে কী, তা পর্ব ৭৫ “Turing machine-এর পরিচয় — computability আর halting problem”-এ।
৬ · প্রমাণ: grammar ঠিক এই language-টিই বানায়
“G-এর language L” দাবিটি আসলে সেটের সমতা, L(G) = L, তাই দুটি inclusion প্রমাণ করতে হয়। Soundness (L(G) ⊆ L): grammar যা বানায়, সবই ঠিক; সাধারণত derivation-এর দৈর্ঘ্যে induction. Completeness (L ⊆ L(G)): L-এর প্রতিটি string grammar বানাতে পারে; সাধারণত string-এর দৈর্ঘ্যে strong induction (পর্ব ১১)। §২-এর ত্রুটিপূর্ণ grammar দেখিয়েছে, শুধু soundness যথেষ্ট নয়।
পর্ব ১২-এ balanced parentheses recursive নিয়মে সংজ্ঞায়িত হয়েছিল। এখানে একটি স্বাধীন, গণনাভিত্তিক সংজ্ঞা নিই: w ∈ {(, )}* balanced, যদি w-এ “(” আর “)”-এর সংখ্যা সমান, আর w-এর প্রতিটি prefix-এ “(” অন্তত “)”-এর সমান। Prefix-এ “(” বিয়োগ “)”-কে বলব গভীরতা।
GB: S → ( S ) S | ε হলে L(GB) = {w : w balanced}.
- (Soundness, ভিত্তি) S ⇒* w-এর প্রথম ধাপ S → ε হলে w = ε, যা balanced. — derivation-এর দৈর্ঘ্যে strong induction
- (Soundness, ধাপ) প্রথম ধাপ S → ( S ) S হলে w = (u)v, যেখানে দুই S থেকে আলাদাভাবে u আর v পাওয়া গেছে, প্রতিটি ছোট derivation-এ। Context-free নিয়মে দুই S একে অপরকে প্রভাবিত করে না, তাই এই ভাগ বৈধ। Induction hypothesis অনুযায়ী u, v balanced. — context-free-এর স্বাধীনতা
- (w balanced) গোনা সমান স্পষ্ট। Prefix-এর গভীরতা: “(” পরে 1; u-এর ভেতরে 1 + (u-এর prefix-এর গভীরতা) ≥ 1; “)” পরে 0; তারপর v-এর prefix-এর গভীরতা ≥ 0. কোথাও ঋণাত্মক নয়। — গভীরতার হিসাব
- (Completeness) |w|-এ strong induction. w = ε হলে S → ε. নইলে w balanced, তাই প্রথম চিহ্ন “(” (নইলে প্রথম prefix-এর গভীরতা −1). গভীরতা প্রথম যে অবস্থানে আবার 0 হয়, সেটি k (শেষে গভীরতা 0, তাই k আছে)। — first return
- w = (u)v লিখুন, যেখানে (u) হলো w-এর প্রথম k চিহ্ন। u-এর ভেতরে w-এর গভীরতা ≥ 1, তাই u-এর নিজের প্রতিটি prefix-এর গভীরতা ≥ 0, আর u শেষ হয় 0-এ: u balanced. v শুরু হয় গভীরতা 0 থেকে আর শেষ হয় 0-এ, মাঝে ঋণাত্মক নয়: v balanced. — দুই অংশ
- |u|, |v| < |w|, তাই induction hypothesis অনুযায়ী S ⇒* u আর S ⇒* v. ফলে S ⇒ (S)S ⇒* (u)S ⇒* (u)v = w. ∎ — induction hypothesis
ধাপ ৪–৫-এর ভাঙন অনন্য, কারণ “প্রথম যেখানে গভীরতা 0” একটিই। তাই প্রতিটি balanced string-এর ঠিক একটি parse tree, অর্থাৎ GB unambiguous. সঙ্গে একটি গণনাও আসে: দৈর্ঘ্য 2n-এর balanced string-এর সংখ্যা cn হলে, |u| = 2i বেছে নিয়ে cn+1 = ∑i=0..n cicn−i, c0 = 1. এটিই Catalan সংখ্যা-র recurrence, যা §২-এর আউটপুটে 1, 1, 2, 5, 14, 42 দেখিয়েছে। ত্রুটিপূর্ণ S → (S) | ()S | ε-এ প্রমাণটি ঠিক ধাপ ৬-এ ভাঙে: u আর v দুটিই অশূন্য হলে, যেমন (())()-এ u = (), v = (), কোনো নিয়ম (u)v-কে একসঙ্গে বানাতে পারে না।
৭ · Chomsky hierarchy — নিয়মের আকারে চার শ্রেণি
Noam Chomsky ১৯৫৬ সালে “Three models for the description of language” প্রবন্ধে আর ১৯৫৯ সালের একটি প্রবন্ধে নিয়মের আকারের ওপর ভিত্তি করে grammar-এর কয়েকটি স্তর প্রস্তাব করেন। পাঠ্যবইয়ে প্রচলিত রূপটি নিচে (ছোট বিবরণ বইভেদে সামান্য আলাদা)। ধরুন A, B ∈ V, a ∈ T, আর α, β ∈ (V ∪ T)*.
- Type 0 (unrestricted): সংজ্ঞা ২.১-এর যেকোনো নিয়ম α → β.
- Type 1 (context-sensitive): প্রতিটি নিয়মে |α| ≤ |β| (noncontracting); ব্যতিক্রম শুধু S → ε, যদি S কোনো নিয়মের ডান দিকে না থাকে।
- Type 2 (context-free): প্রতিটি নিয়ম A → β: বাঁ দিকে একটিমাত্র variable.
- Type 3 (regular): প্রতিটি নিয়ম A → aB বা A → a, সঙ্গে type 1-এর মতো S → ε.
একটি language type i, যদি কোনো type-i grammar তাকে তৈরি করে। Type 3 language-ই regular language, আর type 2 language হলো context-free language.
“Context-sensitive” নামের উৎস: পুরোনো রূপে নিয়ম ছিল γAδ → γβδ, অর্থাৎ A-কে β দিয়ে বদলানো যায় শুধু γ আর δ-এর “প্রসঙ্গে”। Noncontracting grammar আর এই রূপ একই language-শ্রেণি দেয়, এটি একটি জানা উপপাদ্য। Type 2-এ প্রসঙ্গ লাগে না: A যেখানেই থাকুক, বদলানো যায়। একটি সূক্ষ্মতা: type 2 grammar-এ A → ε থাকতে পারে, যা noncontracting নয়; তবু প্রতিটি context-free language type 1, কারণ যেকোনো context-free grammar থেকে ε-নিয়ম সরিয়ে (দরকার হলে শুধু S → ε রেখে) একই language-এর grammar বানানো যায়।
| Type | নিয়মের আকার | যন্ত্র (চেনার জন্য) | উদাহরণ language |
|---|---|---|---|
| 3 · regular | A → aB | a | finite automaton (পর্ব ৭৪) | a*b+, বাজারঘরের অর্ডার নম্বর |
| 2 · context-free | A → β | pushdown automaton | aⁿbⁿ, balanced বন্ধনী, গাণিতিক রাশি |
| 1 · context-sensitive | |α| ≤ |β| | linear bounded automaton | aⁿbⁿcⁿ |
| 0 · unrestricted | α → β | Turing machine (পর্ব ৭৫) | halting problem-এর “হ্যাঁ”-সেট |
চারটি শ্রেণি একটির ভেতরে আরেকটি
প্রতিটি ভেতরের বাক্স বাইরের বাক্সের প্রকৃত (proper) subset. প্রতিটি বাক্সে এমন একটি উদাহরণ, যা ঠিক তার ভেতরের বাক্সে নেই।
Type 3 ⊊ type 2 ⊊ type 1 ⊊ type 0. বিশেষত (ক) {aⁿbⁿ : n ≥ 1} context-free কিন্তু regular নয়; (খ) {aⁿbⁿcⁿ : n ≥ 1} context-sensitive কিন্তু context-free নয়।
“⊆” অংশগুলো সংজ্ঞা থেকেই প্রায় সরাসরি (type 2 ⊆ type 1-এর ε-সূক্ষ্মতা ওপরে)। কড়াভাবে আলাদা হওয়ার প্রমাণ এখানে নয়: (ক)-এর জন্য regular language-এর pumping lemma পর্ব ৭৪-এ পূর্ণ প্রমাণসহ আসবে; (খ)-এর জন্য context-free language-এর pumping lemma লাগে, যা পরিকল্পিত Theory of Computation কোর্সে; আর type 1 ⊊ type 0-এর যুক্তি পর্ব ৭৫-এর decidability-র ওপর দাঁড়ায়। এই পর্বের সব বিষয়ই আসলে পরিচিতি; automata, grammar আর computability-র পূর্ণ তত্ত্ব পরিকল্পিত Theory of Computation কোর্সের বিষয়।
Grammar-এর শ্রেণি আর language-এর শ্রেণি আলাদা জিনিস। একটি grammar-এর type দেখে বলা যায় তার language অন্তত কতটা সরল, কিন্তু অন্য কোনো সরল grammar একই language দিতে পারে। নিচের stepper চারটি grammar-কে শ্রেণিতে বসায়; প্রতিটির language scratch-এ দৈর্ঘ্য 5–12 পর্যন্ত সব derivation বানিয়ে মেলানো।
1G1: S → aS | bT | b2 T → bT | b3G2: S → aSb | ab4G3: S → aSBC | aBC5 CB → BC, aB → ab6 bB → bb, bC → bc7 cC → cc8G4: S → aS | ε9 aa → a
| নিয়মের আকার | |
| grammar-এর type | |
| language | |
| language-এর শ্রেণি |
- G1. প্রতিটি নিয়ম “একটি terminal, তারপর বড়জোর একটি variable”: type 3. S প্রথমে যত খুশি a দেয়, তারপর অন্তত একটি b. Language {ambn : m ≥ 0, n ≥ 1}, regular.
- G2. S → aSb-এর ডান দিকে variable দুই terminal-এর মাঝখানে, তাই type 3 নয়; বাঁ দিকে একটি variable, তাই type 2. Language {aⁿbⁿ : n ≥ 1}, যা উপপাদ্য ৭.২ অনুযায়ী regular নয়: কোনো type 3 grammar এটি দিতে পারে না।
- G3. CB → BC, aB → ab ইত্যাদির বাঁ দিকে দুটি চিহ্ন, তাই type 2 নয়; কিন্তু কোনো নিয়মে string ছোট হয় না, তাই type 1. S → aSBC বারবার খাটিয়ে aⁿ(BC)ⁿ, তারপর CB → BC দিয়ে সব B বাঁয়ে আর সব C ডানে সরে, আর শেষের চার নিয়ম বাঁ থেকে ডানে ছোট হাতের অক্ষরে বদলায়। Language {aⁿbⁿcⁿ : n ≥ 1}; scratch-এ দৈর্ঘ্য 12 পর্যন্ত ঠিক abc, aabbcc, aaabbbccc, aaaabbbbcccc এসেছে।
- G4. aa → a নিয়মে দুই চিহ্ন এক চিহ্নে নামে, তাই noncontracting নয়: grammar-টি শুধু type 0. কিন্তু তার language a*, যা G: S → aS | ε-এর মতো type 3 grammar দিয়েও পাওয়া যায়। Grammar “জটিল”, language সরল।
- সারকথা। Grammar-এর type একটি ওপরের সীমা। Language-এর শ্রেণি হলো তাকে তৈরি করতে পারে এমন সবচেয়ে সরল grammar-এর type: G1 আর G4-এর language regular, G2-এর context-free, G3-এর context-sensitive.
৮ · কয়টি language-এর grammar আছে? — countability-র উত্তর
Type 0 সবচেয়ে উদার শ্রেণি। তাহলে কি প্রতিটি language-এর কোনো না কোনো grammar আছে? পর্ব ১৮-এর গণনা বলে, না; আর যুক্তিটি এক অনুচ্ছেদের।
একটি নির্দিষ্ট alphabet T-এর ওপর সব grammar-এর সেট countable, কিন্তু T-এর ওপর সব language-এর সেট uncountable. তাই এমন language আছে, যা কোনো grammar তৈরি করে না; এমনকি T = {a} হলেও।
- Variable-গুলোর নাম বদলালে L(G) বদলায় না, তাই ধরে নিন variable-এর নাম V1, V2, V3, … . তাহলে প্রতিটি grammar একটি সসীম লেখা, যেমন
S=V1; V1->aV1b; V1->;, একটি সসীম alphabet Δ = T ∪ {V, 0, …, 9, -, >, ;, =, S}-এর ওপর। — সংকেতে লেখা - আলাদা grammar আলাদা লেখা পায়, তাই grammar-এর সেট থেকে Δ*-এ একটি one-to-one ফাংশন আছে। Δ* countable (পর্ব ১৮), আর countable সেটের subset countable. — injection
- প্রতিটি grammar ঠিক একটি language দেয়, তাই grammar আছে এমন language-এর সেটও countable. — countable সেটের image
- সব language-এর সেট P(T*), আর T* countably infinite, তাই Cantor-এর উপপাদ্যে P(T*) uncountable. Uncountable সেট একটি countable subset-এর সমান হতে পারে না। ∎ — পর্ব ১৮-এর diagonal
T = {a}-এ diagonal সরাসরি লেখা যায়। Grammar-গুলো ক্রমে G1, G2, … সাজান (shortlex ক্রমে তাদের লেখা), আর নিন D = {ai : ai ∉ L(Gi)}. প্রতিটি i-এ D আর L(Gi) অন্তত string ai-এ আলাদা, তাই D কোনো grammar-এর language নয়। এই diagonal argument একটি অস্তিত্বের প্রমাণ; কোনো স্বাভাবিক, নির্দিষ্ট উদাহরণ এটি দেয় না। পর্ব ৭৫ দেখাবে type 0 grammar আর Turing machine একই language চেনে, আর halting problem-এর “না”-সেটের মতো নির্দিষ্ট language-এর কোনো grammar নেই।
৯ · পরীক্ষার অঙ্ক আর একটি ভুল প্রমাণ
অঙ্ক ১: string আর language গোনা
(ক) Σ = {0, 1, 2}-এ দৈর্ঘ্য 2 পর্যন্ত কয়টি string? (খ) L = {a, ab} হলে L2 লিখুন। (গ) L*-এ দৈর্ঘ্য 5-এর কয়টি string?
- (ক) 1 + 3 + 9 = 13 (ε-সহ)। সূত্রে (33 − 1)/2 = 13.
- (খ) L2 = {aa, aab, aba, abab}: চারটি জোড়া থেকে চারটি আলাদা string, এখানে কোনো মিল নেই।
- (গ) §১-এর recurrence f(n) = f(n − 1) + f(n − 2), f(0) = f(1) = 1: f(2) = 2, f(3) = 3, f(4) = 5, f(5) = 8. তালিকা: aaaaa, aaaab, aaaba, aabaa, abaaa, aabab, abaab, ababa. (scratch-এ গুনে মেলানো।)
অঙ্ক ২: grammar বানান, ambiguity ধরুন
L = {aibj : i ≥ j ≥ 0}-এর একটি grammar দিন। প্রস্তাব G: S → aS | aSb | ε. এটি কি ambiguous? হলে একটি unambiguous grammar দিন।
- G-এর প্রতিটি নিয়ম বাঁয়ে একটি a দেয়, আর b দিলে সঙ্গে একটি a দেয়, তাই b কখনো a-এর চেয়ে বেশি হয় না, আর সব a সব b-এর আগে থাকে; উল্টো দিকে aibj পেতে j বার aSb, তারপর i − j বার aS, শেষে ε. তাই L(G) = L.
- aab-এর দুটি গাছ: S ⇒ aS ⇒ aaSb ⇒ aab, আর S ⇒ aSb ⇒ aaSb ⇒ aab. প্রথমটিতে বাইরের a “একা”, দ্বিতীয়টিতে ভেতরের a. তাই G ambiguous.
- সমাধান: আগে সব একা a, পরে জোড়া: S → aS | T, T → aTb | ε. এখন aibj-এ একা a ঠিক i − j-টি আর তারা বাইরে, তাই গাছ একটিই। Scratch-এ দৈর্ঘ্য 8 পর্যন্ত দুই grammar-এর language L-এর সমান আর নতুন grammar-এ প্রতিটি string-এর ঠিক একটি গাছ মিলেছে; aab-এ G-এর গাছ 2, aaab-এ 3.
অঙ্ক ৩: গাছ গোনা
Grammar A (E → E + E | E * E | ( E ) | a)-এ a * a * a * a * a-এর কয়টি parse tree? চারটি অপারেটর, প্রতিটি গাছ একটি পূর্ণ বন্ধনী-বিন্যাস, তাই C4 = C(8, 4)/5 = 70/5 = 14. Grammar U-তে 1.
প্রমাণের ভুল ধরুন: “সমান সংখ্যক বন্ধনী”
দাবি: “S → ( S ) | S S | ε grammar-টি ঠিক সেই সব string বানায়, যাতে ( আর ) সমান সংখ্যক। প্রমাণ: S → ( S ) একটি ( আর একটি ) যোগ করে, S → S S আর S → ε কোনো বন্ধনী যোগ করে না। তাই derivation-এর প্রতিটি ধাপে দুই বন্ধনীর সংখ্যা সমান থাকে। ∎”
ভুল কোথায়: যুক্তিটি শুধু soundness দেখায়: grammar-এর প্রতিটি string-এ সংখ্যা সমান। দাবির অন্য দিক, “সমান সংখ্যার প্রতিটি string grammar বানায়”, প্রমাণই হয়নি, আর তা মিথ্যা: )(-এ সংখ্যা সমান, কিন্তু grammar-এর প্রতিটি string ( দিয়ে শুরু হয় (বা ε)। সঠিক দাবি হলো L = balanced string-এর সেট, যার প্রমাণে উপপাদ্য ৬.১-এর মতো দুই দিক লাগে; আর এই grammar ambiguous-ও, কারণ ()()()-কে S S দিয়ে দুইভাবে ভাগ করা যায়।
আরও গভীরে: Context-free grammar-এ যেকোনো string-এর সদস্যতা Cocke–Younger–Kasami (CYK) algorithm-এ O(n3) সময়ে নির্ণয় করা যায়, আর Leslie Valiant ১৯৭৫ সালে দেখান যে এটি matrix গুণের মতো দ্রুত করা সম্ভব। কিন্তু দুটি context-free grammar একই language দেয় কি না, বা একটি grammar সব string বানায় কি না, এসব প্রশ্ন undecidable. Programming language-এর বাস্তব parser সাধারণত LL বা LR-এর মতো সীমিত grammar-শ্রেণি নেয়, যাতে linear সময়ে parse হয়; Donald Knuth ১৯৬৫ সালে LR(k) grammar সংজ্ঞায়িত করেন, আর প্রতিটি LR(k) grammar unambiguous.
পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:
- ∅* আর {ε}* কী? দুটিই {ε}.
- {aⁿbⁿ : n ≥ 0}-এর grammar? S → aSb | ε; এটি context-free, কিন্তু language-টি regular নয়।
- একটি grammar ambiguous, কীভাবে দেখাবেন? একটি string আর তার দুটি আলাদা parse tree (বা দুটি leftmost derivation) দেখান।
- a + a * a-এর leftmost derivation দিন। E ⇒ E + T ⇒ T + T ⇒ F + T ⇒ a + T ⇒ a + T * F ⇒ a + F * F ⇒ a + a * F ⇒ a + a * a.
- Chomsky-র চার শ্রেণি আর তাদের যন্ত্র? Regular–finite automaton, context-free–pushdown automaton, context-sensitive–linear bounded automaton, unrestricted–Turing machine.
১০ · AI-এর যুগে — grammar নিয়ে AI-এর উত্তর যাচাই
AI সহকারী grammar লিখতে বেশ পারদর্শী, কিন্তু এমন ভুল নিয়মিত করে, যা চোখে পড়ে না, কারণ grammar দেখতে “ঠিকঠাক” লাগে। কয়েকটি পরিচিত ভুল আর ধরার উপায়:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| Grammar ভাষার একটি অংশই বানায় (ε বা ছোট string বাদ, বা §২-এর মতো কিছু গঠন বাদ), অথবা বাড়তি string বানায় | দৈর্ঘ্য n পর্যন্ত সব বাক্য বানিয়ে একটি স্বাধীন predicate-এর সঙ্গে দুই দিকে মেলান |
| “প্রমাণে” শুধু soundness, অর্থাৎ grammar যা বানায় তা ঠিক; completeness বাদ | প্রমাণে দুটি inclusion আলাদা শিরোনামে আছে কি না দেখুন |
| “এই grammar unambiguous” বলা, কোনো যুক্তি ছাড়া | ছোট string-এ parse tree গুনুন; একটিতে 2 পেলেই দাবি বাতিল |
| Grammar-এর type আর language-এর type গুলিয়ে ফেলা, বা aⁿbⁿ-কে regular বলা | শ্রেণির সংজ্ঞা আর উপপাদ্য ৭.২ মিলিয়ে দেখুন |
| Precedence উল্টো (গুণের চেয়ে যোগ শক্ত), বা associativity ডান দিকে | a + a * a আর a − a − a-এর গাছ হাতে আঁকুন, সংখ্যা বসিয়ে মান মেলান |
| ইতিহাসের সাল আর নাম (BNF, Chomsky, Pāṇini) আত্মবিশ্বাসের সঙ্গে বানানো | মূল প্রবন্ধ বা বিশ্বস্ত পাঠ্যবইয়ে মেলান; না পেলে “অনিশ্চিত” লিখুন |
AI-এর কাছে grammar বা প্রমাণ চাইলে এমন কাঠামো দিন, যাতে প্রতিটি দাবি আলাদা করে যাচাই করা যায়:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
Grammar-এর দাবি যাচাই করা সহজ, কারণ ছোট দৈর্ঘ্যে সব string গুনে ফেলা যায়: দৈর্ঘ্য 10 পর্যন্ত {(, )}-এ মাত্র 2,047টি। তবে মনে রাখবেন, ছোট ক্ষেত্রে মিল একটি প্রমাণ নয়; ভুল ধরার জাল মাত্র।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Pāṇini থেকে Backus: programming language-এর syntax লেখার রীতি কোথা থেকে এল?
নিয়ম দিয়ে ভাষা বর্ণনার ধারণা খুব পুরোনো। সংস্কৃত ব্যাকরণবিদ Pāṇini-র “অষ্টাধ্যায়ী” প্রায় চার হাজার সূত্রে সংস্কৃতের রূপতত্ত্ব আর বাক্যগঠন বর্ণনা করে; তাঁর সময়কাল সাধারণত খ্রিস্টপূর্ব পঞ্চম থেকে চতুর্থ শতাব্দীর মধ্যে ধরা হয়, তবে সুনির্দিষ্ট তারিখ অনিশ্চিত। তাঁর সূত্রে প্রতিস্থাপনের নিয়ম, সংক্ষিপ্ত প্রতীক আর নিয়মের মধ্যে অগ্রাধিকারের ব্যবস্থা আছে, যা আধুনিক rewriting system-এর সঙ্গে আশ্চর্য মিল দেখায়।
আধুনিক গল্প শুরু হয় ১৯৫০-এর দশকের শেষে। FORTRAN-এর মতো প্রথম দিকের ভাষার syntax বর্ণনা হতো ইংরেজি গদ্যে, যা দ্ব্যর্থক আর অসম্পূর্ণ। আন্তর্জাতিক একটি ভাষা (পরে ALGOL নামে পরিচিত) নিয়ে কাজের সময় John Backus ১৯৫৯ সালে প্যারিসের একটি সম্মেলনে syntax-কে আনুষ্ঠানিক নিয়মে লেখার একটি রীতি উপস্থাপন করেন। Peter Naur এই রীতি কিছুটা সরল করে ALGOL 60-এর রিপোর্টে (১৯৬০) ব্যবহার করেন, আর সেই রিপোর্ট পরে ভাষা-বর্ণনার আদর্শ হয়ে দাঁড়ায়।
নাম নিয়েও একটি ছোট ইতিহাস আছে। শুরুতে অনেকে “Backus Normal Form” বলতেন। Donald Knuth ১৯৬৪ সালে একটি চিঠিতে যুক্তি দেন যে এটি কোনো “normal form” নয়, আর Naur-এর অবদানও স্বীকার করা উচিত, তাই “Backus–Naur Form”. ১৯৬৭ সালে P. Z. Ingerman আরেকটি চিঠিতে Pāṇini-র অগ্রাধিকারের কথা তুলে “Pāṇini–Backus Form” নামের প্রস্তাব দেন বলে উল্লেখ পাওয়া যায়; নামটি জনপ্রিয় হয়নি, কিন্তু প্রশ্নটি ইতিহাসের প্রতি একটি সৎ দৃষ্টি।
Chomsky-র ১৯৫৬ সালের কাজ আর Backus-এর রীতি প্রায় একই সময়ে, ভিন্ন উদ্দেশ্যে এসেছিল: একটি ভাষাবিজ্ঞানে, অন্যটি প্রকৌশলে। পরে দেখা গেল BNF আসলে context-free grammar-ই। আজ JSON, HTTP আর ইমেইলের মানদণ্ডে ABNF নামের একটি রূপ ব্যবহৃত হয়, আর প্রায় প্রতিটি programming language-এর রেফারেন্সে কোনো না কোনো BNF-ধাঁচের grammar থাকে।
মূল উপলব্ধি: BNF হলো context-free grammar লেখার একটি ব্যবহারিক রীতি; এর জন্ম প্রকৌশলের প্রয়োজনে, গণিত এসেছে ভাষাবিজ্ঞান থেকে, আর নিয়মে ভাষা বাঁধার ধারণা হাজার বছরের পুরোনো।
প্র ০২মানুষের ভাষা কি context-free? Chomsky-র শ্রেণি কেন ভাষাবিজ্ঞানে বিতর্ক জাগাল?
Chomsky-র মূল প্রশ্ন ছিল ভাষাবিজ্ঞানের: ইংরেজির মতো একটি স্বাভাবিক ভাষার ব্যাকরণ কোন ধরনের নিয়মে লেখা সম্ভব? ১৯৫৬ সালের প্রবন্ধে তিনি যুক্তি দেন যে finite-state মডেল (আজকের type 3) ইংরেজির জন্য যথেষ্ট নয়, কারণ ইংরেজিতে “if … then …”, “either … or …”-এর মতো কাঠামো যত খুশি গভীরভাবে একটির ভেতরে আরেকটি বসানো যায়, অনেকটা aⁿbⁿ বা balanced বন্ধনীর মতো। বাংলাতেও “যে লোকটি যে বইটি যে দোকান থেকে কিনেছিল …” ধরনের বাক্য ভেতরে ভেতরে গাঁথা যায়, যদিও বাস্তবে তিন-চার স্তরের পর মানুষ আর বুঝতে পারে না।
পরের প্রশ্ন: context-free কি যথেষ্ট? কয়েক দশক ধরে বিতর্ক চলে, কারণ বহু প্রস্তাবিত পাল্টা-উদাহরণ পরে ভুল প্রমাণিত হয়। ১৯৮৫ সালে Stuart Shieber সুইস জার্মানের একটি গঠন বিশ্লেষণ করে যুক্তি দেন যে তা context-free নয়। সেখানে কয়েকটি বিশেষ্য আর তাদের ক্রিয়া “crossing” ক্রমে আসে, অর্থাৎ a1a2b1b2-এর মতো, যেখানে ai আর bi মিলতে হবে। এটি গাণিতিকভাবে {ww}-ধাঁচের ভাষার আত্মীয়, যা context-free নয় বলে জানা। এই যুক্তি ব্যাপকভাবে গৃহীত বলে বর্ণিত, যদিও ভাষাবিজ্ঞানীরা এর ব্যাখ্যা নিয়ে আলোচনা করেন।
এর ফলে “mildly context-sensitive” নামের একটি মধ্যবর্তী শ্রেণির ধারণা গুরুত্ব পায়: context-free-এর চেয়ে একটু শক্তিশালী, কিন্তু context-sensitive-এর মতো ভারী নয়, আর polynomial সময়ে parse করা যায়। Tree-adjoining grammar-এর মতো formalism এই শ্রেণির উদাহরণ।
আজকের বড় ভাষা মডেল কোনো স্পষ্ট grammar ছাড়াই তথ্য থেকে ভাষার নিয়ম “শেখে”। তবু তারা কতটা গভীর গাঁথা বাক্য ঠিকঠাক সামলায়, সেই পরীক্ষায় গবেষকরা প্রায়ই balanced বন্ধনী বা aⁿbⁿ-এর মতো formal language ব্যবহার করেন, কারণ সেখানে সঠিক উত্তর গাণিতিকভাবে নিশ্চিত।
মূল উপলব্ধি: Chomsky hierarchy জন্মেছিল মানুষের ভাষার প্রশ্ন থেকে; ভেতরে ভেতরে গাঁথা গঠন finite-state-এর বাইরে নেয়, আর crossing নির্ভরতা কিছু ভাষাকে context-free-এরও বাইরে নিয়ে যায় বলে যুক্তি আছে।
প্র ০৩Dangling else: একই সমস্যার সমাধান বিভিন্ন programming language কীভাবে করেছে?
সমস্যাটি §৫-এ দেখেছেন: if b then if b then x else x-এ else দুই if-এর যেকোনোটির হতে পারে। মানুষের চোখে indentation দেখে মনে হয় উত্তর স্পষ্ট, কিন্তু অনেক ভাষায় space-এর কোনো অর্থ নেই, তাই grammar-কেই সিদ্ধান্ত দিতে হয়। ভুল সিদ্ধান্তে একটি শর্তের কাজ অন্য শর্তে চলে যায়, যা কম্পাইল হয়ে যায় কিন্তু ভুল আচরণ করে, ধরা কঠিন।
প্রথম পথ: grammar এমনভাবে লেখা, যাতে ambiguity-ই না থাকে। ALGOL 60-এর রিপোর্ট then-এর পরে সরাসরি আরেকটি if-statement বসাতে দেয়নি বলে বর্ণিত; তাই সেখানে এই বাক্যটিই বৈধ নয়, আর লেখককে begin … end দিয়ে স্পষ্ট করতে হয়। আরেকটি পরিচিত কৌশল হলো দুই ধরনের statement আলাদা করা: “matched” (প্রতিটি if-এর else আছে) আর “unmatched”, আর নিয়ম করা যে else-এর আগে শুধু matched statement বসতে পারে। এতে grammar বড় হয়, কিন্তু unambiguous থাকে।
দ্বিতীয় পথ: grammar ambiguous রেখে একটি বাড়তি নিয়ম, “else সবচেয়ে কাছের মেলানো-হয়নি এমন if-এর সঙ্গে যায়”। C, C++ আর Java এই পথে। Yacc-ধাঁচের LR parser generator এখানে একটি shift-reduce conflict দেখায়, আর ডিফল্টে shift বেছে নেয়, যা ঠিক এই নিয়মটিই কার্যকর করে; তাই অনেক grammar-লেখক এই একটি conflict জেনেশুনে রেখে দেন।
তৃতীয় পথ: syntax বদলানো। Python-এ indentation-ই block ঠিক করে, তাই else কোন if-এর তা দেখা যায়। Go, Rust আর Swift-এর মতো ভাষায় if-এর body-তে বন্ধনী বাধ্যতামূলক, ফলে প্রশ্নটি ওঠেই না। কিছু ভাষা “end if” বা “fi”-এর মতো সমাপ্তি-চিহ্ন রাখে।
বাজারঘরের কোডে শিক্ষাটি ব্যবহারিক: বন্ধনী ছাড়া nested if লিখবেন না, আর linter-এর এই সতর্কবার্তা বন্ধ করবেন না।
মূল উপলব্ধি: Ambiguity দূর করার তিন উপায়: grammar নতুন করে লেখা, বাড়তি নির্বাচন-নিয়ম, বা ভাষার syntax বদলানো; প্রতিটিতে grammar-এর সরলতা, ব্যবহারকারীর সুবিধা আর ভুলের ঝুঁকির মধ্যে আপস।
প্র ০৪Ambiguity যাচাইয়ের algorithm নেই, তবু compiler কীভাবে নিশ্চিত হয় যে তার grammar unambiguous?
§৫-এ বলা হয়েছে, “একটি context-free grammar ambiguous কি না” সব grammar-এর জন্য নির্ণয় করে এমন algorithm নেই। এটি কোনো প্রকৌশলের দুর্বলতা নয়, গাণিতিক সীমা। তাহলে programming language-এর বড় প্রকল্পগুলো কী করে?
উত্তর: সব grammar নয়, একটি সীমিত শ্রেণি নিয়ে কাজ করা, যেখানে প্রশ্নটি সহজ। Donald Knuth ১৯৬৫ সালে LR(k) grammar সংজ্ঞায়িত করেন: এমন grammar, যা বাঁ থেকে ডানে পড়ে, শুধু সামনের k-টি চিহ্ন দেখে প্রতিটি সিদ্ধান্ত নিতে পারে। একটি grammar LR(1) কি না, তা algorithm দিয়ে যাচাই করা যায়: parser generator টেবিল বানানোর চেষ্টা করে, আর কোনো ঘরে দুটি সম্ভাব্য কাজ (conflict) পেলে জানায়। Conflict না থাকলে grammar নিশ্চিতভাবে unambiguous, আর parser linear সময়ে চলে। Conflict থাকলে grammar ambiguous হতেও পারে, না-ও পারে; তখন লেখক grammar বদলান বা precedence ঘোষণা করেন। এটি “নিরাপদ দিকে ভুল”: যাচাইকারী কখনো ভুল করে unambiguous বলে না।
আরেকটি আধুনিক পথ হলো Parsing Expression Grammar (PEG), যা Bryan Ford ২০০৪ সালে প্রস্তাব করেন। PEG-এ “অথবা” ক্রমযুক্ত: প্রথম বিকল্প মিললে সেটাই নেওয়া হয়, পরেরগুলো দেখা হয় না। তাই সংজ্ঞাগতভাবেই প্রতিটি ইনপুটের বড়জোর একটি parse, ambiguity থাকতেই পারে না। তবে মূল্য আছে: grammar আসলে কোন language বোঝায়, তা পড়ে বোঝা কঠিন হতে পারে, কারণ বিকল্পের ক্রম বদলালে language বদলে যায়। Python ৩.৯ সংস্করণে CPython-এর parser PEG-ভিত্তিক করা হয় (PEP 617).
গাণিতিক শিক্ষাটি সাধারণ: একটি সাধারণ প্রশ্ন undecidable হলেও তার একটি যথেষ্ট শর্ত প্রায়ই decidable, আর বাস্তবে সেটুকুই যথেষ্ট। পর্ব ৭৭ “SAT আর SMT solver — discrete math দিয়ে software যাচাই”-এ এই দর্শন আবার দেখবেন।
মূল উপলব্ধি: Ambiguity সাধারণভাবে undecidable, তাই বাস্তব parser LR বা PEG-এর মতো সীমিত কাঠামো নেয়, যেখানে unambiguity হয় যাচাই করা যায়, নয় সংজ্ঞায় বাঁধা।
প্র ০৫নিরাপত্তা আর AI-এর যুগে grammar কেন আবার গুরুত্বপূর্ণ হয়ে উঠছে?
একটি প্রোগ্রাম বাইরের ইনপুট পড়ার সময় আসলে একটি language-এর সদস্যতা যাচাই করছে: এই HTTP অনুরোধ, এই JSON, এই কুপন কোড বৈধ কি না। সমস্যা হয় যখন এই language কোথাও স্পষ্টভাবে লেখা থাকে না, আর যাচাই ছড়িয়ে থাকে কোডের নানা if-else-এ। কিছু নিরাপত্তা-গবেষক (প্রায়ই “LangSec” নামে পরিচিত একটি ধারা) যুক্তি দেন যে অনেক দুর্বলতার মূলে এটাই: প্রোগ্রাম ইনপুটের এমন অংশ নিয়ে কাজ শুরু করে, যা আসলে ভাষার বাইরে। তাঁদের পরামর্শ হলো ইনপুটের grammar আগে স্পষ্ট করা, সম্ভব হলে সরল শ্রেণিতে (regular বা context-free) রাখা, আর ইনপুট পুরোটা একটি parser দিয়ে যাচাই করে তবেই ব্যবহার করা। §৩-এর বাজারঘর কমান্ড-recogniser এর ছোট উদাহরণ: অবৈধ কমান্ড ভেতরের কোনো হিসাবে পৌঁছায়ই না।
শ্রেণি বেছে নেওয়াও নিরাপত্তার বিষয়। Regular language-এর সদস্যতা যাচাই সবসময় দ্রুত; context-free-এও polynomial সময়ে। কিন্তু ইনপুটের ভাষা যদি এমন হয়, যার সদস্যতা যাচাই করতে সীমাহীন হিসাব লাগতে পারে, তবে আক্রমণকারী ভারী ইনপুট দিয়ে সার্ভার আটকে দিতে পারেন। Backreference-যুক্ত regex যে আসলে regular নয়, আর কিছু regex engine-এ খারাপ ইনপুটে সময় বিস্ফোরিত হতে পারে, তা পর্ব ৭৪-এ আসবে।
AI-এর যুগে grammar নতুন জায়গা পেয়েছে। বড় ভাষা মডেলকে প্রায়ই নির্দিষ্ট আকারের উত্তর দিতে বলা হয়, যেমন বৈধ JSON. কিছু টুল মডেলের প্রতিটি পরবর্তী token বাছার সময় একটি grammar দিয়ে সীমা টানে, যাতে শুধু grammar-সম্মত token-ই বেছে নেওয়া যায়; একে সাধারণত constrained বা grammar-guided decoding বলা হয়। এতে আউটপুটের syntax নিশ্চিত হয়, যদিও বিষয়বস্তু ঠিক কি না, তা আলাদা প্রশ্ন।
দুই ক্ষেত্রেই এই পর্বের মূল ধারণা কাজ করছে: সসীম নিয়মে একটি language-এর সংজ্ঞা, আর সেই সংজ্ঞা ধরে যন্ত্রের সিদ্ধান্ত।
মূল উপলব্ধি: ইনপুটের grammar স্পষ্ট করলে নিরাপত্তা বাড়ে আর যাচাই সহজ হয়; আর AI-এর আউটপুটকে grammar দিয়ে বাঁধলে তার গঠন নিশ্চিত হয়, অর্থ নয়।
অনুশীলন
আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।
-
প্রমাণ: G: S → aSb | ε. প্রমাণ করুন L(G) = {aⁿbⁿ : n ≥ 0}.
- (⊆, শক্তিশালী দাবি) S থেকে পাওয়া প্রতিটি sentential form হয় aⁿSbⁿ, নয় aⁿbⁿ (কোনো n ≥ 0). ভিত্তি: S = a0Sb0. ধাপ: aⁿSbⁿ-এ একমাত্র variable S; S → aSb দেয় an+1Sbn+1, S → ε দেয় aⁿbⁿ; aⁿbⁿ-এ কোনো variable নেই, আর ধাপ নেই। — derivation-এর দৈর্ঘ্যে induction
- তাই প্রতিটি terminal বাক্য aⁿbⁿ আকারের। — শুধু দ্বিতীয় রূপে variable নেই
- (⊇) যেকোনো n-এর জন্য S → aSb n বার, তারপর S → ε: S ⇒* aⁿSbⁿ ⇒ aⁿbⁿ. ∎ — সরাসরি নির্মাণ
লক্ষ করুন, (⊆)-এর জন্য শুধু বাক্য নয়, সব sentential form-এর আকার নিয়ে দাবি করতে হয়েছে; induction-কে “শক্তিশালী” করা প্রায়ই grammar প্রমাণের চাবি।
-
গণনা: (ক) Σ = {a, b, c}-এ দৈর্ঘ্য 4 পর্যন্ত (ε-সহ) কয়টি string? (খ) দৈর্ঘ্য 8-এর কয়টি balanced বন্ধনী-string? (গ) Grammar A (E → E + E | E * E | ( E ) | a)-এ a + a * a + a-এর কয়টি parse tree? (ঘ) L = {a, aa} হলে |L2| কত?
(ক) 1 + 3 + 9 + 27 + 81 = 121 = (35 − 1)/2. (খ) Catalan C4 = 14, §৬-এর recurrence থেকে c4 = c0c3 + c1c2 + c2c1 + c3c0 = 5 + 2 + 2 + 5. (গ) তিনটি অপারেটর, আর A-তে সব অপারেটর সমান, তাই C3 = 5. (ঘ) L2 = {aa, aaa, aaaa}: চারটি জোড়ার মধ্যে a · aa আর aa · a একই, তাই 3. সব সংখ্যা scratch-এ গুনে মেলানো।
-
আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে। তারপর যেকোনো Python-এ চালিয়ে মেলান।
L = {"a", "ab"} M = {"b", ""} LM = {x + y for x in L for y in M} print(sorted(LM), len(LM))['a', 'ab', 'abb'] 3চারটি জোড়া: a + b = ab, a + "" = a, ab + b = abb, ab + "" = ab. সেট বলে ab একবারই থাকে, তাই |LM| = 3 < |L| · |M| = 4. খালি string "" এখানে ε-এর কাজ করে, আর M-এ ε থাকায় L ⊆ LM. (CPython 3.14-এ চালিয়ে মেলানো।)
-
প্রমাণের ভুল ধরুন: দাবি: “S → aS | Sb | ε grammar-টি unambiguous. কারণ প্রতিটি নিয়ম ঠিক একটি অক্ষর যোগ করে, তাই প্রতিটি string-এর derivation-এর দৈর্ঘ্য নির্দিষ্ট, আর একই দৈর্ঘ্যের দুটি derivation একই গাছ দেয়। ∎” ভুল কোথায়? একটি পাল্টা-উদাহরণ আর একটি সংশোধিত grammar দিন।
- ভুল ধাপ: “একই দৈর্ঘ্যের derivation একই গাছ দেয়” কোনো যুক্তি ছাড়া বলা হয়েছে, আর তা মিথ্যা। দৈর্ঘ্য সমান হলেও কোন ক্রমে কোন নিয়ম খাটানো হলো, তাতে গাছ বদলায়।
- পাল্টা-উদাহরণ: ab: S ⇒ aS ⇒ aSb ⇒ ab (root-এ S → aS), আর S ⇒ Sb ⇒ aSb ⇒ ab (root-এ S → Sb). দুটি আলাদা গাছ, দুটিই leftmost. Scratch-এ গুনে ab-এর 2টি আর aabb-এর 6টি গাছ পাওয়া গেছে।
- সংশোধন: language a*b*; আগে সব a, পরে সব b: S → aS | T, T → bT | ε. এখন প্রতিটি ধাপে পছন্দ একটিই, তাই গাছও একটি।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রম“কোনো grammar তৈরি করে না এমন language আছে”, এই প্রমাণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Variable-এর নাম V1, V2, … ধরে প্রতিটি grammar-কে একটি সসীম alphabet-এর ওপর সসীম লেখায় সংকেত করুন
- আলাদা grammar আলাদা লেখা পায়, তাই grammar-এর সেট countable
- প্রতিটি grammar একটি language দেয়, তাই grammar-যুক্ত language-এর সেট countable
- Cantor-এর উপপাদ্যে সব language-এর সেট P(T*) uncountable
- Uncountable সেট countable সেটের সমান নয়, তাই কোনো language-এর grammar নেই
Σ = {0, 1, 2} হলে দৈর্ঘ্য 2 পর্যন্ত (empty string ε-সহ) Σ-এর ওপর কয়টি string আছে?
- 9
- 12
- 13
- 27
খালি language ∅-এর Kleene star ∅* কী?
- ∅
- {ε}
- Σ*
- সংজ্ঞায়িত নয়
Grammar S → aSb | ε সম্পর্কে কোন কথাটি সঠিক?
- এটি type 3 grammar, আর language regular
- এটি context-free grammar, আর তার language regular নয়
- এটি শুধু type 1 grammar
- এর language সব জোড় দৈর্ঘ্যের string
Grammar E → E + E | a-তে string a + a + a + a-এর কয়টি আলাদা parse tree আছে?
- 1
- 3
- 5
- 6
নিচের কোন language context-free নয়?
- {aⁿbⁿ : n ≥ 0}
- সব balanced বন্ধনী-string
- a*b*
- {aⁿbⁿcⁿ : n ≥ 1}
“{a, b}-এর ওপর কিছু language-এর কোনো grammar নেই”, এর মূল কারণ কী?
- Grammar countable, কিন্তু language uncountable
- অসীম language-এর grammar হয় না
- Type 0 grammar-এর নিয়ম খুব সীমিত
- Σ* uncountable
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Language: Σ*-এর যেকোনো subset; |Σn| = kn; LM, Ln, L*; ∅* = {ε}, L(M ∪ N) = LM ∪ LN, কিন্তু ∩-এ শুধু ⊆.
- Grammar: G = (V, T, S, P), L(G) = {w ∈ T* : S ⇒* w}; BNF হলো context-free grammar লেখার রীতি, recursive descent তার সরল parser.
- গাছ: context-free grammar-এ parse tree আর leftmost derivation এক-এক মেলে; দুটি গাছ মানে ambiguous; E/T/F স্তর precedence দেয়।
- প্রমাণ: L(G) = L মানে soundness আর completeness; balanced বন্ধনীতে first-return ভাঙন, যা Catalan recurrence-ও দেয়।
- শ্রেণি: regular ⊊ context-free ⊊ context-sensitive ⊊ type 0; grammar countable, language uncountable, তাই বেশিরভাগ language grammar-হীন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭৪ · Finite automaton আর regular expression-এর পরিচয়পরবর্তী পর্বType 3 language-এর যন্ত্র আর pumping lemma.
- পর্ব ১২ · Recursive সংজ্ঞা আর structural inductionপুনরালোচনাΣ*, balanced বন্ধনী আর grammar-এর প্রমাণের হাতিয়ার।
- পর্ব ১৮ · Cardinality — countable, uncountable আর Cantor-এর diagonalপুনরালোচনাকেন বেশিরভাগ language-এর grammar বা প্রোগ্রাম নেই।
- ডিসক্রিট ম্যাথমেটিক্স · কোর্স হোম৮৩টি পর্বপুরো কোর্সের মানচিত্র আর ১৪টি মডিউল।
- Python প্রোগ্রামিং · পর্ব ৩০ · Regular expressionকোডে প্রয়োগপ্যাটার্ন দিয়ে লেখা খোঁজা আর যাচাই।
- DSA · পর্ব ১৩ · Stackপাশের বিষয়বন্ধনী মেলানো, expression হিসাব আর recursive descent-এর stack.