সরাসরি পর্বে যান
পর্ব ২৩ · ৫৯-এর মধ্যে · মডিউল ৪

functional programming — pure function, currying আর composition

Functional Programming in JavaScript — Pure Functions, Immutability, Higher-Order Functions, Currying, Composition & Reducers

Taught in Bangla, this lesson introduces functional programming in JavaScript: pure functions and referential transparency, immutability with const, freeze, spread and the to-methods, writing your own higher-order functions, partial application, currying, composition with compose and pipe, pure reducers for declarative transformations, and where FP helps or hurts in real code.

এই পর্বে যা শিখবেন

  • Functional programming কী, আর JavaScript-এ এর কোন অংশগুলো সবচেয়ে কাজের
  • Pure function, referential transparency আর side effect চেনা — আর side effect কোথায় রাখবেন
  • বাস্তব immutability: const, Object.freeze, spread, toSorted/with-এর সীমা ও শক্তি
  • নিজের higher-order function, partial application, currying আর fn.length-এর ফাঁদ
  • compose/pipe দিয়ে দামের পাইপলাইন, validation পাইপলাইন আর pure cart reducer বানানো

১ · Functional programming কী — state বদলানো নয়, function জোড়া

একটি অনলাইন দোকানের checkout পাতা ভাবুন। কার্টের দাম থেকে ছাড় বাদ যাবে, তারপর VAT যোগ হবে, তারপর পয়সা পর্যন্ত গোল করা হবে, শেষে বাংলায় টাকার ফরম্যাটে দেখানো হবে। এই কাজ দুইভাবে লেখা যায়।

প্রথম ধরন: একটি let price variable নিন, তারপর একটির পর একটি লাইনে সেটিকে বদলান — price = price * 0.9, তারপর price = price * 1.15, তারপর গোল করা। প্রতিটি লাইন আগের লাইনের রেখে যাওয়া অবস্থার ওপর নির্ভর করে। মাঝখানে অন্য কোনো কোড price ছুঁলে পুরো হিসাব ভুল। একে বলে imperative ধরন — কম্পিউটারকে ধাপে ধাপে নির্দেশ দেওয়া।

দ্বিতীয় ধরন: প্রতিটি ধাপকে একটি ছোট function বানান — applyDiscount, addVat, roundToPaisa. প্রত্যেকটি একটি সংখ্যা নেয়, নতুন একটি সংখ্যা ফেরত দেয়, আর কিছুই বদলায় না। তারপর তিনটিকে জুড়ে একটি নতুন function বানান: finalPrice. কোনো variable বদলায় না; ডেটা এক function থেকে আরেক function-এ বয়ে যায়। এটিই functional programming-এর মূল চিন্তা।

ঢাকার একটি garments কারখানার সেলাই লাইনের কথা ভাবুন। প্রতিটি টেবিলে একজন শ্রমিক একটিই কাজ করেন: একজন হাতা লাগান, পরের জন বোতাম, তার পরের জন লেবেল। প্রত্যেকে হাতে একটি জিনিস পান আর পরের জনকে একটি জিনিস দেন — কেউ অন্যের টেবিলে গিয়ে কিছু বদলান না। কোনো শার্টে ভুল হলে ঠিক কোন টেবিলে হয়েছে, সহজেই ধরা যায়; একটি টেবিল বদলে নতুন ধাপ যোগ করাও সহজ। Functional programming-এ প্রতিটি টেবিল একটি pure function, আর পুরো লাইনটি তাদের composition.

JavaScript কোনো “pure” functional ভাষা নয় — তবু কেন কাজে লাগে

Haskell-এর মতো কিছু ভাষায় সব function pure হতে বাধ্য, আর variable একবার বসালে আর বদলানো যায় না। JavaScript তেমন নয়: object বদলানো যায়, let আছে, লুপ আছে, class আছে (পর্ব ২৬)। কিন্তু JavaScript-এ একটি জিনিস প্রথম দিন থেকেই আছে, যা functional ধারণাগুলোকে বাস্তবে সহজ করে: first-class function. পর্ব ১৩-এ দেখেছেন, function একটি মান — variable-এ রাখা যায়, argument হিসেবে পাঠানো যায়, function থেকে ফেরত দেওয়া যায়। পর্ব ১৬-এর map, filter, reduce এরই ফল, আর পর্ব ২২-এর closure function-কে নিজের সাথে ডেটা বয়ে নিতে দেয়।

তাই JavaScript-এ functional programming মানে “সব কিছু function দিয়ে লিখতেই হবে” নয়। মানে হলো কয়েকটি শক্তিশালী অভ্যাস বেছে নেওয়া — যেখানে সেগুলো কোডকে সহজ করে:

🧪 Pure function

একই input → একই output, বাইরের কিছু বদলায় না। অংশ ২-এ।

🧊 Immutability

ডেটা বদলানোর বদলে নতুন কপি। অংশ ৩-এ।

🧰 Higher-order function আর currying

Function নেয় বা function ফেরত দেয়; argument এক এক করে। অংশ ৪ ও ৫-এ।

🔗 Composition

ছোট function জুড়ে বড় function — pipe, compose. অংশ ৬-এ।

মূল ধারণা

Imperative কোড প্রশ্ন করে “এখন state কী, এরপর কী বদলাব?” Functional কোড প্রশ্ন করে “এই input থেকে কোন output চাই, আর কোন ছোট রূপান্তরগুলো জুড়ে সেটা পাব?” দ্বিতীয় প্রশ্নের উত্তরে লেখা কোড পড়তে declarative — কী চাই, তার বর্ণনা।

২ · Pure function আর referential transparency

একটি function-কে pure function বলা হয়, যদি দুটি শর্ত মানে:

  1. একই input-এ সবসময় একই output. ফল শুধু argument-এর ওপর নির্ভর করে — বাইরের variable, সময়, এলোমেলো সংখ্যা বা নেটওয়ার্কের ওপর নয়।
  2. কোনো side effect নেই। মান ফেরত দেওয়া ছাড়া আর কিছু করে না: global variable বদলায় না, পাওয়া object বদলায় না, পাতায় কিছু লেখে না, সার্ভারে পাঠায় না।

(a, b) => a + b pure. Math.max pure. কিন্তু Math.random() pure নয় (প্রতিবার ভিন্ন ফল), Date.now() pure নয় (সময়ের ওপর নির্ভর), arr.push(x) pure নয় (মূল array বদলায়), আর console.log-ও কঠোর অর্থে pure নয় — এটি বাইরের জগতে (console-এ) কিছু লেখে। নিচের ডায়াগ্রামে একটি impure আর একটি pure function দুবার করে একই input দিয়ে ডেকে দেখুন:

1let total = 0;2const addImpure = (x) => (total += x);3const addPure = (sum, x) => sum + x;4addImpure(5);5addImpure(5);6addPure(0, 5);7addPure(0, 5);
প্রতিটি কলের পরে
global total
ফেরত মান
বাইরে কিছু বদলাল?
  1. দুটি function, একটি global variable. addImpure বাইরের total-এ যোগ করে আর সেটাই ফেরত দেয়। addPure যা দরকার সব argument হিসেবে পায়, আর শুধু যোগফল ফেরত দেয়।
  2. addImpure(5) — প্রথমবার। total ০ থেকে ৫ হলো, ফেরত ৫. দেখতে নিরীহ। কিন্তু function-টি একটি চিহ্ন রেখে গেল — বাইরের অবস্থা বদলে গেছে।
  3. addImpure(5) — দ্বিতীয়বার। একই input, কিন্তু এবার ফেরত ১০! কারণ ফল নির্ভর করছে আগের সব কলের ইতিহাসের ওপর। কোনো টেস্টে এই function-কে আলাদাভাবে পরীক্ষা করতে হলে আগে total ঠিক করে নিতে হবে; দুটি জায়গা থেকে একসাথে ডাকলে ফল অনুমান করা কঠিন।
  4. addPure(0, 5) — প্রথমবার। ফেরত ৫, আর total ছোঁয়াই হয়নি। ফলের পুরো কারণ argument-এর ভেতরেই লেখা।
  5. addPure(0, 5) — দ্বিতীয়বার। আবারও ৫ — হাজারবার ডাকলেও ৫. তাই addPure(0, 5) লেখার জায়গায় সরাসরি 5 বসিয়ে দিলেও প্রোগ্রামের আচরণ বদলাবে না। এই বৈশিষ্ট্যের নামই referential transparency.

শেষ ধাপের বৈশিষ্ট্যটির নাম referential transparency: একটি expression-কে তার ফল দিয়ে বদলে দিলে প্রোগ্রামের অর্থ বদলায় না। গণিতে এটি স্বাভাবিক — 2 + 3 যেখানে আছে সেখানে 5 লিখলে কিছুই বদলায় না। প্রোগ্রামে এটি একটি উপহার, কারণ তখন কোড পড়তে গিয়ে একটি function কল দেখে আপনি “এর ফল কী” ভাবলেই চলে, “এটি আর কী কী করে” ভাবতে হয় না।

Pure function কেন এত কাজের — তিনটি সুবিধা

সুবিধাকেন সম্ভববাস্তব উদাহরণ
পরীক্ষা করা সহজশুধু input দিন, output মেলান — কোনো setup, mock বা পরিষ্কার করার কাজ নেইexpect(addVat(100)).toBeCloseTo(115) (পর্ব ৪৮-এ Vitest)
Cache করা যায়একই input-এ একই ফল, তাই আগের ফল মনে রেখে দিলেই হয়পর্ব ২২-এর memoization; React-এর useMemo
ভাগ করে চালানো যায়কেউ কারও ডেটা বদলায় না, তাই ক্রম বা একসাথে চলায় কিছু যায় আসে নাWeb Worker-এ ভারী হিসাব (পর্ব ৪২), সার্ভারে MapReduce

