পাঠ ১০ · ৫৬-এর মধ্যে · মডিউল ২
Home / Courses / Formal Language & Automata Theory / Theory of Computation / রেগুলার এক্সপ্রেশন

রেগুলার এক্সপ্রেশন — ফরমাল ডেফিনিশন

Regular expressions — formal definition
৭ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • regex-এর নির্ভুল রিকার্সিভ সংজ্ঞা — বেস কেস ও ইনডাক্টিভ কেস
  • প্রতিটি regex কীভাবে একটি ভাষা $L(R)$ বর্ণনা করে, ধাপে ধাপে
  • regex AST নেস্টেড টাপল হিসেবে কীভাবে উপস্থাপন করা যায়
  • একটি সত্যিকারের set-based ভাষা জেনারেটর, matcher নয় — সরাসরি সংজ্ঞা অনুসরণ করে

১ · রিকার্সিভ, ফরমাল সংজ্ঞা

একটি রেগুলার এক্সপ্রেশনRegular Expressionভাষা বর্ণনার একটি রিকার্সিভ নোটেশন — L03-এর স্ট্রাকচারাল ইনডাকশনের ধাঁচে সংজ্ঞায়িত। সংজ্ঞায়িত হয় নিচের বেস কেস ও ইনডাক্টিভ কেস দিয়ে — L03-এর স্ট্রাকচারাল ইনডাকশনের একই প্যাটার্ন (একটি বেস কেস, তারপর ইতিমধ্যে-তৈরি অংশ থেকে বড় অংশ বানানোর নিয়ম)।

$\emptyset$
খালি ভাষা — কিছুই ম্যাচ করে না।
$\varepsilon$
শুধু খালি স্ট্রিং ম্যাচ করে।
প্রতিটি $a \in \Sigma$
শুধু একক-ক্যারেক্টার স্ট্রিং "a" ম্যাচ করে।
$(R \cup S)$
ইউনিয়ন/অল্টারনেশন — R অথবা S।
$(RS)$
কনক্যাটেনেশন — R-এর পর S।
$(R^*)$
ক্লিনি স্টার — R শূন্য বা তার বেশিবার।

এবং — রিকার্সিভ সংজ্ঞায় প্রমিত একটি গুরুত্বপূর্ণ ক্লোজিং শর্ত — এর বাইরে আর কিছুই regex নয়। এই শর্তটিই সংজ্ঞাকে সম্পূর্ণ ও নির্ভুল করে তোলে (base cases + inductive cases + "nothing else" — L03-এর যেকোনো রিকার্সিভ সংজ্ঞার স্ট্যান্ডার্ড কাঠামো)।

২ · regex-এর ভাষা $L(R)$

প্রতিটি regex $R$ একটি ভাষা $L(R)$ বর্ণনা করে — এটিও ঠিক একই স্ট্রাকচারাল প্যাটার্নে সংজ্ঞায়িত:

$$L(\emptyset) = \{\} \qquad L(\varepsilon) = \{\varepsilon\} \qquad L(a) = \{a\}$$

$$L(R \cup S) = L(R) \cup L(S) \qquad L(RS) = \{xy : x \in L(R), y \in L(S)\}$$

$$L(R^*) = \bigcup_{i=0}^{\infty} L(R)^i$$

শেষ সূত্রটি ($L(R^*)$) মানে: $L(R)$ থেকে শূন্য বা তার বেশি স্ট্রিং একের পর এক জোড়া লাগিয়ে যত স্ট্রিং বানানো যায়, সেই সবের সেট — $i=0$ কেসে শুধু ε (কিছুই না জোড়া লাগিয়ে)।

প্র্যাকটিক্যাল বনাম থিওরেটিক্যাল regex

../programming-languages-compilers/-এর M4/L17 আগেই একটি regex-AST-matcher বানিয়েছে প্র্যাকটিক্যাল লেক্সিং-এর জন্য — সেখানে লক্ষ্য ছিল "একটি স্ট্রিং কি ম্যাচ করে" দ্রুত যাচাই করা। এই পাঠের লক্ষ্য ভিন্ন — regex ঠিক কোন ভাষা বর্ণনা করে তার নির্ভুল গাণিতিক সংজ্ঞা দেওয়া, যা L11-এর Kleene's theorem-এর ভিত্তি হিসেবে কাজ করবে।

৩ · কোড: regex AST থেকে সরাসরি ভাষা জেনারেট করা

