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

Paradigm — imperative, functional, logic, OO: চিন্তার চার ফ্রেম

Programming Paradigms

Paradigm মানে style না — কোন জিনিস ভাষায় বলা যায়, কী নিষিদ্ধ, আর সেই নিষেধের বিনিময়ে কোন প্রমাণ ফ্রি; একই সমস্যা চার ফ্রেমে সমাধান করলে পার্থক্যটা হাতেই টের পাওয়া যায়।

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

  • paradigm-কে সংজ্ঞায়িত করতে পারবেন 'কোন abstraction first-class + কোন restriction কোন guarantee কেনে' — চারটা paradigm-কে এই ছকে ফেলে তুলনা করতে পারবেন
  • referential transparency থেকে equational reasoning, memoization আর parallelism — functional ভাবনার তিনটা বিনামূল্যের সুফল একই expression-এ দেখাতে পারবেন
  • closure আর object-এর duality প্রমাণ করে বলতে পারবেন কোন সমস্যায় কোনটা বেশি expressive
  • unification আর backtracking ধারণা দিয়ে Prolog query-র সম্পূর্ণ search-tree hand-trace করতে পারবেন
  • একটা সমস্যা (N-Queens) চার ফ্রেমে সমাধান করে বলতে পারবেন কোন ফ্রেম কোন bug-class অসম্ভব করে, কোনটা সহজ করে — আর generic constraint solver-এ logic-ফ্রেমকে reusable framework বানাতে পারবেন

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

আগে এটা বুঝি

এই module-এ আমরা ভাষার ভেতরটা খুলে দেখেছি — meaning (syntax-vs-semantics, formal-semantics), types, memory, runtime, evaluation order, abnormal control, concurrency, ভাষার মধ্যে সেতু। শেষ লেসনে ফিরে যাই সবচেয়ে বড় প্রশ্নে, যেটা প্রথম দিন থেকেই ঝুলে ছিল: program লেখা আসলে কী?

একই সমস্যা চারজন engineer চার রকম ভাবে লিখতে পারে:

Imperative : "ধাপে ধাপে কর — state বদলাও, loop ঘাও"      → C, assembly
Functional : "মান রূপান্তরের বর্ণনা দাও — কী হবে, কীভাবে না" → Haskell, ML
OO         : "কে কী জানে, কে কাকে বার্তা পাঠাবে"          → Smalltalk, Java
Logic      : "সত্য কী, শর্ত কী — খোঁজো engine নিজেই"      → Prolog, Datalog

এই চার রকম লেখাকে বলে চারটা paradigm — আর বেশিরভাগ বই এটাকে বোঝায় “style” বা “syntax-এর স্বাদ”। সেটা ভুল, আর এই module-এর পুরো যাত্রা কেন ভুল সেটা এখন বলতে পারবেন: syntax-vs-semantics লেসনের মূল শিক্ষাই হলো — surface form আর অর্থ আলাদা জিনিস। Paradigm surface form না; paradigm হলো ভাষার semantic commitment: কোন জিনিসগুলো first-class (variable-এর মতো রাখা-পাঠানো যায়), আর কোন জিনিসগুলো নিষিদ্ধ — কারণ প্রতিটা নিষেধ বিনিময়ে একটা guarantee কেনে।

আজকের প্রশ্ন তিনটা: (১) প্রতিটা paradigm কোন restriction নেয়, আর সেই restriction কী কী ফ্রি দেয়? (২) একই সমস্যা চার ফ্রেমে সমাধান করলে bug-এর চরিত্র কীভাবে বদলায় — কোন ফ্রেমে ভুল করা সবচেয়ে কঠিন? (৩) modern ভাষাগুলো (Rust, Python, Swift) কেন এক paradigm-এ থামে না?

মূল ধারণা

Paradigm-এর ছক — restriction বিনিময়ে guarantee

ParadigmFirst-class citizenমূল restrictionবিনিময়ে যা ফ্রি
Imperativecommand / state changeকোনো না — সব allowedMachine-এর সাথে সরাসরি mapping; performance predictable
Functionalfunction / valueMutation নিষিদ্ধ বা সীমিতReferential transparency → reasoning, memoization, parallelism
Object-orientedobject / messageState শুধু নিজের methods দিয়ে ছোঁও যায়Invariant interface-এর পেছনে সুরক্ষিত; subtype substitution
Logicrelation / factControl flow explicitly লেখা যায় নাSpecification-ই program; search engine চালায়

এই ছকের শেষ কলামটাই আসল। Restriction ছাড়া guarantee হয় না — আর এই trade-off চেনা মানে প্রতিটা নতুন ভাষা দেখেই বলে দেওয়া কোন promise-টা আপনাকে দিচ্ছে, কোন স্বাধীনতার বিনিময়ে।

Functional — transparency-এর তিন সুফল