Side effect ছাড়া প্রোগ্রাম হয় না — তাই রাখুন কিনারায়

একটি সত্য স্বীকার করা দরকার: শুধু pure function দিয়ে কোনো কাজের প্রোগ্রাম হয় না। পাতায় দেখানো, সার্ভারে অর্ডার পাঠানো, localStorage-এ রাখা — এগুলোই তো প্রোগ্রামের উদ্দেশ্য, আর সবই side effect. Functional চিন্তা side effect নিষেধ করে না; বলে side effect-কে আলাদা করে কিনারায় রাখুন.

পর্ব ২১-এর খরচের খাতায় ঠিক এটাই করেছিলেন: হিসাবের সব নিয়ম ছিল pure function-এ, আর DOM ছোঁয়ার কাজ ছিল একটি পাতলা স্তরে। এই নকশার নাম functional core, imperative shell. ভেতরের core সিদ্ধান্ত নেয় (“কার্টে নতুন মোট কত?”), বাইরের shell কাজ করে (“পাতায় সংখ্যাটা বসাও, সার্ভারে পাঠাও”)। ফল: বেশিরভাগ যুক্তি টেস্ট করা যায় কোনো ব্রাউজার ছাড়াই।

লুকানো impurity চিনুন। function total(cart) { cart.sort(…); return … } — return দেখে pure মনে হয়, কিন্তু sort কলকারীর array বদলে দিয়েছে (পর্ব ১৬)। new Date() বা Math.random() ভেতরে ডাকলেও function আর pure থাকে না। সমাধান: এমন মান argument হিসেবে নিন — isExpired(item, now) — তখন টেস্টে নিজের পছন্দের “এখন” পাঠাতে পারবেন।

৩ · Immutability বাস্তবে — const, freeze, spread আর to-পরিবার

Pure function-এর দ্বিতীয় শর্ত (কিছু বদলাবে না) মানতে হলে ডেটার সাথে আচরণ বদলাতে হয়: পুরোনো object বদলানোর বদলে বদলানো অংশসহ নতুন object বানাতে হয়। এই অভ্যাসের নাম immutability. JavaScript-এ এর চারটি স্তর আছে, আর প্রতিটির সীমা জানা জরুরি।

স্তর ১ — const immutability নয়

পর্ব ০৭-এ দেখেছেন: const শুধু নামটিকে একটি মানের সাথে বেঁধে রাখে, মানটিকে নয়। const cart = []; cart.push("চাল"); দিব্যি চলে — cart এখনো একই array-কে দেখাচ্ছে, শুধু array-টির ভেতরটা বদলেছে। একে বলে mutation. const আটকায় শুধু cart = […] লিখে নতুন মান বসানো।

স্তর ২ — Object.freeze, কিন্তু shallow

Object.freeze একটি object-এর property যোগ, মোছা বা বদলানো বন্ধ করে দেয়। দুটি জিনিস মনে রাখুন। এক, সাধারণ (sloppy) mode-এ বদলানোর চেষ্টা চুপচাপ উপেক্ষিত হয় — কোনো error নেই; শুধু strict mode-এ (module আর class-এর ভেতরে ডিফল্ট) TypeError আসে। দুই, freeze shallow: শুধু ওপরের তলার property আটকায়। ভেতরে আরেকটি object থাকলে সেটি দিব্যি বদলানো যায়, যদি না আপনি সেটিকেও আলাদা করে freeze করেন (একটি recursive “deep freeze” function দিয়ে)। অংশ ৫-এর কোডে দুটোই নিজে চালিয়ে দেখবেন।

স্তর ৩ — spread দিয়ে নতুন কপি

প্রতিদিনের সবচেয়ে কাজের কৌশল পর্ব ১৭-এর spread: { ...user, city: "রাজশাহী" } একটি নতুন object দেয় যার শুধু city বদলানো; [...cart, newItem] নতুন array দেয় যার শেষে নতুন পণ্য। এটিই immutable update. মনে রাখুন spread একটি shallow copy — ভেতরের object-গুলো পুরোনোটার সাথে ভাগাভাগি থাকে। এটি আসলে সুবিধা: না-বদলানো অংশ কপি না করে ভাগ করে নেওয়া (structural sharing) বড় ডেটাতেও আপডেট সস্তা রাখে। শুধু ভেতরের কিছু বদলাতে হলে সেই স্তরটিও spread করতে হয়:

const next = {
  ...state,
  user: { ...state.user, address: { ...state.user.address, city: "রাজশাহী" } },
};

তিন স্তর নিচের একটি field বদলাতে তিনবার spread — পথের প্রতিটি object নতুন, পথের বাইরের সব (যেমন state.cart) পুরোনো reference. ঠিক এই কারণে React বা Redux দ্রুত বুঝতে পারে কী বদলেছে: reference বদলেছে কি না, শুধু সেটুকু তুলনা করলেই হয়। কিন্তু গভীর ডেটায় এমন লেখা ক্লান্তিকর আর ভুলপ্রবণ — নিচে এর সমাধান।

স্তর ৪ — ES2023-এর “to-পরিবার” আর লাইব্রেরি

Array-এর পুরোনো method sort, reverse, splice আর arr[i] = v সবই mutation. ES2023 এদের non-mutating জোড়া দিয়েছে: toSorted, toReversed, toSpliced আর with(index, value). যেমন scores.with(2, 95) একটি নতুন array দেয় যার তৃতীয় ঘরে ৯৫, মূলটি অক্ষত। Functional কোডে এগুলোই প্রথম পছন্দ।

গভীর nested ডেটায় দুটি জনপ্রিয় লাইব্রেরি আছে। Immer আপনাকে একটি “খসড়া” (draft) বদলানোর মতো স্বাভাবিক কোড লিখতে দেয়, আর ভেতরে ভেতরে structural sharing দিয়ে একটি নতুন immutable object বানায় — Redux Toolkit এটিই ব্যবহার করে। Immutable.js (Facebook, ২০১৪) নিজস্ব persistent data structure (List, Map) দেয়। এগুলো npm package লাগে, তাই এখানে চালানো যায় না — শুধু চেহারা দেখুন:

import { produce } from "immer";   // npm install immer

const next = produce(state, (draft) => {
  draft.user.address.city = "রাজশাহী";   // দেখতে mutation, আসলে draft-এ
});
// প্রত্যাশিত আচরণ: state অক্ষত, next নতুন; পথের বাইরের অংশ ভাগাভাগি

৪ · নিজের higher-order function আর partial application

পর্ব ১৬-এ higher-order function ব্যবহার করেছেন — map-কে একটি callback দিয়েছেন। এবার নিজে বানানোর পালা। নিয়ম একটাই: function নিন, আর/অথবা function ফেরত দিন। সবচেয়ে কাজের নকশা হলো wrapper — একটি function নিয়ে তার চারপাশে বাড়তি আচরণ জুড়ে একটি নতুন function ফেরত দেওয়া, মূলটিকে না ছুঁয়ে।

const withLogging = (fn, label) => (...args) => {
  const result = fn(...args);
  console.log(`${label}(${args.join(", ")}) → ${result}`);
  return result;
};
const addVat = (p) => p * 1.15;
const loggedVat = withLogging(addVat, "addVat");
loggedVat(200);   // ছাপে: addVat(200) → 229.99999999999997  (floating-point, পর্ব ১০)

withLogging একটি function আর একটি নাম নেয়, আর একটি নতুন function ফেরত দেয়। নতুন function যেকোনো সংখ্যক argument নেয় (...args, পর্ব ১৩), মূল function ডাকে, ফলটা ছাপে, আর হুবহু ফেরত দেয়। addVat নিজে pure-ই রইল; logging-এর side effect শুধু wrapper-এ। পর্ব ২২-এর closure এখানে কাজ করছে — ফেরত দেওয়া function fn আর label মনে রেখেছে। ছাপা সংখ্যার শেষের ...৯৯৭ দেখে চমকাবেন না: 200 * 1.15 binary-তে হুবহু লেখা যায় না, তাই টাকার হিসাবে শেষে গোল করার ধাপ লাগে (অংশ ৬)।

একই নকশায় আরও অনেক কিছু বানানো যায়: once(fn) (একবারই চলবে), memoize(fn) (পর্ব ২২), debounce(fn, ms) (পর্ব ৩৬)। আর একটি খুব বাস্তব উদাহরণ — ব্যর্থ হলে আবার চেষ্টা করা retry wrapper. নিচে এর synchronous রূপ; নেটওয়ার্কের জন্য async রূপ আর অপেক্ষার কৌশল (exponential backoff) পর্ব ৩৬-এ:

const withRetry = (fn, times) => (...args) => {
  for (let i = 1; ; i++) {
    try { return fn(...args); }
    catch (e) { if (i >= times) throw e; }   // শেষ চেষ্টাতেও ব্যর্থ হলে error ছাড়ো
  }
};
const safeParse = withRetry(readSensor, 3);   // readSensor মাঝে মাঝে error ছোড়ে

Partial application — কিছু argument আগেই বসিয়ে রাখা

ধরুন একটি সাধারণ function আছে: const vat = (rate, price) => price * (1 + rate);. বাংলাদেশে বেশিরভাগ পণ্যে ১৫% VAT, তাই প্রতিবার vat(0.15, price) লেখা বিরক্তিকর। এর সমাধান partial application: কিছু argument আগেই বসিয়ে একটি নতুন, ছোট function বানানো। দুই উপায়:

const vat15 = (price) => vat(0.15, price);   // arrow wrapper — সবচেয়ে পরিষ্কার
const vat15b = vat.bind(null, 0.15);         // bind — প্রথম argument আটকে দেয়
vat15(200);  vat15b(200);                    // দুটোই প্রায় 230

