পাঠ ০৮ · ৫৮-এর মধ্যে · মডিউল ২
Home / Courses / Concepts of Programming Languages & Compiler Design / ফাংশনাল প্যারাডাইম

ফাংশনাল প্রোগ্রামিং প্যারাডাইম

Functional programming paradigm
৯ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ফাংশনাল প্যারাডাইমের সংজ্ঞা এবং L06-এর ইম্পারেটিভ প্যারাডাইমের সাথে সরাসরি বৈসাদৃশ্য
  • পিওর ফাংশনের নির্দিষ্ট সংজ্ঞা এবং কেন এটি টেস্টেবল, ক্যাশযোগ্য ও প্যারালাল-সেফ
  • ইমিউটেবিলিটি এবং প্রথম-শ্রেণির (first-class)/হায়ার-অর্ডার ফাংশনের ধারণা
  • Python-এ একই সমস্যার ইম্পারেটিভ বনাম ফাংশনাল সমাধান পাশাপাশি দেখে সরাসরি তুলনা

১ · ফাংশনাল প্যারাডাইম — "কী" বনাম "কীভাবে"

ফাংশনাল প্রোগ্রামিংFunctional Programmingপ্রোগ্রামকে এক্সপ্রেশন ও পিওর ফাংশনের সমষ্টি হিসেবে গঠন করা একটি প্যারাডাইম, এক্সপ্লিসিট স্টেট-মিউটেশনের বদলে। প্রোগ্রামকে এক্সপ্রেশন ও পিওর ফাংশনের সমষ্টি হিসেবে তৈরি করে, এক্সপ্লিসিট স্টেট-মিউটেট-করা স্টেটমেন্টের বদলে। L06-এর ইম্পারেটিভ প্যারাডাইম বলে "কীভাবে, ধাপে ধাপে" গণনা করতে হবে (একটি মিউটেবল অ্যাকিউমুলেটর, একটি লুপ) — ফাংশনাল প্যারাডাইম বলে শুধু "কী" গণনা করতে হবে, প্রতিটি ধাপ একটি নতুন এক্সপ্রেশনের মান হিসেবে প্রকাশ করে, কোনো ভ্যারিয়েবলকে "বদলে" না দিয়ে।

২ · পিওর ফাংশন — কেন্দ্রীয় ধারণা

একটি পিওর ফাংশনPure Functionএকটি ফাংশন যার আউটপুট শুধু তার ইনপুটের ওপর নির্ভর করে, এবং যার কোনো সাইড এফেক্ট নেই। হলো এমন একটি ফাংশন যার আউটপুট শুধুমাত্র তার ইনপুটের ওপর নির্ভর করে — কোনো বাহ্যিক/শেয়ার্ড স্টেট পরিবর্তন করে না (কোনো সাইড এফেক্ট নেই), এবং নিজের আর্গুমেন্ট ছাড়া কোনো বাহ্যিক/শেয়ার্ড স্টেটের ওপর নির্ভরও করে না। এর মানে একই আর্গুমেন্ট দিয়ে একটি পিওর ফাংশন যতবারই, যখনই কল করা হোক না কেন, ঠিক একই ফলাফল দেবে। এটি সত্যিকারের একটি শক্তিশালী বৈশিষ্ট্য — পিওর ফাংশন সহজে টেস্ট করা যায় (শুধু ইনপুট-আউটপুট চেক করলেই হয়), মেমোয়াইজ/ক্যাশ করা যায় (একই ইনপুটে একই আউটপুট গ্যারান্টিড), এবং প্যারালালে নিরাপদে চালানো যায় (শেয়ার্ড স্টেট না থাকায় একে অপরকে ব্যাহত করতে পারে না)।

৩ · ইমিউটেবিলিটি ও হায়ার-অর্ডার ফাংশন