Pure function-এর সংজ্ঞা: এক input → এক output, বাইরে কিছু দেখে না বদলায় না। একে বলে referential transparency — যেকোনো expression-কে তার মান দিয়ে replace করা যায়, অর্থ অক্ষত। এই একটা property থেকে তিনটা সুফল গাণিতিকভাবে ঝরে আসে:

১. Equational reasoning : f(f(x)) = f²(x) — সমীকরণ সামলে proof করা যায়;
                          compiler-ও constant folding/common-subexpression
                          এভাবেই safe করে
২. Memoization          : f(5) একবার কষলে cache করে রাখা নিরাপদ — কারণ
                          ফল কখনো বদলাবে না (evaluation-strategies লেসনের
                          thunk-memoization ঠিক এর উপরেই দাঁড়িয়ে)
৩. Parallelism for free : নির্ভরতা না থাকলে যেকোনো জায়গায় ভাগ করা যায় —
                          race condition-এর concept-ই নেই, কারণ shared
                          mutable state-ই নেই

তৃতীয়টা খেয়াল করুন — concurrency-models লেসনের ছকে shared-state column-এর সব রোগ (race, lock-order deadlock) immutable data-তে structural ভাবে অসম্ভব। Paradigm-সিদ্ধান্ত concurrency-সিদ্ধান্তকে আগেই ঠিক করে দেয়।

OO — encapsulation আর message-passing heritage

type-systems আর polymorphism লেসনে subtype polymorphism দেখেছেন — interface- এর পেছনে যেকোনো implementation ঢুকতে পারে। Paradigm-দৃষ্টিতে OO-র মূল দাবি আরেকটা: state লুকানো, access শুধু message/method দিয়ে। তাহলে invariant (“balance কখনো negative না”) এক জায়গায় রক্ষা করলেই সব জায়গায় রক্ষিত।

History-টা মনে রাখুন — Smalltalk-এ “method call” বলতে সত্যিই message পাঠানো: receiver decide করে respond করবে কি না (method missing হলেও crash না, doesNotUnderstand)। এই heritage থেকেই actor model — Erlang-এর actor আসলে Smalltalk-এর object-এর distributed রূপ, mailbox-সহ। concurrency-models লেসনের actor column আর আজকের OO ফ্রেম এক বংশের।

Logic — relations, functions না

Functions input→output; relations কয়েকটা জিনিসের মধ্যে সম্পর্ক — যেকোনো দিক থেকে query করা যায়। parent(alice, bob) দিলে জিজ্ঞেস করা যায় “alice-এর সন্তান কে?” এবং “bob-এর বাবা কে?” — একই fact, দুই দিক। Engine চালায় দুইটা যন্ত্র: unification (pattern match করে variable bind) আর backtracking (choice point ব্যর্থ হলে পেছনে গিয়ে পরের বিকল্প)। বিস্তারিত trace আজকের example section-এ। Kowalski-র famous সূত্র মনে রাখুন: Algorithm = Logic + Control — কী সত্য (logic) আর কীভাবে খোঁজা হবে (control) আলাদা করা যায়; paradigm-টার পুরো দাবি এই separation-এ।

Multi-paradigm reality

Python-এর comprehension functional, class imperative-OO, generator lazy (evaluation-strategies!) — সব মেশানো। Rust: ownership imperative, trait+ iterator functional, pattern matching ML-ঘরানা, actor/channel library-তে। Deliberate design: কোনো এক restriction-ই সব সমস্যায় optimal না — ভালো ভাষা আপনাকে সমস্যা অনুযায়ী ফ্রেম বদলাতে দেয়, আর ভালো engineer জানে কখন বদলাতে হবে। আজকের experiment ঠিক সেই বদলানোর ব্যায়াম।

ভেতরে কী ঘটছে

Closure ↔ object duality

functional আর OO ফ্রেমের মধ্যে একটা গোপন সেতু আছে — closures-and-environments লেসনের জ্ঞান এখানে কাজে লাগবে:

# একই জিনিস, দুই পোশাকে: state + behavior-এর bundle

def make_counter():                 # closure-সংস্করণ
    count = 0                       # captured environment-এ বাস
    def step():
        nonlocal count
        count += 1
        return count
    return step                     # behavior + hidden state একসাথে

class Counter:                      # object-সংস্করণ
    def __init__(self):
        self.count = 0              # instance field-এ বাস
    def step(self):
        self.count += 1
        return self.count

দুটোই “state + সেই state-এর একগুচ্ছ operation”। পার্থক্য শুধু interface: closure একটা operation দেয়, object একগুচ্ছ দেয়; object-এর state-কে multiple methods dispatch করে select করে, closure-এ environment-এর lexical scope-ই selector। SICP-র বিখ্যাত মন্তব্যটা মনে আছে হয়তো: “closures are poor man’s objects, objects are poor man’s closures.” এই duality বোঝা মানে functional/OO-র মধ্যের দেয়ালটা marketing-এর বুঝতে পারা — নিচে দুজনের machine-ই এক: heap record + fields + code pointers (runtime-systems লেসনের frame/object layout)।