Arrow wrapper পড়তে সহজ, যেকোনো অবস্থানের argument আটকাতে পারে, আর this নিয়ে কোনো ঝামেলা নেই। bind শুধু বাঁ দিক থেকে argument আটকায়, আর এর প্রথম argument আসলে this-এর মান (এখানে null, কারণ vat-এর this লাগে না)। bind, call, apply আর this-এর পুরো গল্প পরের পর্বে — পর্ব ২৪-এ। আধুনিক কোডে partial application-এর জন্য arrow wrapper-ই বেশি দেখবেন।

৫ · Currying — এক এক করে argument

Partial application-এর একটি নিয়মতান্ত্রিক রূপ হলো currying: কয়েকটি argument নেওয়া একটি function-কে এমন একটি function-এর শিকলে বদলানো, যার প্রতিটি একটি করে argument নেয়। add(1, 2, 3)-এর curried রূপ add(1)(2)(3). Arrow function-এ এটি লেখা খুবই সহজ — প্রতিটি => একটি স্তর:

const formatTaka = (locale) => (amount) =>
  new Intl.NumberFormat(locale, { style: "currency", currency: "BDT" }).format(amount);

const bnTaka = formatTaka("bn-BD");   // locale একবার ঠিক করা
bnTaka(1035);                         // "১,০৩৫.০০৳"
["en-US", "bn-BD"].map((loc) => formatTaka(loc)(500));

formatTaka("bn-BD") ফরম্যাট করে না — একটি নতুন function ফেরত দেয়, যে closure-এ "bn-BD" মনে রেখেছে। এরপর যতবার খুশি bnTaka(amount). Currying-এর আসল সুবিধা এখানেই: যে argument কম বদলায় সেটি আগে, যেটি বেশি বদলায় সেটি শেষে রাখলে, আগের অংশ বসিয়ে বিশেষায়িত function বানানো যায়, আর সেটি সরাসরি map-এ বা পাইপলাইনে পাঠানো যায়: amounts.map(bnTaka). (বাস্তব কোডে Intl.NumberFormat বস্তুটি একবার বানিয়ে রাখা দ্রুততর — Intl পর্ব ২০-এ।)

একটি সাধারণ curry(fn) — fn.length দিয়ে

প্রতিটি function হাতে curried লেখার বদলে একটি সাধারণ সাহায্যকারী বানানো যায়, যা যেকোনো function-কে curried করে দেয়। কৌশল: function-এর length property বলে সে কয়টি parameter ঘোষণা করেছে — একে বলে তার arity. যতক্ষণ যথেষ্ট argument জমা হয়নি, ততক্ষণ argument জমিয়ে নতুন function ফেরত দাও; যথেষ্ট হলে আসল function ডাকো। ধাপে ধাপে দেখুন:

1const curry = (fn) => function curried(...args) {2  return args.length >= fn.length3    ? fn(...args)4    : (...more) => curried(...args, ...more);5};6const add3 = (a, b, c) => a + b + c;7curry(add3)(1)(2)(3);
প্রতিটি কলে
এখনকার কল
args
args.length ≥ fn.length?
ফেরত
  1. curry(add3) — প্রস্তুতি। curry কিছু হিসাব করে না; শুধু curried নামের একটি function ফেরত দেয়, যে closure-এ fn (অর্থাৎ add3) মনে রেখেছে। add3.length হলো ৩ — তিনটি parameter.
  2. (1) — প্রথম argument. curried(1) চলে: args = [1]. ১ কি ৩-এর সমান বা বেশি? না। তাই একটি নতুন arrow function ফেরত আসে, যে closure-এ args = [1] ধরে রেখেছে।
  3. (2) — দ্বিতীয় argument. নতুন function more = [2] পেয়ে ডাকে curried(1, 2). এখন args.length ২ — এখনো ৩ নয়। আবার নতুন function, এবার [1, 2] মনে রেখে।
  4. (3) — যথেষ্ট হলো। ডাক যায় curried(1, 2, 3). ৩ ≥ ৩ — শর্ত সত্য, তাই এবার আসল add3(1, 2, 3) চলে।
  5. ফল ৬. একই curried একসাথে কয়েকটি argument-ও নেয়: c3(1, 2)(3) আর c3(1)(2, 3)-ও ৬. প্রতিটি মাঝের ধাপে একটি নতুন function আর একটি নতুন array তৈরি হয় — তাই খুব ঘন ঘন চলা কোডে currying-এর ছোট একটি খরচ আছে।

fn.length-এর তিনটি ফাঁদ। length গোনে শুধু প্রথম default মানওয়ালা বা rest parameter-এর আগের parameter. তাই ((a, b = 2) => a).length হলো ১, ((...xs) => xs).length হলো ০, আর function f(a, b = 1, c) {}-এর length ১. এমন function-কে এই curry দিলে সেটি আগেভাগে চলে যাবে (বা rest-এ প্রথম কলেই)। Wrapper-এর ভেতরে (...args) থাকলে wrapper-এর length-ও ০ — অংশ ৪-এর withLogging দিয়ে মোড়ানো function আর curry করা যাবে না। সমাধান: curry(fn, arity = fn.length) — দরকারে সংখ্যাটা নিজে দিন।

এবার নিজে চালিয়ে দেখুন — অংশ ২-এর pure/impure তুলনা, অংশ ৩-এর shallow freeze, আর এই অংশের curry ও length:

let total = 0;
const addImpure = (x) => (total += x);
const addPure = (sum, x) => sum + x;
console.log(addImpure(5), addImpure(5), addPure(0, 5), addPure(0, 5));

const cfg = Object.freeze({ vat: 0.15, shop: { name: "দোকান" } });
cfg.vat = 0.5;                    // চুপচাপ উপেক্ষিত (strict mode নয়)
cfg.shop.name = "নতুন দোকান";      // ভেতরের object freeze হয়নি
console.log(cfg.vat, cfg.shop.name, Object.isFrozen(cfg));

const curry = (fn) => function curried(...args) {
  return args.length >= fn.length
    ? fn(...args)
    : (...more) => curried(...args, ...more);
};
const add3 = (a, b, c) => a + b + c;
const c3 = curry(add3);
console.log(c3(1)(2)(3), c3(1, 2)(3), c3(1)(2, 3));
console.log(add3.length, ((a, b = 2) => a).length, ((...xs) => xs).length);

প্রথম লাইনে 5 10 5 5 — impure function একই input-এ ৫ আর ১০ দিল, pure function দুবারই ৫. দ্বিতীয় লাইনে 0.15 নতুন দোকান true — cfg.vat বদলায়নি (কোনো error-ও আসেনি, কারণ এই কোড strict mode-এ চলছে না), কিন্তু ভেতরের shop.name বদলে গেছে, যদিও Object.isFrozen(cfg) বলছে true! এটাই shallow freeze. তৃতীয় লাইনে 6 6 6 — argument যেভাবেই ভাগ করে দিন, তিনটি জমলেই add3 চলে। শেষ লাইনে 3 1 0 — default আর rest parameter length-কে কীভাবে কমিয়ে দেয়।

৬ · Composition — compose আর pipe

গণিতে দুটি function-এর composition লেখা হয় (f ∘ g)(x) = f(g(x)) — আগে g, তারপর তার ফলে f. প্রোগ্রামে function composition মানে ঠিক এটাই: কয়েকটি ছোট function জুড়ে একটি নতুন function, যেখানে প্রতিটির output পরেরটির input. হাতে লিখলে roundToPaisa(addVat(applyDiscount(1000))) — কাজ করে, কিন্তু পড়তে হয় ভেতর থেকে বাইরে, ডান থেকে বাঁয়ে; আর পাঁচটি ধাপ হলে বন্ধনীর জঙ্গল।

সমাধান দুটি ছোট সাহায্যকারী, দুটোই পর্ব ১৬-এর reduce দিয়ে এক লাইনে:

const pipe    = (...fns) => (x) => fns.reduce((v, f) => f(v), x);       // বাঁ থেকে ডানে
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);  // ডান থেকে বাঁয়ে

দুটোই একই কাজ করে, শুধু ক্রম উল্টো। compose(h, g, f)(x) মানে h(g(f(x))) — গণিতের লেখার ক্রম, আর Ramda বা Redux-এর compose এভাবে চলে। pipe(f, g, h)(x) মানে একই জিনিস, কিন্তু আপনি যে ক্রমে কাজ ঘটে সেই ক্রমেই লেখেন — বাঁ থেকে ডানে, কারখানার লাইনের মতো। বেশিরভাগ মানুষের কাছে pipe পড়া সহজ, তাই এই পর্বে আমরা pipe ব্যবহার করব। এখানে reduce-এর accumulator একটি মান, আর array-টি function-এর — reduce যে কত সাধারণ, এটি তার সুন্দর প্রমাণ।

1const pipe = (...fns) => (x) =>2  fns.reduce((v, f) => f(v), x);3const finalPrice = pipe(applyDiscount, addVat, roundToPaisa);4finalPrice(120);
reduce-এর প্রতিটি কল
fns
f (এখনকার function)
v আগে
v পরে = f(v)
  1. pipe কিছু চালায় না — function বানায়। pipe(applyDiscount, addVat, roundToPaisa) তিনটি function-কে fns array-তে জমিয়ে একটি নতুন function ফেরত দেয়। এখনো কোনো দাম হিসাব হয়নি; finalPrice শুধু একটি “রেসিপি”।
  2. finalPrice(120) — reduce শুরু। x = 120 reduce-এর initial value হয়, তাই প্রথম কলে v = 120, f = applyDiscount. ফল 120 × 0.9 = 108 — ১০% ছাড়।
  3. দ্বিতীয় কল — addVat. এবার v = 108, ফল 108 × 1.15. গণিতে ১২৪.২, কিন্তু JavaScript দেখায় 124.19999999999999 — পর্ব ১০-এর floating-point. টাকার হিসাবে ঠিক এই কারণেই শেষে গোল করার ধাপ রাখা হয়েছে।
  4. তৃতীয় কল — roundToPaisa. Math.round(v * 100) / 100 দুই দশমিক পর্যন্ত গোল করে: 124.2. প্রতিটি ধাপ আগের ধাপের output নিয়েছে, কেউ কারও variable ছোঁয়নি।
  5. শেষ accumulator-ই উত্তর। finalPrice(120) = 124.2. নতুন ধাপ (যেমন ডেলিভারি চার্জ) যোগ করতে শুধু pipe-এর তালিকায় আরেকটি function বসান; বাকি কিছুই বদলাতে হবে না। আর প্রতিটি ধাপ আলাদাভাবে টেস্ট করা যায়।