ইমিউটেবিলিটিImmutabilityডেটা একবার তৈরি হলে আর কখনো পরিবর্তন না করার নীতি — পরিবর্তন দরকার হলে নতুন ডেটা তৈরি হয়।
ডেটা তৈরির পর কখনো পরিবর্তন করা হয় না — একটি লিস্ট সরাসরি বদলানোর বদলে একটি পিওর ফাংশন কাঙ্ক্ষিত পরিবর্তনসহ একটি সম্পূর্ণ নতুন লিস্ট রিটার্ন করে, মূলটি অপরিবর্তিত থাকে।
হায়ার-অর্ডার ফাংশনHigher-Order Functionএকটি ফাংশন যা অন্য ফাংশনকে আর্গুমেন্ট হিসেবে নেয়, অথবা একটি ফাংশন রিটার্ন করে।
ফাংশন অন্য যেকোনো মানের মতোই আর্গুমেন্ট হিসেবে পাঠানো বা ফলাফল হিসেবে রিটার্ন করা যায় — map/filter/reduce-এর মতো শক্তিশালী কম্পোজিশন প্যাটার্ন সম্ভব করে, যা Python/JavaScript-এর মতো non-purely-functional ভাষাতেও প্রাত্যহিক ব্যবহারের বিষয়।
sum_to_n(3) কল হয় 3 + sum_to_n(2) কল করে 2 + sum_to_n(1) কল করে 1 + sum_to_n(0) কল করে base case: 0 → ভাঁজ খুলে 0+1+2+3=6 ফেরে
(ব্যাখ্যার সুবিধার জন্য ছোট n=3 উদাহরণ) কোনো ধাপেই কোনো ভ্যারিয়েবল মিউটেট হয়নি — প্রতিটি কল শুধু একটি নতুন মান রিটার্ন করে, যা পূর্ববর্তী কলের রিটার্ন এক্সপ্রেশনে ব্যবহৃত হয়।

৪ · কোড: L06-এর সামেশন সমস্যা, এবার ফাংশনাল স্টাইলে

নিচের কোড L06-এর "1 থেকে N পর্যন্ত যোগফল" সমস্যার সমাধান করে, কিন্তু এবার কোনো মিউটেবল অ্যাকিউমুলেটর ছাড়াই — একটি বিশুদ্ধ রিকার্সিভ ফাংশন, যেখানে প্রতিটি কল শুধু একটি নতুন মান রিটার্ন করে। এরপর একটি হায়ার-অর্ডার ফাংশন compose(f, g) দেখানো হয়েছে, যা দুটি ফাংশন নিয়ে একটি তৃতীয় নতুন ফাংশন তৈরি করে।

Python
# --- অংশ ১: বিশুদ্ধ রিকার্সিভ সামেশন -- কোনো মিউটেবল অ্যাকিউমুলেটর ভ্যারিয়েবল নেই ---
def sum_to_n(n):
    if n == 0:
        return 0            # base case -- নতুন কোনো স্টেট মিউটেট হয় না
    return n + sum_to_n(n - 1)  # প্রতিটি কল কেবল একটি নতুন মান রিটার্ন করে

N = 10
functional_result = sum_to_n(N)
closed_form = N * (N + 1) // 2   # L06-এর একই ক্লোজড-ফর্ম সূত্র

print(f"ফাংশনাল sum_to_n({N})   = {functional_result}")
print(f"ক্লোজড-ফর্ম N*(N+1)/2      = {closed_form}")
print(f"L06-এর ইম্পারেটিভ ফলাফলের সাথে মিলে গেছে: {functional_result == closed_form}")

# --- অংশ ২: হায়ার-অর্ডার ফাংশন -- compose(f, g) দুটি ফাংশন থেকে একটি নতুন ফাংশন বানায় ---
def compose(f, g):
    return lambda x: f(g(x))   # f(g(x))-এর সমতুল্য একটি নতুন ফাংশন রিটার্ন করে

def add_one(x):
    return x + 1

def square(x):
    return x * x

composed = compose(square, add_one)   # composed(x) == square(add_one(x))
x = 4
manual = square(add_one(x))