নিচে regex-কে নেস্টেড Python টাপল হিসেবে উপস্থাপন করা হয়েছে (যেমন ("sym", "a"), ("union", R, S)), এবং language_of ফাংশনটি Python-এর re মডিউল ব্যবহার না করে সরাসরি উপরের সংজ্ঞা অনুসরণ করে $L(R)$-এর একটি (দৈর্ঘ্য-সীমাবদ্ধ) সেট তৈরি করে — matcher নয়, genuine সেট কনস্ট্রাকশন। উদাহরণ regex: $(a \cup b)^* a$ — অর্থাৎ "'a'-তে শেষ হওয়া যেকোনো স্ট্রিং"।

Python
def language_of(node, max_length):
    """genuinely computes L(R) up to a length bound, via structural recursion on the regex AST
    (nested tuples) -- never uses Python's `re` module."""
    kind = node[0]

    if kind == "empty":
        return set()
    if kind == "eps":
        return {""}
    if kind == "sym":
        a = node[1]
        return {a} if len(a) <= max_length else set()
    if kind == "union":
        _, R, S = node
        return language_of(R, max_length) | language_of(S, max_length)
    if kind == "concat":
        _, R, S = node
        LR = language_of(R, max_length)
        LS = language_of(S, max_length)
        return {x + y for x in LR for y in LS if len(x) + len(y) <= max_length}
    if kind == "star":
        _, R = node
        LR = {w for w in language_of(R, max_length) if w != ""}
        result = {""}
        frontier = {""}
        while frontier:
            new = set()
            for s in frontier:
                for r in LR:
                    cand = s + r
                    if len(cand) <= max_length and cand not in result:
                        new.add(cand)
            result |= new
            frontier = new
        return result
    raise ValueError(f"unknown regex node: {node}")


# regex (a U b)* a  as nested tuples
regex = ("concat",
         ("star", ("union", ("sym", "a"), ("sym", "b"))),
         ("sym", "a"))

MAX_LEN = 3
lang = language_of(regex, MAX_LEN)
print(f"L((a∪b)*a) up to length {MAX_LEN}:", sorted(lang, key=lambda w: (len(w), w)))

# hand-verification: every generated string over {a,b} up to length 3 that ends in 'a'
from itertools import product
expected = set()
for length in range(0, MAX_LEN + 1):
    for combo in product("ab", repeat=length):
        s = "".join(combo)
        if s.endswith("a"):
            expected.add(s)

print("expected set (strings ending in 'a', length<=3):", sorted(expected, key=lambda w: (len(w), w)))
assert lang == expected
print("regex-generated ভাষা এবং হাতে-গোনা 'a'-এ শেষ হওয়া স্ট্রিং সেট হুবহু মিলেছে।")

# sanity checks on base cases
assert language_of(("empty",), 3) == set()
assert language_of(("eps",), 3) == {""}
assert language_of(("sym", "a"), 3) == {"a"}
assert language_of(("union", ("sym", "a"), ("sym", "b")), 3) == {"a", "b"}
print("বেস কেসগুলোও সঠিক।")

    
হাতে-যাচাই: $(a\cup b)^*a$ মানে "যেকোনো স্ট্রিং (a/b দিয়ে গঠিত), তারপর একটি 'a'"। দৈর্ঘ্য ১-এ শুধু "a" (খালি prefix + a)। দৈর্ঘ্য ২-এ prefix "a" বা "b" + "a" = "aa","ba"। দৈর্ঘ্য ৩-এ prefix "aa","ab","ba","bb" + "a" = "aaa","aba","baa","bba"। মোট ৭টি স্ট্রিং, দৈর্ঘ্য ≤৩ — কোড আউটপুট হুবহু এই সেট, এবং assert lang == expected পাস করে, যেখানে expected সেটটি সম্পূর্ণ ভিন্নভাবে (regex সংজ্ঞা ব্যবহার না করে, সরাসরি "শেষে 'a' আছে কি না" চেক করে) গণনা করা হয়েছে — একটি স্বাধীন ক্রস-চেক।
মূল কথা · Key takeaway

regex একটি রিকার্সিভ নোটেশন যার ভাষা $L(R)$ তার নিজস্ব রিকার্সিভ কাঠামো অনুসরণ করেই সংজ্ঞায়িত — এই precise সংজ্ঞাই L11-এর Kleene's theorem-এর ভিত্তি, যেখানে আমরা দেখব ঠিক এই একই রিকার্সিভ কাঠামো অনুসরণ করে একটি regex থেকে সরাসরি একটি সমতুল্য NFA বানানো যায়।

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

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

প্র ০১ $\emptyset$ ও $\varepsilon$ — এই দুটো regex কি একই জিনিস? পার্থক্য কী?