এবার পুরো ছবিটি চালিয়ে দেখুন — দামের পাইপলাইন, curried টাকা-ফরম্যাটার, আর একটি sign-up ফর্মের validation পাইপলাইন, যেখানে প্রতিটি নিয়ম একটি ছোট function:

const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const applyDiscount = (p) => p * 0.9;
const addVat = (p) => p * 1.15;
const roundToPaisa = (p) => Math.round(p * 100) / 100;
const finalPrice = pipe(applyDiscount, addVat, roundToPaisa);
console.log(finalPrice(1000), finalPrice(120), addVat(applyDiscount(120)));

const formatTaka = (locale) => (amount) =>
  new Intl.NumberFormat(locale, { style: "currency", currency: "BDT" }).format(amount);
console.log(formatTaka("bn-BD")(finalPrice(1000)));

const required = (field) => (form) => (form[field] ? null : `${field} দরকার`);
const minLen = (field, n) => (form) =>
  (form[field] ?? "").length >= n ? null : `${field} কমপক্ষে ${n} অক্ষর`;
const validate = (rules) => (form) => rules.map((rule) => rule(form)).filter(Boolean);
const checkSignup = validate([required("email"), minLen("password", 8)]);
console.log(checkSignup({ email: "", password: "abc" }));
console.log(checkSignup({ email: "a@b.com", password: "12345678" }));

প্রথম লাইনে 1035 124.2 124.19999999999999 — ১০০০ টাকায় ১০% ছাড় আর ১৫% VAT মিলিয়ে ১০৩৫; শেষ সংখ্যাটি গোল করার ধাপ ছাড়া কী হতো, তা দেখায়। দ্বিতীয় লাইনে ১,০৩৫.০০৳ — curried ফরম্যাটার বাংলা অঙ্ক আর টাকার চিহ্ন বসিয়েছে (চিহ্নের অবস্থান ব্রাউজারের ভাষা-তথ্যের ওপর নির্ভর করে)। তৃতীয় লাইনে [ 'email দরকার', 'password কমপক্ষে 8 অক্ষর' ] আর শেষ লাইনে [] — খালি array মানে কোনো ভুল নেই। Validation-এর নকশাটি খেয়াল করুন: প্রতিটি নিয়ম একটি curried function, যে আগে কনফিগারেশন (field, n) আর পরে ফর্ম নেয়; validate নিয়মের array নিয়ে একটি checker ফেরত দেয়। নতুন নিয়ম মানে array-তে একটি নতুন function — পর্ব ৩৯-এ আসল form-এ এটি লাগাবেন।

Point-free style — কখন সুন্দর, কখন ধাঁধা

উপরে pipe(applyDiscount, addVat, roundToPaisa)-এ কোথাও price নামটি লেখা নেই — ডেটার নাম না বলে শুধু function জুড়ে function বানানো হয়েছে। এই লেখার ধরনকে বলে point-free style (“point” মানে এখানে argument). ছোট, ভালো নামওয়ালা ধাপে এটি দারুণ পাঠযোগ্য। কিন্তু বাড়াবাড়ি করলে ধাঁধা হয়ে যায়:

// ধাঁধা: কী করে বোঝা কঠিন
const f = pipe(filter(prop("active")), map(pipe(prop("price"), multiply(1.15))), sum);
// পরিষ্কার: নাম আছে, ধাপ আছে
const activeTotalWithVat = (items) => items
  .filter((i) => i.active)
  .map((i) => addVat(i.price))
  .reduce((s, p) => s + p, 0);

প্রথম লাইনটি Ramda-র মতো লাইব্রেরির curried helper ধরে নিয়ে লেখা। নিয়মিত Ramda ব্যবহারকারীর কাছে এটি সংক্ষিপ্ত, কিন্তু বেশিরভাগ দলের কাছে পড়া কঠিন, error-এর stack trace-এ অর্থবহ নাম থাকে না, আর debugger-এ breakpoint বসানোর জায়গা নেই। নিয়ম: point-free ব্যবহার করুন যখন প্রতিটি ধাপের একটি পরিষ্কার নাম আছে; argument-এর নাম যখন পাঠককে সাহায্য করে, তখন সেটি লিখুন। আর পর্ব ১৬-এর map(parseInt) ফাঁদ মনে রাখুন — সরাসরি function পাঠানোও এক ধরনের point-free, আর সেটি বিপজ্জনক হতে পারে।

ভবিষ্যতের |> — TC39-এর pipeline operator প্রস্তাব

বহু বছর ধরে JavaScript-এ একটি pipeline operator আনার আলোচনা চলছে, যাতে pipe helper ছাড়াই বাঁ থেকে ডানে লেখা যায়। কয়েকটি প্রতিদ্বন্দ্বী নকশার মধ্যে ২০২১ সালে “Hack-style” নকশাটি Stage 2-এ ওঠে, আর আমাদের জানা মতে এই লেখার সময়ও সেখানেই আছে — অর্থাৎ এখনো মানদণ্ডের অংশ নয়, কোনো ব্রাউজার বা Node.js-এ সরাসরি চলে না, আর চেহারা বদলাতে পারে। এই নকশায় একটি বিশেষ “topic” চিহ্ন আগের ধাপের মানকে বোঝায়; চিহ্নটি কী হবে, তা নিয়ে আলোচনা এখনো চলছে। উদাহরণে % ধরে নিচ্ছি:

// প্রস্তাবিত ব্যাকরণ — এখন কোথাও চলে না (Stage 2)
const label = 1000
  |> applyDiscount(%)
  |> addVat(%)
  |> roundToPaisa(%)
  |> formatTaka("bn-BD")(%);

৭ · Pure reducer আর declarative ডেটা রূপান্তর

Functional চিন্তার সবচেয়ে প্রভাবশালী বাস্তব প্রয়োগ সম্ভবত reducer: একটি pure function, যে বর্তমান state আর একটি “action” (কী ঘটল তার বর্ণনা) নিয়ে নতুন state ফেরত দেয় — (state, action) => newState. নামটি এসেছে reduce থেকে, কারণ action-এর একটি তালিকাকে actions.reduce(reducer, initialState) দিয়ে চালালে শেষ state পাওয়া যায়। Redux আর React-এর useReducer এই চিন্তার ওপর দাঁড়িয়ে। একটি শপিং কার্টের reducer:

const cartReducer = (state, action) => {
  switch (action.type) {
    case "add":    return [...state, action.item];
    case "remove": return state.filter((i) => i.id !== action.id);
    case "qty":    return state.map((i) => i.id === action.id ? { ...i, qty: action.qty } : i);
    default:       return state;                  // অজানা action — state হুবহু
  }
};

প্রতিটি শাখা নতুন array ফেরত দেয় — spread, filter, map; কোথাও push, splice বা i.qty = … নেই। default শাখা গুরুত্বপূর্ণ: অজানা action-এ একই state ফেরত দিলে reference বদলায় না, আর UI বোঝে “কিছু বদলায়নি”। Reducer-এর ভেতরে নেটওয়ার্ক কল, Date.now() বা Math.random() নিষেধ — এগুলো action তৈরির সময় (shell-এ) করে মান action-এর ভেতরে পাঠান।

1const s0 = [];2const s1 = cartReducer(s0, { type: "add", item: চাল });3const s2 = cartReducer(s1, { type: "add", item: ডাল });4const s3 = cartReducer(s2, { type: "remove", id: 1 });5// = [a1, a2, a3].reduce(cartReducer, [])
State-এর ইতিহাস
action
কোন শাখা
নতুন state
পুরোনো state
  1. শুরুর state. খালি কার্ট []. Reducer নিজে কোনো state রাখে না — প্রতিবার state argument হিসেবে পায়।
  2. “add চাল”. add শাখা [...state, item] দিয়ে একটি নতুন array বানাল। s0 খালিই রইল — reducer সেটি ছোঁয়নি।
  3. “add ডাল”. আবার নতুন array s2. ভেতরের চাল-object টি s1 আর s2-এ একই reference — structural sharing, কপির খরচ নেই।
  4. “remove id 1”. filter চাল বাদ দিয়ে নতুন array দিল: s3 = [ডাল]. মোট ৮০ টাকা।
  5. পুরো ইতিহাস অক্ষত। প্রতিটি পুরোনো state এখনো আছে, বদলায়নি। তাই “undo” মানে শুধু আগের state-এ ফিরে যাওয়া (পর্ব ২১-এর undo stack), আর Redux DevTools-এর “time-travel debugging” মানে এই তালিকায় আগে-পিছে যাওয়া। একই action-তালিকা যতবার চালান, একই শেষ state — reducer pure বলেই।

Declarative ডেটা রূপান্তর — নামওয়ালা ছোট ধাপ

পর্ব ১৬-এ chaining দিয়ে পাইপলাইন বানিয়েছেন। Functional চিন্তা এক ধাপ এগোয়: প্রতিটি ধাপকে নাম দিন, যাতে পাইপলাইন একটি বাক্যের মতো পড়া যায়, আর প্রতিটি নিয়ম আলাদাভাবে টেস্ট ও পুনর্ব্যবহার করা যায়:

const isDelivered = (o) => o.status === "delivered";
const inDistrict  = (d) => (o) => o.district === d;     // curried predicate
const lineTotal   = (o) => o.price * o.qty;
const sum         = (a, b) => a + b;

const dhakaRevenue = orders
  .filter(isDelivered)
  .filter(inDistrict("ঢাকা"))
  .map(lineTotal)
  .reduce(sum, 0);

শেষ চার লাইন পড়ুন: “ডেলিভারি হওয়াগুলো, ঢাকার, প্রতিটির লাইন-মোট, যোগফল।” কীভাবে ঘুরতে হবে, কোথাও লেখা নেই — এটিই declarative. inDistrict("ঢাকা") একটি curried predicate: জেলার নাম বসিয়ে একটি নতুন function, যা সরাসরি filter-এ যায়। এখানে আমাদের নিজের ছোট function সরাসরি পাঠানো নিরাপদ, কারণ আমরা জানি এগুলো শুধু প্রথম argument দেখে।

৮ · Recursion, memoization, null সামলানো আর বাস্তবের FP

Recursion বনাম iteration — আর tail call-এর গল্প

Pure functional ভাষায় লুপ নেই, কারণ লুপ মানে একটি counter বদলানো। সেখানে পুনরাবৃত্তি হয় recursion দিয়ে — function নিজেকে ছোট সমস্যা দিয়ে ডাকে। যেমন const sumList = (xs) => xs.length === 0 ? 0 : xs[0] + sumList(xs.slice(1));. গাছের মতো nested ডেটা (folder-এর ভেতরে folder, মন্তব্যের ভেতরে উত্তর) ঘোরার জন্য recursion সত্যিই স্বাভাবিক আর সুন্দর।

কিন্তু JavaScript-এ একটি বাস্তব সীমা আছে। প্রতিটি কল call stack-এ একটি নতুন frame যোগ করে (পর্ব ০৫), আর stack-এর আকার সীমিত। কয়েক হাজার থেকে কয়েক দশ হাজার স্তর গভীরে (engine আর frame-এর আকারভেদে) Chrome, Edge আর Node.js দেয় RangeError: Maximum call stack size exceeded. Functional ভাষাগুলো এ সমস্যা এড়ায় tail call optimization দিয়ে: function-এর একেবারে শেষ কাজ যদি আরেকটি কল হয়, তবে পুরোনো frame ফেলে দিয়ে নতুন কল সেই জায়গা নেয় — stack বাড়ে না।

ES2015 মানদণ্ডে strict mode-এর জন্য এই “proper tail calls” বাধ্যতামূলক করা হয়েছিল। কিন্তু বাস্তবে শুধু Safari-র engine (JavaScriptCore) এটি চালু করেছে; V8 (Chrome, Edge, Node.js) আর Firefox করেনি — কারণ ভাবনার প্রশ্ন ৫-এ। তাই ব্যবহারিক নিয়ম: অল্প গভীরতার গাছ-ধাঁচের ডেটায় recursion; লম্বা তালিকায় reduce, for…of বা নিজের stack দিয়ে iteration. ১০ লাখ উপাদানের array-এ উপরের sumList ভেঙে যাবে (আর slice প্রতিবার কপি করে বলে ধীরও); reduce যাবে না।

Memoization — pure function-এর পুরস্কার

পর্ব ২২-এ closure দিয়ে memoize(fn) বানিয়েছেন: একটি Map-এ আগের input-এর ফল রেখে, আবার একই input এলে হিসাব না করে রাখা ফল ফেরত দেওয়া। এখানে শুধু মনে রাখুন কেন এটি নিরাপদ: memoization শুধু pure function-এ সঠিক. Impure function (যেমন অংশ ২-এর addImpure, বা সময়-নির্ভর কিছু) memoize করলে পুরোনো, ভুল ফল ফেরত আসবে। আর cache-এর key সাধারণত argument দিয়ে বানানো হয়, তাই object argument-এ reference বদলালেই cache কাজ করবে না — immutable update-এর সাথে এটি খুব ভালো মেলে, কারণ “নতুন reference মানেই নতুন ডেটা”।

null সামলানো — ছোট “হয়তো আছে” ধাপ

পাইপলাইনের মাঝে কোনো ধাপ null বা undefined দিলে পরের ধাপ ভেঙে যায়: TypeError: Cannot read properties of undefined. আধুনিক JavaScript-এ সবচেয়ে সহজ উত্তর পর্ব ০৮-এর ?. আর ??: user?.address?.city ?? "অজানা" — যেকোনো স্তরে মান না থাকলে পুরো expression নিরাপদে undefined হয়, আর শেষে একটি ডিফল্ট বসে। Functional ধাঁচে আরেকটি পরিচিত কৌশল হলো মানটিকে একটি ছোট array-তে রাখা — খালি array মানে “নেই”: [user].filter((u) => u != null).map(getCity). মান না থাকলে map কিছুই করে না, কোনো error নেই।

এই চিন্তা — একটি “বাক্স” যার ভেতরে মান থাকতেও পারে, না-ও পারে, আর বাক্স না খুলেই তার ওপর function চালানো — functional ভাষায় Maybe বা Option নামে পরিচিত। যে বাক্সে map চালানো যায় তাকে তত্ত্বে Functor বলে, আর যেখানে ধাপগুলো নিজেরাই বাক্স ফেরত দেয় সেগুলো জোড়ার নিয়মকে Monad. এই নামগুলো আরও পড়ার জন্য রেখে দিন; দৈনন্দিন JavaScript-এ ?., ?? আর Promise (পর্ব ৩৩) একই চিন্তার ব্যবহারিক রূপ।

বাস্তবের FP — যেখানে আপনি ইতিমধ্যেই এটি ব্যবহার করছেন

কোথায়কোন functional ধারণাএক লাইনে
ReactUI = f(state)Component একটি function: একই props/state → একই UI; state বদলাতে নতুন object দিতে হয় (পর্ব ৫৪)
Redux / useReducerPure reducer, immutable state(state, action) => newState; time-travel debugging
lodash/fpAuto-curried, data-lastLodash-এর একটি রূপ যেখানে ডেটা শেষ argument, যাতে compose করা যায়
RamdaCurry, compose, point-freeসব function curried; R.pipe, R.map, R.prop
Array method, PromiseHigher-order function, chainingmap/filter/reduce আর .then — প্রতিদিনের FP
C/C++/Python/Java জানলে

Java-তে function সরাসরি মান নয় — lambda আসলে একটি functional interface-এর (Function<T,R>) object, আর andThen/compose সেখানে built-in (Java-র lambda ও functional interface, Java-র stream). Python-এ functools.partial, functools.reduce আর lru_cache একই কাজ করে (Python-এর functional tools), আর Python-ও tail call optimize করে না। C++-এ lambda আর std::bind আছে, কিন্তু capture-এর জীবনকাল নিজে সামলাতে হয় (C++-এর lambda). JavaScript-এ এসব কোনো আলাদা interface বা library ছাড়াই, ভাষার মূল অংশ।

পরীক্ষা ও ইন্টারভিউতে
  • Pure function কী? — একই input-এ সবসময় একই output দেয় আর কোনো side effect নেই; যেমন (a, b) => a + b, কিন্তু Math.random() নয়।
  • Currying আর partial application-এর পার্থক্য? — Currying কয়েক-argument-এর function-কে এক-argument-এর function-এর শিকলে বদলায়; partial application যেকোনো কয়টি argument আগে বসিয়ে বাকিগুলোর জন্য function দেয়।
  • const দিয়ে ঘোষণা করা object কি immutable? — না; const শুধু নতুন মান বসানো আটকায়। ভেতর বদলানো আটকাতে Object.freeze, তাও shallow.
  • Interview-এর ক্লাসিক: curry(fn) লিখুন। — fn.length-এর সমান argument না জমা পর্যন্ত argument জমিয়ে নতুন function ফেরত দিন; জমলে fn(...args).
  • compose আর pipe-এর পার্থক্য? — একই কাজ, ক্রম উল্টো: compose ডান থেকে বাঁয়ে (reduceRight), pipe বাঁ থেকে ডানে (reduce).

৯ · AI-এর যুগে — AI-এর লেখা “functional” কোড যাচাই করা

“এই কোডটা functional style-এ লিখে দাও” — AI-কে বললে প্রায়ই খুব চতুর দেখতে কোড আসে। চতুর আর সঠিক এক জিনিস নয়। এই বিষয়ে AI-এর কয়েকটি নির্দিষ্ট অভ্যাস:

AI-এর ভুলকেন সমস্যাকী চাইবেন
Point-free ধাঁধা, নিজের বানানো compose-এর ভেতরে composeদলের কেউ পড়তে পারে না; stack trace-এ নাম নেইপ্রতিটি ধাপের নামওয়ালা function, argument-এর নাম যেখানে সাহায্য করে
“Pure” reducer-এর ভেতরে state.items.push(…) বা state.total = …পুরোনো state বদলায়; UI আপডেট হয় না, undo ভাঙেপ্রতিটি শাখায় নতুন object/array; Object.freeze দিয়ে টেস্ট
সবকিছু একটি reduce-এ, প্রতিবার [...acc, x]পড়া কঠিন; বড় তালিকায় প্রতিবার কপি করে ধীরfilter/map chain; reduce শুধু অনেক থেকে এক
Default বা rest parameter-ওয়ালা function-এ generic curryfn.length ভুল, তাই আগেভাগে চলেarity নিজে দেওয়া, বা হাতে লেখা curried function
লম্বা তালিকায় recursionV8-এ tail call optimization নেই — stack overflowreduce বা লুপ; recursion শুধু অল্প গভীর গাছে
“Immutable” বলে Object.freeze একবারShallow — ভেতরের object বদলায়Immutable update, বা deep freeze শুধু টেস্টে

AI-কে functional কোড চাইলে পাঠযোগ্যতা, purity আর টেস্ট — তিনটিই আগে থেকে বলে দিন:

একটি শপিং কার্টের জন্য JavaScript-এ একটি reducer লিখে দাও: (state, action) => newState. State: { items: [{ id, name, price, qty }], coupon: null }. Action-গুলো: add, remove, setQty আর applyCoupon. নিয়ম: ১. reducer সম্পূর্ণ pure হবে — state বা action কোনোভাবেই বদলাবে না; push, splice, sort, সরাসরি property বসানো নিষেধ। spread, map, filter, toSorted ব্যবহার করো। ২. Date.now(), Math.random(), fetch, console.log reducer-এর ভেতরে থাকবে না। ৩. অজানা action-এ একই state reference ফেরত দেবে। ৪. দাম, VAT আর গোল করার জন্য ছোট নামওয়ালা pure function বানাও, আর pipe দিয়ে জোড়ো। point-free ধাঁধা নয় — প্রতিটি ধাপ একজন নতুন ডেভেলপার পড়তে পারবে। ৫. reduce শুধু যেখানে অনেক থেকে একটি মান বানাতে হয়; recursion ব্যবহার করবে না। ৬. শেষে টেস্ট দাও: শুরুর state-কে deep freeze করে প্রতিটি action চালাও (mutation হলে strict mode-এ error আসবে), অজানা action-এ state === newState, আর একই action-তালিকা দুবার চালালে একই ফল।

AI-এর লেখা functional কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Functional programming কোনো ফ্যাশন নয় — এর শিকড় ১৯৩০-এর দশকের গণিতে। Lambda calculus আর Lisp থেকে JavaScript পর্যন্ত পথটা কেমন?

১৯৩০-এর দশকে আমেরিকার গণিতবিদ Alonzo Church একটি প্রশ্ন নিয়ে কাজ করছিলেন, যা তখন কম্পিউটারের আগের যুগের: “হিসাব করা যায়” — এই কথার সুনির্দিষ্ট গাণিতিক অর্থ কী? তাঁর উত্তর ছিল lambda calculus — একটি আশ্চর্য রকম ছোট ব্যবস্থা, যেখানে আছে শুধু তিনটি জিনিস: variable, function বানানো (λx. …) আর function প্রয়োগ। কোনো সংখ্যা নেই, কোনো লুপ নেই, কোনো variable বদলানো নেই — তবু দেখা গেল সংখ্যা, যোগ, শর্ত, এমনকি পুনরাবৃত্তিও শুধু function দিয়ে প্রকাশ করা যায়। প্রায় একই সময়ে Alan Turing তাঁর “Turing machine” দিয়ে একই প্রশ্নের উত্তর দেন, আর পরে প্রমাণ হয় দুটি ব্যবস্থা সমান ক্ষমতার। অর্থাৎ শুরু থেকেই হিসাবের দুটি সমান মডেল ছিল: একটি “যন্ত্রের অবস্থা ধাপে ধাপে বদলানো” (Turing), আরেকটি “function প্রয়োগ” (Church).

১৯৫৮ সালে MIT-তে John McCarthy Lisp-এর কাজ শুরু করেন, যা Church-এর ধারণা থেকে অনুপ্রাণিত — এমনকি function বানানোর কীওয়ার্ডের নামই ছিল lambda. Lisp-এ function ছিল ডেটার মতো মান, আর তালিকার ওপর function প্রয়োগ ছিল মূল কাজ। পরের দশকগুলোতে Scheme (১৯৭৫), ML (১৯৭৩), আর ১৯৯০ সালে Haskell এই ধারাকে এগিয়ে নেয়।

JavaScript এই গল্পে ঢোকে একটি দুর্ঘটনার মতো। ১৯৯৫ সালে Brendan Eich-কে Netscape-এ নিয়োগ দেওয়া হয়েছিল ব্রাউজারে “Scheme বসানোর” প্রতিশ্রুতিতে; পরে ব্যবস্থাপনা চাইল Java-র মতো দেখতে একটি ভাষা। ফল: বাইরে Java-র মতো বন্ধনী আর কীওয়ার্ড, ভেতরে Scheme-এর মতো first-class function আর closure. দীর্ঘদিন এই ভেতরের শক্তি খুব কম মানুষ ব্যবহার করত। Douglas Crockford-এর ২০০৮ সালের বই “JavaScript: The Good Parts” স্পষ্ট করে বলে — JavaScript-এর সবচেয়ে ভালো অংশগুলোর একটি হলো এর function. আজ (x) => x * 2 লেখার সময় আপনি আসলে Church-এর λx. x × 2 লিখছেন।

এখানে একটি গভীর বিষয় আছে: প্রায় সব আধুনিক কম্পিউটার Turing-এর মডেলের মতো — memory-র ঘর আর ধাপে ধাপে সেগুলো বদলানো। তাই imperative কোড যন্ত্রের কাছাকাছি, আর functional কোড গণিতের কাছাকাছি। ভাষা-নকশার পুরো ইতিহাসকে এই দুই মেরুর মধ্যে টানাপোড়েন হিসেবে দেখা যায়।

মূল উপলব্ধি: Functional programming একটি পুরোনো, গাণিতিকভাবে দৃঢ় ভিত্তির ওপর দাঁড়িয়ে। JavaScript-এর arrow function আর closure সেই ভিত্তির সরাসরি উত্তরাধিকার — আর তাই এগুলো শিখলে আপনি শুধু একটি ভাষার ব্যাকরণ নয়, হিসাবের একটি মৌলিক মডেল শিখছেন।

প্র ০২১৯৭৭ সালে FORTRAN-এর স্রষ্টা John Backus তাঁর Turing Award বক্তৃতায় প্রশ্ন তোলেন: “প্রোগ্রামিংকে কি von Neumann ধরন থেকে মুক্ত করা যায়?” তিনি আসলে কী বলতে চেয়েছিলেন?

John Backus ছিলেন imperative প্রোগ্রামিংয়ের এক প্রধান স্থপতি — ১৯৫০-এর দশকে IBM-এ তাঁর দল FORTRAN বানায়, প্রথম ব্যাপকভাবে সফল high-level ভাষা। BNF নামের ব্যাকরণ লেখার পদ্ধতিতেও তাঁর নাম জড়িয়ে। তাই ১৯৭৭ সালে Turing Award পেয়ে তিনি যখন বক্তৃতা দিলেন “Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs” (১৯৭৮ সালে Communications of the ACM-এ প্রকাশিত), তখন সেটি ছিল নিজের তৈরি ধারারই কঠোর সমালোচনা।

তাঁর যুক্তি ছিল এরকম। von Neumann কম্পিউটারে CPU আর memory-র মাঝে একটি সরু পথ আছে, আর প্রতিটি হিসাব মানে সেই পথ দিয়ে একটি একটি করে শব্দ (word) আনা-নেওয়া। Backus এর নাম দেন “von Neumann bottleneck”. তাঁর আসল দাবি ছিল আরও গভীর: প্রোগ্রামিং ভাষাগুলোও এই যন্ত্রের ছাঁচে তৈরি — assignment statement (x = x + 1) হলো সেই সরু পথের ভাষাগত রূপ। ফলে প্রোগ্রামারকে ভাবতে হয় “একটি একটি শব্দ” ধরে, বড় ডেটা-রূপান্তর ধরে নয়। আর এমন প্রোগ্রাম নিয়ে গাণিতিকভাবে যুক্তি দেওয়া কঠিন, কারণ প্রতিটি লাইনের অর্থ নির্ভর করে তখনকার পুরো memory-র অবস্থার ওপর।

বিকল্প হিসেবে তিনি প্রস্তাব করেন FP নামের একটি ভাষা, যেখানে variable নেই; প্রোগ্রাম বানানো হয় কয়েকটি “combining form” দিয়ে function জুড়ে — composition, প্রতিটি উপাদানে প্রয়োগ (আজকের map), আর insert (আজকের reduce). প্রোগ্রাম তখন বীজগণিতের মতো: দুটি প্রোগ্রাম সমান কি না, সমীকরণ সাজিয়ে প্রমাণ করা যায়। এই পর্বের pipe(applyDiscount, addVat, roundToPaisa) — যেখানে কোনো variable-এর নাম নেই — ঠিক সেই ধাঁচের।

FP ভাষাটি নিজে জনপ্রিয় হয়নি। কিন্তু বক্তৃতাটি functional programming গবেষণায় বিশাল উৎসাহ জাগায়, আর এর অনেক চিন্তা পরে ML, Haskell, এবং শেষ পর্যন্ত JavaScript-এর array method আর React-এর দর্শনে পৌঁছায়। একজন imperative ভাষার স্রষ্টার মুখে imperative ধারার সমালোচনা — এটিই বক্তৃতাটিকে বিশেষ গুরুত্ব দিয়েছিল।

মূল উপলব্ধি: ভাষা আমাদের চিন্তার আকার দেয়। Assignment-কেন্দ্রিক ভাষা আমাদের “এক ঘর, এক ধাপ” ভাবতে শেখায়; composition-কেন্দ্রিক চিন্তা পুরো রূপান্তর একসাথে দেখতে শেখায়। দুটোই জানা থাকলে আপনি সমস্যার জন্য ঠিক লেন্সটি বেছে নিতে পারেন।

প্র ০৩“Currying” নামটি এসেছে Haskell Curry-র নাম থেকে, অথচ ধারণাটি তিনি প্রথম দেননি। Moses Schönfinkel কে ছিলেন, আর একটি ধারণার নাম কীভাবে ভুল মানুষের নামে বসে যায়?