সব paradigm একই silicon-এ নামে

এই module-এর সবচেয়ে গুরুত্বপূর্ণ unifying সত্য — memory-models লেসনের প্রথম ছবিটা ফেরত আনুন: C, Go, JVM, CPython — চারটাই একই virtual pages, একই registers, একই instructions। Paradigm বাস করে source level-এ, আর compiler/runtime-এর কাজ সেই উঁচু ভাষাকে একই নিচু ভাষায় নামানো:

FP-র map/filter   → loop + index math (inlined)
OO-র dispatch     → vtable lookup: load ptr → load slot → indirect call
Logic-র search    → explicit stack + continuation (WAM-স্টাইল registers)
Laziness          → heap Thunk + force check (evaluation-strategies build!)

Prolog-এর backtracking যখন “automagic” লাগে, মনে রাখুন — Warren Abstract Machine ঠিক আমাদের constraint-solver-এর মতোই একটা explicit stack machine, শুধু ৩৫ বছরের optimization জমা। Magic কোথাও নেই; শুধু কারো হাতে-লেখা machinery আপনার চোখের আড়ালে।

Pattern matching — সব ফ্রেমে ঢুকে পড়া ভাবনা

ML/Haskell-এর pattern matching, Rust-এর match, Python-এর match/case (3.10+) — সবাই ভেতরে একই যন্ত্রে নামে: patterns-কে decision tree-তে compile করে, common tests factor করে। Unification-এর সঙ্গে সম্পর্কটাও এখানেই — pattern match হলো one-way unification (pattern-এর variable bind হয়, ডেটার structure যাচাই হয়), Prolog-এর টা two-way। Type-systems লেসনের sum-type/ exhaustiveness discussion এখানে এসে মেলে: pattern matching sum type-এর স্বাভাবিক consumption grammar।

উদাহরণ

Prolog mini-tour — একটা query-র সম্পূর্ণ search tree

% family.pl — facts আর একটা rule
parent(alice, bob).
parent(bob,   carol).
parent(bob,   dave).

grandparent(X, Z) :- parent(X, Y), parent(Y, Z).

?- grandparent(Who, carol).

Query-টা মানে: কার grandchild carol? Engine চলে unification + depth-first backtracking দিয়ে:

goal: grandparent(Who, carol)
 └─ rule unfold: parent(X, Y) ∧ parent(Y, carol)     [X,Y unbound]
     │
     ├─ fact ১: parent(alice, bob)  → X=alice, Y=bob ✓
     │    └─ sub-goal: parent(bob, carol)?
     │         └─ fact ২ match ✓ → Who = alice   ● প্রথম উত্তর
     ├─ fact ২: parent(bob, carol)  → X=bob, Y=carol ✓
     │    └─ sub-goal: parent(carol, carol)? ✗ কোনো fact নেই
     │         → FAIL → backtrack (X,Y unbind → আগের choice point)
     └─ fact ৩: parent(bob, dave)   → X=bob, Y=dave ✓
          └─ sub-goal: parent(dave, carol)? ✗ → FAIL → backtrack
               আর choice নেই → মোট উত্তর: Who = alice

তিনটা observation — paradigm-এর পুরো স্বাদ এখানেই:

  1. Program-টাই specification: আমরা লিখেছি “grandparent মানে কী”, খোঁজার কৌশল এক শব্দও নেই — control পুরোটা engine-এর (Kowalski)।
  2. Unification = bidirectional binding: parent(X, Y) fact থেকে X/Y পেয়েছে, parent(Y, carol) সেই Y যাচাই করেছে — variable শুধু ভরা হয় না, মেলানো হয়।
  3. Backtracking automatic: ব্যর্থ শাখা থেকে পেছনে গিয়ে পরের বিকল্প — ঠিক যেন runtime-ই আমাদের আজকের build-এর backtrack() চালাচ্ছে।

বাস্তব জগতে এই ফ্রেম টয় না: Datalog (Prolog-এর finite subset) চলে static analysis-এ (Soufflé pointer analysis), network policy verification-এ, database query engines-এ (Datomic), security policy languages-তে — যেখানে সমস্যাটা স্বাভাবিকভাবেই “relation + query”, function না।

ছবিতে

চার ফ্রেম, এক মানচিত্র

                 "কী বলা হয়" ◄──────────────► "কীভাবে চলে"

spec-heavy ◄─ LOGIC ──────────────────── FUNCTIONAL ─► transformation
             (facts+rules,                (pure values,
              engine searches)             composition)
                    │                           │
                    │  দুজনেই state-কে সন্দেহ করে │
                    ▼                           ▼
             OBJECT-ORIENTED ───────────── IMPERATIVE ► command
             (state encapsulated,          (state explicit,
              messages between)             control flow হাতে)

                 "কে state দেখে" অক্ষটাও পড়ুন — উপরের দুই ফ্রেম
                 বলে "যতটা পারো কম", নিচের দুই ফ্রেম বলে "ঠিক ঠাকুরে রাখো"