print(f"\ncompose(square, add_one)({x}) = {composed(x)}")
print(f"হাতে ধাপে ধাপে square(add_one({x}))   = {manual}")
print(f"মিলে গেছে: {composed(x) == manual}")

    
sum_to_n-এ কোথাও কোনো total = total + i-এর মতো মিউটেশন নেই — প্রতিটি রিকার্সিভ কল কেবল একটি এক্সপ্রেশন (n + sum_to_n(n - 1)) মূল্যায়ন করে রিটার্ন করে, এবং সেই মানগুলো "কল স্ট্যাক" ভাঁজ খুলে ওপরের দিকে যোগ হতে থাকে। N = 10-এর জন্য ফলাফল 55, যা 10*11/2 = 55 ক্লোজড-ফর্ম সূত্রের সাথে হুবহু মেলে — L06-এর ইম্পারেটিভ লুপ একই N-এর জন্য একই সূত্র মেনে চলে বলে ফলাফল অভিন্ন থাকবে।
মূল কথা · Key takeaway

একই সমস্যার দুটি সম্পূর্ণ ভিন্ন সমাধান-শৈলী থাকতে পারে — L06-এর ইম্পারেটিভ সংস্করণ ধাপে ধাপে একটি ভ্যারিয়েবল বদলায়, L08-এর ফাংশনাল সংস্করণ কোনো ভ্যারিয়েবলই বদলায় না, শুধু নতুন মান রিটার্ন করে। দুটোই সঠিক ফলাফল দেয়, কিন্তু ফাংশনাল সংস্করণে কোনো "লুকানো স্টেট" নেই যা নিয়ে ভাবতে হবে — এটিই কেন পিওর ফাংশন টেস্ট করা ও যুক্তি করা তুলনামূলক সহজ।

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

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

প্র ০১ একটি ফাংশন যদি প্রতিবার কল হওয়ার সময় একটি গ্লোবাল লিস্টে একটি লগ-এন্ট্রি যোগ করে (কিন্তু নিজের রিটার্ন ভ্যালু শুধু আর্গুমেন্টের ওপরই নির্ভর করে), এটি কি তবু পিওর ফাংশন?

না — এটি পিওর ফাংশন নয়, যদিও রিটার্ন ভ্যালু শুধু আর্গুমেন্টের ওপর নির্ভর করছে। পিওর ফাংশনের সংজ্ঞায় দুটো শর্তই থাকতে হয়: (১) আউটপুট শুধু ইনপুটের ওপর নির্ভরশীল, এবং (২) কোনো সাইড এফেক্ট নেই। গ্লোবাল লিস্টে এন্ট্রি যোগ করা একটি সাইড এফেক্ট — এটি বাহ্যিক/শেয়ার্ড স্টেট পরিবর্তন করছে। এই ফাংশনটি দুইবার একই আর্গুমেন্টে কল করলে রিটার্ন ভ্যালু একই থাকবে ঠিকই, কিন্তু গ্লোবাল লিস্টের অবস্থা প্রতিবার ভিন্ন হবে — তাই এটি টেস্ট/ক্যাশ/প্যারালাইজ করার নিরাপদ গ্যারান্টি দেয় না।

প্র ০২ ইমিউটেবিলিটি (ডেটা কখনো বদলানো হয় না) মানে কি প্রোগ্রামে কখনোই কোনো "নতুন তথ্য" যোগ করা যায় না?

না, এটি একটি সাধারণ ভুল ধারণা। ইমিউটেবিলিটি মানে বিদ্যমান ডেটাকে "জায়গায় বসে" (in place) পরিবর্তন না করা — কিন্তু নতুন তথ্যসহ একটি সম্পূর্ণ নতুন ডেটা স্ট্রাকচার তৈরি করা সম্পূর্ণ স্বাভাবিক ও অনুমোদিত। যেমন compose(square, add_one) একটি সম্পূর্ণ নতুন ফাংশন অবজেক্ট তৈরি করে, বিদ্যমান কোনো ফাংশন বদলায় না। একইভাবে একটি লিস্টে "নতুন উপাদান যোগ করা"-কে ফাংশনালভাবে প্রকাশ করা হয় মূল লিস্ট + নতুন উপাদান নিয়ে একটি সম্পূর্ণ নতুন লিস্ট রিটার্ন করার মাধ্যমে, মূল লিস্ট অস্পৃশ্য রেখে।