না, সম্পূর্ণ আলাদা। $L(\emptyset) = \{\}$ — কোনো স্ট্রিং-ই এই ভাষার অংশ নয়, খালি স্ট্রিংও না। $L(\varepsilon) = \{\varepsilon\}$ — ঠিক একটি স্ট্রিং, খালি স্ট্রিং, এই ভাষার অংশ। একটি "কিছুই ম্যাচ করে না" (empty set), অন্যটি "শুধু খালি স্ট্রিং ম্যাচ করে" (একটি নন-এম্পটি সেট, যার একমাত্র সদস্য খালি স্ট্রিং) — এই পার্থক্যটি অনেক প্রুফে গুরুত্বপূর্ণ (যেমন CFG-তে ε-প্রোডাকশন বনাম অনুৎপাদনশীল ভেরিয়েবল, L19)।

প্র ০২ $L(R^*)$-এর সূত্রে $i=0$ টার্মটি ($L(R)^0$) কী প্রতিনিধিত্ব করে?

$L(R)^0 = \{\varepsilon\}$ — অর্থাৎ $L(R)$ থেকে "শূন্যটি" স্ট্রিং জোড়া লাগানো, যার ফল সবসময় খালি স্ট্রিং। এটিই নিশ্চিত করে $\varepsilon$ সবসময় $L(R^*)$-এর সদস্য, R যাই হোক না কেন — এমনকি $R = \emptyset$ হলেও $L(\emptyset^*) = \{\varepsilon\}$ (খালি ভাষা থেকে শূন্যবার জোড়া লাগিয়ে শুধু ε পাওয়া যায়)।

প্র ০৩ কোড সেলের language_of ফাংশন কেন max_length প্যারামিটার নেয় — সরাসরি পুরো $L(R)$ কেন রিটার্ন করে না?

কারণ $R^*$ (ক্লিনি স্টার) প্রায়ই একটি অসীম ভাষা বর্ণনা করে (যেমন $(a\cup b)^*$-এ যেকোনো দৈর্ঘ্যের স্ট্রিং আছে) — একটি Python সেট কখনো অসীম উপাদান ধরে রাখতে পারে না। তাই language_of একটি দৈর্ঘ্য-সীমা পর্যন্ত সত্যিকারের, সম্পূর্ণ সঠিক (truncated) ভাষা জেনারেট করে — এটি একটি বাস্তবসম্মত সীমাবদ্ধতা, ভুল সংজ্ঞা নয়।

অনুশীলন

  1. চিন্তা করুন: regex $ab^*$ (অর্থাৎ ("concat", ("sym","a"), ("star", ("sym","b")))) -এর $L(R)$ কী হবে (শব্দে বর্ণনা করুন), এবং দৈর্ঘ্য ≤৩ পর্যন্ত কোন কোন স্ট্রিং এতে থাকবে?

    $L(ab^*) = \{a, ab, abb, abbb, \dots\}$ — একটি 'a', তারপর শূন্য বা তার বেশি 'b'। দৈর্ঘ্য ≤৩ পর্যন্ত: "a" (দৈর্ঘ্য ১), "ab" (দৈর্ঘ্য ২), "abb" (দৈর্ঘ্য ৩) — মোট ৩টি স্ট্রিং। লক্ষ্য করুন "b" বা "" এই ভাষায় নেই, কারণ প্রতিটি স্ট্রিং অবশ্যই একটি 'a' দিয়ে শুরু হতে হবে (কনক্যাটেনেশনের প্রথম অংশ)।

  2. পরীক্ষা করুন: কোড সেলে regex-কে ("concat", ("sym","a"), ("star", ("sym","b")))-এ পরিবর্তন করে (এবং expected-এর তুলনা লাইনটি বাদ দিয়ে) Run চেপে প্রিন্ট হওয়া সেটটি উপরের অনুশীলন ১-এর হাতে-গোনা উত্তরের সাথে মেলে কি না দেখুন।

    আউটপুট হবে {'a', 'ab', 'abb'} (দৈর্ঘ্য ≤৩ অনুযায়ী সাজানো) — ঠিক অনুশীলন ১-এর হাতে-গোনা উত্তরের সাথে মেলে, নিশ্চিত করে যে language_of কনক্যাটেনেশন ও স্টার উভয়ই সংজ্ঞা অনুযায়ী সঠিকভাবে হ্যান্ডল করছে।

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

আগের পাঠ
এপসিলন-NFA ও এপসিলন-ক্লোজার