problem: "৩ সন্তানের মধ্যে বৈধ বার্তা-ক্রম বের করো"

LOGIC lens      : বৈধতার rules লিখো → সব ক্রম enumerate হয়ে যাবে
FUNCTIONAL lens : state → state transition function compose করো
OO lens         : Protocol object — প্রতি participant এক actor
IMPERATIVE lens : nested loop + flags + manual stack
                  (সবচেয়ে দ্রুত, সবচেয়ে ঝুঁকিপূর্ণ)
একই সমস্যা চার জানালা দিয়ে — প্রতিটা জানালা কিছু detail লুকায়, কিছু ফুটিয়ে তোলে। Engineer-এর দক্ষতা কোন জানালা বেছে নেওয়া না; problem-এর কোন দিকটা (search? transformation? state-machine? protocol?) মূল সেটা চিনে সেই জানালা আগে খোলা।

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

EXPERIMENT

N-Queens ৪×৪ — এক সমস্যা, চার ফ্রেম

Linux/macOS, Python 3.8+· ২০ মিনিট

চার implementation, একই contract: 4×4 board-এ ৪ queen এমনভাবে যেন কেউ কাউকে attack না করে।

১. Imperative — explicit stack, হাতে backtracking:

def queens_imperative(n=4):
    solutions = []
    board = [-1] * n            # board[r] = চলমান trial column
    row = 0
    while 0 <= row < n:
        found = False
        start = board[row] + 1
        board[row] = -1
        for col in range(start, n):
            ok = True
            for prev in range(row):
                if board[prev] == col or abs(board[prev] - col) == row - prev:
                    ok = False
                    break
            if ok:
                board[row] = col
                found = True
                break
        if found:
            row += 1
        else:
            row -= 1
        if row == n:
            solutions.append(board.copy())
            row -= 1            # শেষ row আবার retry — পরের solution
    return solutions

২. Functional — spec হিসেবে permutation + predicate:

from itertools import permutations

def queens_functional(n=4):
    def safe(cols):
        pairs = ((r, k) for r in range(n) for k in range(n) if r < k)
        return all(abs(cols[r] - cols[k]) not in (0, abs(r - k))
                   for r, k in pairs)
    return [list(p) for p in filter(safe, permutations(range(n)))]

৩. OO — Board নিজের invariant নিজে রক্ষা করে:

class QueensBoard:
    def __init__(self, n=4):
        self._n = n
        self._cols = []                     # private state

    def can_place(self, col):
        r = len(self._cols)
        return all(c != col and abs(c - col) != r - pr
                   for pr, c in enumerate(self._cols))

    def solutions(self):
        out = []
        def walk():
            if len(self._cols) == self._n:
                out.append(list(self._cols))
                return
            for col in range(self._n):
                if self.can_place(col):
                    self._cols.append(col)  # invariant: can_place-এর ভেতরেই
                    walk()                  # বাইরে থেকে state ছোঁয়া অসম্ভব
                    self._cols.pop()
        walk()
        return out

৪. Logic/search — constraints declare, engine solve করে:

def queens_search(n=4):
    def extend(cols):
        if len(cols) == n:
            return [cols]
        return [sol
                for col in range(n)
                if all(c != col and abs(c - col) != len(cols) - r
                       for r, c in enumerate(cols))
                for sol in extend(cols + [col])]
    return extend([])

Runner আর measurement:

import time

IMPLS = {
    "imperative": queens_imperative,
    "functional": queens_functional,
    "oo":         lambda n=4: QueensBoard(n).solutions(),
    "search":     queens_search,
}

for name, fn in IMPLS.items():
    t0 = time.perf_counter()
    reps = 500
    for _ in range(reps):
        result = fn()
    dt = (time.perf_counter() - t0) / reps
    print(f"{name:<11} {len(result)} solutions {dt*1e6:8.1f} µs/run")

print(sorted(map(tuple, IMPLS["functional"]())))
print(sorted(map(tuple, IMPLS["imperative"]())))

Typical output (এই লেখকের মেশিনে; timing আপনারটায় ভিন্ন হবে):

imperative   2 solutions     47.0 µs/run
functional   2 solutions     68.7 µs/run
oo           2 solutions     88.5 µs/run
search       2 solutions     72.6 µs/run
[(1, 3, 0, 2), (2, 0, 3, 1)]
[(1, 3, 0, 2), (2, 0, 3, 1)]