প্র ০৩ উপরের কোডে compose(square, add_one) আর compose(add_one, square) কি একই ফলাফল দেবে x = 4-এর জন্য?

না, ভিন্ন ফলাফল দেবে — ফাংশন কম্পোজিশন সাধারণত কম্যুটেটিভ (ক্রম-অপরিবর্তনীয়) নয়। compose(square, add_one)(4) মানে square(add_one(4)) = square(5) = 25। কিন্তু compose(add_one, square)(4) মানে add_one(square(4)) = add_one(16) = 17 — সম্পূর্ণ ভিন্ন উত্তর। কোন ফাংশনটি আগে প্রয়োগ হচ্ছে (ভেতরের g) এবং কোনটি পরে (বাইরের f) তার ক্রম গুরুত্বপূর্ণ।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোডে N-এর মান 20 করে Run চাপুন — নতুন functional_result ও closed_form কত হবে, এবং দুটো কি এখনো মিলবে?

    N = 20-এর জন্য: closed_form = 20*21//2 = 210, এবং sum_to_n(20)-ও রিকার্সিভভাবে গণনা করে ঠিক 210 রিটার্ন করবে — দুটো মিলে যাবে (True)। যেকোনো ধনাত্মক পূর্ণসংখ্যা N-এর জন্যই এই মিল বজায় থাকবে, কারণ রিকার্সিভ ফাংশনটি প্রকৃতপক্ষেই সঠিক গাণিতিক সমষ্টি গণনা করে।

  2. চিন্তা করুন: sum_to_n(10)-এর বদলে যদি sum_to_n(100000) কল করা হতো, Python-এ কী সমস্যা হতে পারে যা L06-এর ইম্পারেটিভ (লুপ-ভিত্তিক) সংস্করণে হতো না?

    RecursionError: maximum recursion depth exceeded — Python-এর ডিফল্ট রিকার্শন-ডেপথ সীমা (সাধারণত প্রায় ১০০০) অতিক্রম করে যাবে, কারণ প্রতিটি রিকার্সিভ কল একটি নতুন স্ট্যাক ফ্রেম তৈরি করে যা রিটার্ন না হওয়া পর্যন্ত মেমরিতে থাকে। L06-এর লুপ-ভিত্তিক ইম্পারেটিভ সংস্করণে কোনো নতুন স্ট্যাক ফ্রেম তৈরি হয় না (একই ফাংশন কলে বারবার একটি ভ্যারিয়েবল মিউটেট হয়), তাই এই সীমাবদ্ধতা নেই — এটি রিকার্সিভ ফাংশনাল স্টাইলের একটি বাস্তব ট্রেড-অফ (M11/L50-এ রিকার্শন ইমপ্লিমেন্টেশনে আরও বিস্তারিত)।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ M2 প্রোগ্রামিং প্যারাডাইম মডিউলের বাকি পাঠগুলো দেখুন — লজিক প্রোগ্রামিং ও মাল্টি-প্যারাডাইম ভাষা।
  • আগের পাঠ L07 অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং প্যারাডাইম — মিউটেবল স্টেট সংগঠিত করার একটি ভিন্ন উপায়, ফাংশনাল স্টাইলের বিপরীত দর্শন।
  • পরের পাঠ L09 লজিক ও ডিক্লারেটিভ প্রোগ্রামিং প্যারাডাইম — "কী চাই" প্রকাশ করার আরেকটি ধরন, যেখানে ইঞ্জিন নিজেই সমাধান খুঁজে বের করে।
আগের পাঠ
অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং প্যারাডাইম