Currying-এর মূল চিন্তা — কয়েক argument-এর function মানে আসলে এক argument-এর function, যে আরেকটি function ফেরত দেয় — গণিতে Haskell Curry-র আগেই দেখা গিয়েছিল। জার্মান যুক্তিবিদ Gottlob Frege উনিশ শতকের শেষে এমন চিন্তার ইঙ্গিত দেন। কিন্তু একে পরিষ্কারভাবে ব্যবহার করেন রুশ গণিতবিদ Moses Schönfinkel, ১৯২৪ সালে প্রকাশিত একটি গবেষণাপত্রে, যেখানে তিনি “combinatory logic”-এর ভিত্তি রাখেন — variable ছাড়াই যুক্তিবিদ্যা প্রকাশের একটি পদ্ধতি। সেই কাজে কয়েক-argument-এর function-কে এক-argument-এর শিকলে ভাঙা ছিল একটি মূল কৌশল। Schönfinkel-এর জীবন ছিল দুঃখজনক; পরে তিনি মানসিক অসুস্থতায় ভোগেন আর দারিদ্র্যে মারা যান, তাঁর অনেক কাজ হারিয়ে যায়।

আমেরিকান যুক্তিবিদ Haskell Brooks Curry স্বাধীনভাবে combinatory logic নিয়ে কাজ শুরু করেন, পরে Schönfinkel-এর কাজের কথা জানেন, এবং কয়েক দশক ধরে এই শাখাকে বিশাল ও পূর্ণাঙ্গ করে তোলেন। তাঁর বিস্তৃত কাজের কারণে কৌশলটি তাঁর নামের সাথে জড়িয়ে যায়। “currying” শব্দটি সাধারণত ব্রিটিশ কম্পিউটার বিজ্ঞানী Christopher Strachey-র নামে চালু বলে ধরা হয় (১৯৬০-এর দশকে)। কেউ কেউ মজা করে “Schönfinkelization” নাম প্রস্তাব করেছিলেন, কিন্তু সেটি টেকেনি। Curry-র সম্মান আরও বেড়েছে: তাঁর প্রথম নাম থেকে Haskell ভাষার নাম, আর Curry নামে আরেকটি গবেষণা-ভাষাও আছে।

বিজ্ঞানের ইতিহাসে এটি একটি পরিচিত ঘটনা। একটি সূত্রের নাম প্রায়ই তার প্রথম আবিষ্কারকের নামে হয় না — হয় যিনি সেটি ছড়িয়ে দিয়েছেন বা ব্যাপকভাবে ব্যবহার করেছেন তাঁর নামে। পরিসংখ্যানবিদ Stephen Stigler একে মজা করে একটি “নিয়ম” বলেছেন — আর বলেছেন, এই নিয়মটিও তিনি প্রথম আবিষ্কার করেননি।

প্রোগ্রামিংয়ে এর শিক্ষাটি ব্যবহারিক: lambda calculus-এ প্রতিটি function একটিই argument নেয়, তাই সেখানে currying কোনো কৌশল নয়, স্বাভাবিক অবস্থা। Haskell-এও সব function স্বয়ংক্রিয়ভাবে curried. JavaScript-এ কয়েক-argument-এর function স্বাভাবিক, তাই currying একটি সচেতন বাছাই — যেখানে আগে কনফিগারেশন বসিয়ে বিশেষায়িত function বানানো কাজে লাগে (formatTaka("bn-BD")), সেখানে।

মূল উপলব্ধি: নামের পেছনের ইতিহাস জানলে ধারণাটির মূল বোঝা যায়: currying হলো “প্রতিটি function একটি argument নেয়” — এই গাণিতিক সরলতার প্রোগ্রামিং রূপ। আর বিজ্ঞানের কৃতিত্ব সবসময় ঠিক মানুষের কাছে যায় না।

প্র ০৪Functional programming দশকের পর দশক মূলত গবেষণার বিষয় ছিল। ২০১০-এর দশকে হঠাৎ এটি মূলধারায় ফিরে এল কেন — multicore CPU, React আর Redux কী ভূমিকা রাখল?

প্রথম কারণ হার্ডওয়্যার। প্রায় ২০০৫ সাল পর্যন্ত প্রতি বছর CPU-র clock speed বাড়ত, আর পুরোনো প্রোগ্রাম নিজে থেকেই দ্রুত হয়ে যেত। তারপর তাপ আর বিদ্যুৎ খরচের দেয়ালে ধাক্কা লাগে — নির্মাতারা একটি দ্রুত core-এর বদলে কয়েকটি core দিতে শুরু করেন। Herb Sutter-এর ২০০৫ সালের বিখ্যাত প্রবন্ধ “The Free Lunch Is Over” এই পরিবর্তন তুলে ধরে: এখন দ্রুত হতে হলে প্রোগ্রামকে একসাথে কয়েকটি কাজ করতে হবে। আর shared, mutable state-ওয়ালা কোড একসাথে চালানো ভয়ংকর কঠিন — race condition, lock, deadlock. যে ডেটা কেউ বদলায় না, তা নিয়ে কোনো ঝগড়াও নেই। Immutability আর pure function হঠাৎ একটি একাডেমিক সৌন্দর্য থেকে ব্যবহারিক প্রয়োজন হয়ে উঠল। Clojure (২০০৭), Scala, আর পরে Java 8-এর lambda ও stream (২০১৪) এই প্রয়োজনের উত্তর।

দ্বিতীয় কারণ ব্রাউজারের UI. jQuery-যুগে পাতা বদলানো হতো সরাসরি DOM ছুঁয়ে — এখানে একটি class যোগ, ওখানে একটি লেখা বদল। অ্যাপ বড় হলে কোন কোড কখন কী বদলেছে, ধরা অসম্ভব হয়ে যেত। ২০১৩ সালে Facebook React প্রকাশ করে একটি সরল কিন্তু বৈপ্লবিক চিন্তা নিয়ে: UI হলো state-এর একটি function — UI = f(state). State বদলালে পুরো UI নতুন করে “হিসাব” করুন; কোন DOM অংশ সত্যিই বদলাতে হবে, তা React নিজে বের করবে। এই হিসাব দ্রুত করতে React-কে জানতে হয় state বদলেছে কি না — আর immutable ডেটায় সেটি এক তুলনাতেই জানা যায় (old !== new).

তৃতীয় ধাপ ২০১৫ সালের Redux, যা Dan Abramov আর Andrew Clark বানান, Elm ভাষার “model–update–view” নকশা থেকে অনুপ্রাণিত। পুরো অ্যাপের state একটি জায়গায়, বদলায় শুধু pure reducer দিয়ে। Redux DevTools-এর “time-travel” — প্রতিটি action-এর আগে-পরের state-এ যাওয়া — লাখো ডেভেলপারকে হাতেকলমে দেখাল immutability কী দেয়। একই সময়ে ES2015 arrow function আনে, ফলে ছোট function লেখা সস্তা হয়ে যায়; Lodash, Ramda, Immutable.js জনপ্রিয় হয়।

লক্ষ করুন, এই ফেরা “pure FP ভাষায় সবাই চলে গেল” ধরনের নয়। বরং mainstream ভাষাগুলো FP-র সবচেয়ে কাজের অংশগুলো নিয়ে নিল — lambda, immutable collection, map/filter/reduce, pattern matching — আর পুরোনো ধাঁচের সাথে মিশিয়ে ব্যবহার করতে লাগল।

মূল উপলব্ধি: ধারণা জনপ্রিয় হয় যখন তা একটি বাস্তব যন্ত্রণা দূর করে। Multicore-এর shared state আর বড় UI-এর বিশৃঙ্খলা ছিল সেই যন্ত্রণা; immutability আর pure function ছিল ওষুধ। তাই আজ React শিখতে গেলে আপনাকে আগে এই পর্বের ধারণাগুলো বুঝতে হয়।

প্র ০৫ES2015 মানদণ্ডে “proper tail calls” বাধ্যতামূলক, তবু প্রায় দশ বছর পরেও শুধু Safari এটি চালু রেখেছে। Chrome আর Firefox কেন রাজি হলো না — মানদণ্ড আর বাস্তবতার এই দ্বন্দ্ব থেকে কী শেখার আছে?

Scheme ভাষার মানদণ্ড বহু আগে থেকেই বলে: tail position-এর কল (function-এর একেবারে শেষ কাজ যদি আরেকটি কল হয়) stack বাড়াবে না। এতে recursion লুপের মতোই নিরাপদ হয় — লাখো স্তর গভীর হলেও stack overflow নেই। JavaScript-এর functional ধাঁচ যত জনপ্রিয় হলো, তত দাবি উঠল JavaScript-এও এটি চাই। ES2015 (ES6) মানদণ্ড strict mode-এর কোডের জন্য “proper tail calls” (PTC) যুক্ত করে — এটি ঐচ্ছিক optimization নয়, বাধ্যতামূলক আচরণ।

Apple-এর JavaScriptCore দল এটি বাস্তবায়ন করে, আর ২০১৬ সালের Safari 10 থেকে চালু রাখে। V8 দলও একটি পরীক্ষামূলক বাস্তবায়ন করে flag-এর পেছনে, কিন্তু শেষ পর্যন্ত সরিয়ে নেয়। Mozilla-ও আপত্তি জানায়। মূল আপত্তিগুলো ছিল: এক, debugging. পুরোনো frame ফেলে দেওয়া হলে error-এর stack trace থেকে মাঝের কলগুলো হারিয়ে যায় — ডেভেলপার দেখতে পান না কোন পথে এসে error হলো। DevTools আর error-রিপোর্টিং টুল stack trace-এর ওপর গভীরভাবে নির্ভর করে। দুই, অদৃশ্য আচরণ-পরিবর্তন. PTC কোনো নতুন ব্যাকরণ ছাড়াই কাজ করে, তাই বিদ্যমান কোডের stack trace চুপচাপ বদলে যায়, আর কোনো কল আসলে tail position-এ কি না, প্রোগ্রামার প্রায়ই নিশ্চিত হতে পারেন না। তিন, পারফরম্যান্স ও বাস্তবায়নের জটিলতা — কিছু ক্ষেত্রে এটি engine-এর অন্য optimization-এর সাথে খাপ খায় না।