Bug-resistance discussion — আসল ফল:

  • Imperative: সবচেয়ে দ্রুত, সবচেয়ে ঝুঁকিতে — board[row] = -1 restore, backtrack-এ start calculation, boundary conditions: তিনটা আলাদা জায়গায় হাতে সিঙ্ক রাখতে হয়। আমার নিজের প্রথম draft-এই backtrack-এ পুরনো column retry ভুল ছিল — এই ফ্রেমে এমন bug স্বাভাবিক।
  • Functional: ভুলের জায়গা এক — safe predicate। Spec সঠিক হলে enumeration ভুল হতে পারে না (engine-এর বিশ্বাসযোগ্য)। দাম: সব permutation generate — বড় n-এ অকল্পনীয়।
  • OO: invariant (can_place-এর বাইরে placement অসম্ভব) encapsulation-এ বন্দি — ভুল করতে হলে class-এর ভেতরে ঢুকতে হবে। কিন্তু traversal logic তবু হাতে।
  • Search/logic: formulation ছোট, generalizes (আজকের build দেখুন) — ভুল হলে সেটা constraint-এর অর্থের ভুল, সহজে unit-testable।

যা প্রমাণিত: ফল identical, কিন্তু bug-এর ঠিকানা paradigm-ভেদে সরে — এটাই paradigm নির্বাচনের প্রকৃত criterion: আপনার সমস্যার কোন দিকটা ভুল-করা সবচেয়ে সহজ, সেই দিকটা কোন ফ্রেম ভাষায় অসম্ভব বানায়?

এটা কী প্রমাণ করে

ফল এক (২টা solution), কিন্তু চার ফ্রেমে ভুল-করার সম্ভাবনা ভিন্ন জায়গায় বসে — imperative-এ off-by-one/diagonal-arithmetic, functional-এ spec-এ ভুল (এক জায়গায়), OO-তে state-invariant রক্ষা, logic/search-এ constraint formulation; আর কোডের আকার-আকৃতিই বলে দেয় কোন সমস্যা কোন ফ্রেমের।

নিজে বানান

BUILD IT

Generic constraint solver — logic-ফ্রেমকে framework বানানো

Python · ●●●○○
  1. solve(names, domains, constraints) API design করুন — constraint = (scope, predicate)
  2. Backtracking core লিখুন — প্রতি assignment-এর পরে শুধু affected scope-এর consistency check
  3. Partial assignment মেনে চলা consistent() লিখুন — যে constraint-এর সব variable assigned, শুধু সেটা যাচাই
  4. N-Queens instantiate করুন — শুধু pairwise constraints দিয়ে, কোনো search code না
  5. একই framework-এ graph coloring instantiate করে reuse প্রমাণ করুন

লক্ষ্য: experiment-এর queens_search-কে generalize করা — search machinery একবার লিখুন, সমস্যা প্রতিটা শুধু facts দিয়ে describe হোক। এটাই logic paradigm-এর industrial রূপ (Datalog/SAT/CP solver-দের কঙ্কাল)।

# solver.py — generic backtracking constraint solver


def solve(names, domains, constraints):
    """
    names       : variable-দের ক্রম — এই ক্রমেই assignment হবে
    domains     : {name: legal values-এর list}
    constraints : [(scope, predicate)] — predicate(scoped_dict) -> bool
    ফেরত        : সব solution-এর generator (each: পূর্ণ assignment dict)
    """
    cons = [(frozenset(scope), pred) for scope, pred in constraints]

    def consistent(assign):
        """partial assignment-এ যত constraint fully-covered, সব check"""
        for scope, pred in cons:
            if scope <= assign.keys():
                if not pred({k: assign[k] for k in scope}):
                    return False
        return True

    def backtrack(i, assign):
        if i == len(names):
            yield dict(assign)
            return
        name = names[i]
        for value in domains[name]:
            assign[name] = value
            if consistent(assign):
                yield from backtrack(i + 1, assign)
        assign.pop(name, None)          # parent frame পরিষ্কার রাখো

    yield from backtrack(0, {})


# ── Instantiation ১: N-Queens — শুধু constraints, কোনো search কোড না ──

def nqueens(n):
    names = [f"Q{r}" for r in range(n)]           # Qr = row r-এর queen-column
    domains = {q: list(range(n)) for q in names}
    constraints = []
    for r1 in range(n):
        for r2 in range(r1 + 1, n):
            a, b = f"Q{r1}", f"Q{r2}"
            d = r2 - r1
            constraints.append(
                ((a, b),
                 lambda s, a=a, b=b, d=d:
                     abs(s[a] - s[b]) not in (0, d)))   # এক কলাম/কর্ণে না
    return solve(names, domains, constraints)


# ── Instantiation ২: Graph coloring — একই solver, ভিন্ন facts ──

def coloring(edges, palette):
    nodes = sorted({x for e in edges for x in e})
    domains = {v: list(palette) for v in nodes}
    constraints = [((u, v), lambda s, u=u, v=v: s[u] != s[v])
                   for u, v in edges]
    return solve(nodes, domains, constraints)


if __name__ == "__main__":
    sols = list(nqueens(4))
    print(f"N-Queens 4x4: {len(sols)} solution")
    for s in sols:
        print("   ", [s[f"Q{r}"] for r in range(4)])

    europe = [("DE", "FR"), ("DE", "PL"), ("FR", "ES"),
              ("ES", "PT"), ("PL", "UA"), ("AT", "DE")]
    first = next(coloring(europe, ["red", "green", "blue"]))
    print("graph coloring:", {v: first[v] for v in sorted(first)})

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

N-Queens 4x4: 2 solution
    [1, 3, 0, 2]
    [2, 0, 3, 1]
graph coloring: {'AT': 'red', 'DE': 'green', 'ES': 'red', 'FR': 'blue',
                 'PL': 'red', 'PT': 'green', 'UA': 'green'}

যা ঘটল: search machinery (backtrack + consistency) একবার লেখা; N-Queens আর graph coloring দুটোই শুধু facts — variables, domains, pairwise rules। Experiment-এর চার ফ্রেমের মধ্যে logic ফ্রেমটাই একমাত্র যার মূল্য reuse-এ ফোটে: নতুন constraint problem মানে নতুন ৫ লাইন description, ০ লাইন search। (lambda-গুলোর a=a trick খেয়াল করুন — late-binding capture bug থেকে বাঁচার classic Python idiom; closures লেসনের সরাসরি প্রয়োগ।)

নিজে বাড়ান:

  1. MRV heuristic যোগ করুন — প্রতি ধাপে সবচেয়ে ছোট domain-এর variable আগে assign; 8-Queens-এ node-count কত কমে মাপুন (count instrumentation দিন)।
  2. Forward checking যোগ করুন — assignment-এর পরে neighbor-দের domain থেকে illegal values prune; কোন সমস্যায় লাভ বেশি?
  3. Constraint-এ weight/arithmetic যোগ করুন — optimization সমস্যায় (সব solution না, best solution): branch-and-bound-এর দরজা খুলবে।
  4. Solver-কে generator রেখে সব solutions lazy consume করুন — evaluation-strategies লেসনের demand-driven ভাবনাটা এখানে এসে মেলে: next(solve(...)) একটাই solution চাইলে পুরো space ঘোরার দরকার নেই।

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

চার ফ্রেম production-এ যেখানে বসে

SQL — পৃথিবীর সবচেয়ে বড় declarative victory। আপনি লেখেন “কোন data চাই” (relation!), planner ঠিক করে “কীভাবে আনব” (control)। Query optimizer-এর অস্তিত্বই সম্ভব কারণ SQL logic-আর-control আলাদা করে — Kowalski-র থিসিসের trillion-dollar বাস্তবায়ন।

Datalog পুনর্জাগরণ। Static analysis (Soufflé — pointer analysis, security audit tools), network configuration verification (AWS-এর Zelkova policy checks), database (Datomic), knowledge graphs — যেখানে সমস্যা relation-shaped, সেখানেই logic ফ্রেম ফিরে আসছে।

Excel — বিশ্বের বৃহত্তম functional program। প্রতিটা cell এক pure formula, dependency graph-এ demand-driven recalculation — mutation নেই, event loop নেই, race নেই। কোটি user-এর হাতে functional thinking, নাম না জানিয়েই।

Erlang/Elixir আর OTP। Actor + supervision — telecom-এ জন্ম, chat/fintech- এ রাজত্ব। OO-র message-passing heritage-এর সবচেয়ে committed বংশধর।

Rust — সচেতন multi-paradigm-এর textbook। Ownership (imperative discipline), traits+iterators (functional), pattern matching (ML), channels (CSP library), zero-cost abstractions (systems imperative) — প্রতিটা ফ্রেম থেকে সেরাটা, restriction-গুলো type-system-এ জোর করে।

Airline crew scheduling, chip placement, exam timetabling। Constraint programming-এর ঘরানা — আজকের build-এর industrial ভাইয়েরা; সবার মূল একই: facts describe, engine search।

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

“Paradigm মানে coding style — একই কাজ যে কোনো paradigm-এ একই রকম লেখা যায়।”

Style হলো surface; paradigm হলো semantic commitment — কী first-class, কী নিষিদ্ধ, কী guarantee। Haskell-এ unrestricted mutation নিষিদ্ধ মানে আপনার program-এর সম্পর্কে কিছু প্রমাণ করা যায় যা Python-এ কখনোই না (equational reasoning)। উল্টোটাও সত্য: C-তে memory layout control guarantee আছে যা Haskell দেয় না। ভাষা বদল মানে promise-sheet বদল, font বদল না।

“Functional programming মানে production-এ mutation সম্পূর্ণ নিষিদ্ধ।”

Practical FP mutation নিয়ন্ত্রণ করে, নির্মূল না। Haskell-এর ST monad local mutable refs দেয় pure interface-এর পেছনে; Clojure-র transients; Erlang- এ per-process state আসলে mutable — শুধু সীমানা immutable। নীতিটা: mutation যেখানে দরকার, সেখানেই — কিন্তু ছোট, সীমান্ত-চিহ্নিত, আর type system-এ documented। “Zero mutation” purity হলো teaching ideal, engineering target না।

“OO মানে inheritance hierarchy — class ছাড়া OO না।”

Smalltalk — OO-র জনক-ভাষা — single inheritance, মূল দর্শন message-passing আর encapsulation। Modern consensus আরও দূরে: composition-over-inheritance, interfaces/protocols (Go-র structural typing, Rust-এর traits — inheritance নেই-ই), data-oriented designs। OO-র অপরিহার্য core দুটো: state লুকানো + behavior-এর সাথে বাঁধা, আর polymorphic dispatch। Hierarchy একটা (বর্তমানে সন্দেহভাজন) tool মাত্র।

“Logic programming academic toy — industry-তে Prolog লেখে কেউ না।”

Direct Prolog কম, কিন্তু logic-ফ্রেমের বংশধররা সর্বত্র: SQL (relation + optimizer), Datalog-ভিত্তিক static analyzers (Soufflé, CodeQL-এর ভিত), AWS policy verification (Zelkova), Datomic/TerminusDB, SAT/CP solvers — chip EDA থেকে crew scheduling। আর regex engine থেকে build system dependency resolution — backtracking আপনার প্রতিদিনের tooling-এই চলে, নাম না ধরে।

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

1

Referential transparency-র সংজ্ঞা দিন, আর এ থেকে ঝরে আসা তিনটা সুফলের প্রতিটার এক-লাইন justification লিখুন।

স্মরণ

সংজ্ঞা: যেকোনো expression তার মান দিয়ে replace করা যায় প্রোগ্রামের অর্থ অক্ষত রেখে (কোনো hidden state/effect না)। সুফল: (১) Equational reasoning — replace-property মানেই algebraic law দিয়ে refactor/proof; (২) Memoization — এক input সবসময় এক output, তাই caching semantically safe (thunk-মemoization); (৩) Free parallelism — ফল অন্য কারো উপর নির্ভরশীল না হলে evaluation order মুক্ত, তাই যেকোনো সমান্তরাল ভাগ সঠিক।

2

total = 0; for x in items: total += price(x) — এই imperative loop-কে functional ফ্রেমে rewrite করুন, আর বলুন নতুন রূপটা কেন memoizable/composable কিন্তু পুরনোটা না।

প্রয়োগ

total = sum(map(price, items)) (বা sum(price(x) for x in items))। নতুন রূপটা expression — items fixed থাকলে ফল constant, তাই cache-safe; আর composition-এর জন্য খোলা: sum(map(price, filter(in_stock, items))) — intermediate state-এর কোনো bookkeeping নেই। Loop-সংস্করণে total mutable cell — কোন ধাপে কী হলো তা history-dependent, তাই একই “call” ভিন্ন context- এ ভিন্ন মান দিতে পারে: memoization আর equational replacement দুটোই unsafe।

3

“Closure আর object একই জিনিসের দুই পোশাক” — hood-এর counter উদাহরণ ধরে ব্যাখ্যা করুন। কোন সমস্যায় closure-form বেশি expressive, কোনটায় object-form?

যুক্তি

দুটোই heap-record + captured/instance state + associated code — state ছাড়া access নেই, access ছাড়া state অর্থহীন। Closure-form জেতে যখন এক operation আর lexical scoping যথেষ্ট (callbacks, strategies, memoizers — Python-এর decorator, JS-এর module pattern): কম ceremony, state structurally invisible। Object-form জেতে যখন একগুচ্ছ operation একই state শেয়ার করে + identity দরকার (comparison, reflection, subclassing): multiple dispatch entry, interface conformance, tooling visibility। মূল প্রশ্ন: operation-সংখ্যা আর identity-দরকার দেখে পোশাক বাছুন।

4

Problem: বিমানের crew rostering — ২০০ crew, ৯০ দিনের schedule, union rules (সর্বোচ্চ ঘণ্টা, বিশ্রাম-ব্যবধান), qualification matrix, cost minimization। কোন paradigm(-combination) বেছে নেবেন এবং কেন? Imperative-only approach-এর বিপদ কী?

ডিজাইন

Core: constraint/optimization ফ্রেম (আজকের solver-এর industrial রূপ — CP/ MIP solver) — rules হলো constraints, cost objective, engine search (branch- and-bound/LP relaxation)। কারণ: সমস্যাটা relation+rule-shaped, আর rules প্রায়ই বদলায় (union negotiation) — declarative description মানে rule-change মানেই data-change, search-code touch না। সহায়ক স্তর: data pipeline functional (transform rosters immutably), reporting OO/imperative যেখানে সস্তা। Imperative-only বিপদ: search strategy আর business rules একই কোডে গাঁথা হয়ে যায় — প্রতিটা rule-tweak এখন heuristic-code surgery; correctness reasoning (completeness, optimality gap) অসম্ভব; আর এত বড় space-এ naive hand-search performance-এও ডুববে।

5

concurrency-models লেসনের তুলনা-ছকে আমরা দেখেছিলাম shared-state threads-এর রোগ race/deadlock। Immutable-first paradigm এই রোগগুলোর কোনটা structural ভাবে অসম্ভব করে, কোনটা শুধু কঠিন করে? Persistent data structure-এর ভূমিকা কী?

যুক্তি

Structural অসম্ভব: lost update আর data race — কোনো thread অন্যের পড়া ডেটা mutate-ই করতে পারে না, তাই interleaving যা-ই হোক ফল deterministic। Lock-order deadlock-ও মূলত অদৃশ্য (lock-ই দরকার নেই read-path-এ)। কঠিন হয়ে যায়, অসম্ভব হয় না: logical staleness bugs (পুরনো snapshot-এ সিদ্ধান্ত), আর coordination bugs (কে কখন নতুন version publish করবে — CAS/retry loops)। Persistent data structures (structurally sharing, update-এ নতুন version) এখানে bridge: mutation- স্টাইল ergonomics + immutability guarantees — Clojure/Haskell-এর ভিত; functional- ফ্রেমকে concurrent workload-এ practical বানায় ঠিক এই খরচ-model-এ (O(log n) copy)।

এরপর কী

এরপর কী — module-এর সমাপ্তি

পনেরো লেসনের যাত্রা শেষ হলো। ফেরত দেখুন পথটা: syntax আর semantics দিয়ে শুরু — লেখা আর অর্থ আলাদা জিনিস; formal-semantics-এ অর্থকে যন্ত্রের নিয়মে বাঁধলেন; type-systems, Hindley-Milner, polymorphism — ভুল আগে ধরার গণিত; memory-models, garbage-collection, reference-counting — ডেটার জীবন-মৃত্যুর চার রাজনীতি; runtime-systems — main-এর আগে-পরে যা চলে; closures — ফাংশন যখন environment নিয়ে হাঁটে; evaluation-strategies — গণনা কখন হয়; exception internals — control কীভাবে উল্টোপথে পালায়; concurrency-models — অনেক কাজের চার স্থাপত্য; FFI/ABI — ভাষাদের মধ্যের নীরব চুক্তি; আর আজ paradigms — চিন্তার চার ফ্রেম, প্রতিটার restriction আর guarantee।

এই পুরো জ্ঞানের সবচেয়ে সুন্দর পরীক্ষা এখন সামনে: compilers module — Level 5-এর এই ধারাবাহিকতারই পরবর্তী ধাপ। এতদিন আপনি ভাষার অর্থ পড়তে শিখেছেন; compilers শেখাবে সেই অর্থ silicon-এ অনুবাদ কীভাবে হয় — lexer যেভাবে text-কে token-এ ভাঙে, parser যেভাবে grammar-এ AST গড়ে (abstract-syntax-tree গ্লসারি টার্মটা এখনই চেনা লাগবে), type-checker আর optimizer কীভাবে আপনার চেনা semantic নিয়মগুলোকে ব্যবহার করে, আর codegen কীভাবে সব কিছু নামিয়ে আনে assembly module-এর সেই instruction-গুলোতে। আপনার নিজের হাতে লেখা evaluator, scheduler, unwinder, FFI wrapper — সবগুলোই তখন compiler-এর ভেতরের যন্ত্রাংশ হয়ে ফিরে আসবে।

তারপর Level 6 — algorithms আর তার বাইরে: যে ভাষা আপনি এখন ভেতর থেকে বোঝেন, তাতে কী লিখলে কত খরচ — সেই প্রশ্নের উত্তর এখন আর বিশ্বাসের বিষয় না, আপনার কাছে সেটা মাপার যন্ত্র। এই module-এর প্রতিটা build আপনার নিজের হাতের লেখা প্রমাণ।

আরও পড়ুন

  • Structure and Interpretation of Computer Programs — Harold Abelson, Gerald Jay Sussman, Julie Sussman · 'Languages as frameworks for thinking' — এই লেসনের দর্শনের মূল উৎস
  • Types and Programming Languages — Benjamin C. Pierce · Type system-কে paradigm-হিসেবে পড়ার formal ভিত্তি — type-systems লেসনের ধারাবাহিকতা
  • Algorithm = Logic + Control (CACM 1979) — Robert Kowalski · Logic programming-এর কেন্দ্রীয় থিসিস — যা লিখছ আর কীভাবে চলছে আলাদা
  • Programming Language Pragmatics — Programming Paradigms chapters — Michael L. Scott · চার paradigm-এর ভাষা-ভেদে comparative survey