একটি আপস প্রস্তাবও আসে — “Syntactic Tail Calls” (STC): প্রোগ্রামার স্পষ্ট চিহ্ন দিয়ে বলবেন “এই কলটি tail call হোক” (যেমন return continue f(x)), যাতে আচরণ লুকানো না থাকে। কিন্তু Apple-এর দল যুক্তি দেয় মানদণ্ডে PTC তো আগেই আছে, আর প্রস্তাবটি এগোয়নি। ফল: একটি মানদণ্ড-নির্দিষ্ট feature, যা কার্যত শুধু একটি engine-এ আছে। TC39-এর আধুনিক নিয়ম — কোনো feature Stage 4-এ যেতে কমপক্ষে দুটি স্বাধীন বাস্তবায়ন লাগবে — অনেকটা এ ধরনের অভিজ্ঞতা থেকেই গুরুত্বপূর্ণ।

আপনার জন্য ব্যবহারিক অর্থ: JavaScript-এ গভীর recursion-কে নিরাপদ ধরে নেবেন না। লম্বা তালিকায় reduce বা লুপ, গাছে recursion কিন্তু গভীরতা জেনে; আর খুব গভীর হলে নিজের একটি array-কে stack বানিয়ে iteration. “trampoline” নামের একটি কৌশলও আছে, যেখানে function পরের ধাপটি সরাসরি না ডেকে একটি function ফেরত দেয়, আর একটি লুপ সেগুলো একে একে চালায়।

মূল উপলব্ধি: মানদণ্ডে লেখা থাকলেই feature বাস্তব হয় না — engine নির্মাতারা debugging, পারফরম্যান্স আর পুরোনো কোডের দিক থেকে মূল্য হিসাব করেন। তাই “spec বলে” আর “আমার ব্যবহারকারীর ব্রাউজারে চলে” — দুটো আলাদা প্রশ্ন, আর দ্বিতীয়টিই আসল।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।

  1. Pure নাকি impure? প্রতিটির পাশে লিখুন, আর কারণ দিন: (ক) (xs) => xs.length (খ) (xs) => xs.sort() (গ) (name) => `হ্যালো, ${name}` (ঘ) (p) => p * taxRate, যেখানে taxRate একটি বাইরের let (ঙ) (user) => ({ ...user, lastSeen: Date.now() })

    (ক) pure — শুধু পড়ে, কিছু বদলায় না। (খ) impure — sort কলকারীর array বদলায়; toSorted() দিলে pure. (গ) pure. (ঘ) impure — ফল বাইরের বদলযোগ্য variable-এর ওপর নির্ভর করে; taxRate বদলালে একই p-তে ভিন্ন ফল। সমাধান: (rate) => (p) => p * rate — curried, rate argument হিসেবে। (ঙ) impure — নতুন object বানায় বলে mutation নেই, কিন্তু Date.now() প্রতিবার ভিন্ন মান দেয়। সময়কে argument বানান: (user, now) => ({ ...user, lastSeen: now }).

  2. ক্রম অনুমান করুন: const double = (x) => x * 2; const inc = (x) => x + 1; হলে pipe(double, inc)(5) আর compose(double, inc)(5)-এর মান কত? দুটি ভিন্ন কেন?

    pipe(double, inc)(5) = inc(double(5)) = inc(10) = 11. compose(double, inc)(5) = double(inc(5)) = double(6) = 12. pipe বাঁ থেকে ডানে চালায়, compose ডান থেকে বাঁয়ে। গুণ আর যোগের ক্রম বদলালে ফল বদলায় — composition সাধারণত বিনিময়যোগ্য (commutative) নয়। তাই দলে একটিই বেছে নিন আর সবখানে সেটিই ব্যবহার করুন।

  3. বাগ খুঁজুন: একজন ডেভেলপার Redux reducer লিখলেন: case "add": state.items.push(action.item); return state;. টেস্টে কার্টে পণ্য যোগ হচ্ছে, কিন্তু React-এর পাতায় নতুন পণ্য দেখা যাচ্ছে না। কেন? দুই লাইনে ঠিক করুন।

    push পুরোনো array-টিই বদলায়, আর return state একই reference ফেরত দেয়। React/Redux “কিছু বদলেছে কি না” বোঝে reference তুলনা করে (oldState !== newState) — reference একই, তাই তারা ধরে নেয় কিছুই বদলায়নি, আর পাতা নতুন করে আঁকে না। উপরন্তু পুরোনো state-ও বদলে গেছে, তাই undo বা time-travel ভাঙে। সমাধান: case "add": return { ...state, items: [...state.items, action.item] };. টেস্টে শুরুর state-কে Object.freeze (ভেতরের items-সহ) করে strict mode-এ চালালে এই বাগ সাথে সাথে TypeError হয়ে ধরা পড়ত।

  4. নিজে লিখুন (একটিমাত্র কোডের কাজ): একটি curried function discount(percent)(price) আর একটি addDelivery(charge)(price) লিখুন। তারপর pipe দিয়ে “২০% ছাড়, তারপর ৬০ টাকা ডেলিভারি, তারপর পয়সায় গোল” — এমন checkoutDhaka বানান, আর [500, 1250]-এর ওপর map করুন।

    const discount = (percent) => (price) => price * (1 - percent / 100); const addDelivery = (charge) => (price) => price + charge; const checkoutDhaka = pipe(discount(20), addDelivery(60), roundToPaisa); [500, 1250].map(checkoutDhaka) → [ 460, 1060 ] (৫০০ × ০.৮ = ৪০০, + ৬০ = ৪৬০; ১২৫০ × ০.৮ = ১০০০, + ৬০ = ১০৬০)। লক্ষ করুন, curried function-গুলোতে কনফিগারেশন (percent, charge) আগে আর দাম শেষে — তাই discount(20) বসিয়েই পাইপলাইনে পাঠানো গেল। চট্টগ্রামের জন্য শুধু addDelivery(120) দিয়ে আরেকটি pipe — কোনো কোড কপি করতে হলো না।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রম“Functional core, imperative shell” নকশায় একটি বোতাম চাপা থেকে পাতা আপডেট হওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে বা টেনে)।

  1. 🖱️ Shell: ব্যবহারকারীর click ধরা (event listener)
  2. 📨 Shell: একটি action object বানানো, সময়ের মতো impure মান এখানেই বসানো
  3. 🧪 Core: pure reducer (state, action) থেকে নতুন state ফেরত দেয়
  4. 🧮 Core: pure function নতুন state থেকে দেখানোর ডেটা হিসাব করে
  5. 🖼️ Shell: DOM-এ নতুন ডেটা বসানো (side effect)

JavaScript-এ নিচের কোন function pure?

  • (x) => x + Math.random()
  • (arr, x) => { arr.push(x); return arr; }
  • (arr, x) => [...arr, x]
  • (x) => { count++; return x * 2; }

JavaScript-এ const cfg = Object.freeze({ shop: { name: "A" } }); cfg.shop.name = "B"; চালানোর পরে cfg.shop.name কত?

  • "A"
  • "B"
  • TypeError হয়
  • undefined

JavaScript-এ ((a, b = 2, c) => a).length-এর মান কত?

  • 3
  • 2
  • 0
  • 1

JavaScript-এ const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x); হলে pipe((x) => x + 1, (x) => x * 10)(2)-এর মান কত?

  • 30
  • 21
  • 3
  • একটি function

Redux-ধাঁচের reducer সম্পর্কে কোন কথাটি সঠিক?

  • Reducer-এর ভেতরে fetch দিয়ে সার্ভার থেকে ডেটা আনা উচিত
  • দ্রুততার জন্য state সরাসরি বদলে একই state ফেরত দেওয়া উচিত
  • এটি (state, action) নিয়ে নতুন state ফেরত দেয় এবং কিছু বদলায় না
  • Reducer নিজের ভেতরে একটি global variable-এ state জমা রাখে

V8-ভিত্তিক Chrome বা Node.js-এ লম্বা array-এর ওপর গভীর recursion কেন ঝুঁকিপূর্ণ?

  • JavaScript-এ recursion ব্যবহার করা নিষিদ্ধ
  • V8 proper tail call চালু করেনি, তাই প্রতিটি কলে stack বাড়ে আর stack overflow হতে পারে
  • Recursion সবসময় ভুল ফল দেয়
  • Garbage collector recursion চলার সময় array মুছে দেয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।

মনে রাখুন

  1. Functional programming মানে shared state বদলানোর বদলে ছোট pure function জুড়ে প্রোগ্রাম বানানো। JavaScript-এর first-class function আর closure এটিকে বাস্তবে সহজ করে।
  2. Pure function: একই input → একই output, কোনো side effect নেই — টেস্ট, cache আর ভাগ করে চালানো সহজ। Side effect রাখুন কিনারার shell-এ।
  3. const ≠ immutability; Object.freeze shallow. বদলানোর বদলে নতুন কপি: spread, toSorted, with; গভীর ডেটায় Immer.
  4. Currying-এ কম-বদলানো argument আগে। Generic curry চলে fn.length দেখে — default আর rest parameter সেটিকে কমিয়ে দেয়। Partial application-এ arrow wrapper বা bind.
  5. pipe = reduce, compose = reduceRight. Point-free যখন নাম পরিষ্কার; reducer সবসময় pure; লম্বা তালিকায় recursion নয় (V8-এ tail call optimization নেই)।

পরের পর্বে: এই পর্বে bind-এর প্রথম argument-এ null দিয়েছিলেন আর বলেছিলাম সেটি this. কিন্তু this আসলে কী, কেন একই function ভিন্ন জায়গা থেকে ডাকলে ভিন্ন this পায়, আর arrow function কেন এখানে আলাদা? পর্ব ২৪-এ this, call, apply আর bind.

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান