برنامه نویسی تابعی (Functional Programming) و مفاهیم مقدماتی آن — به زبان ساده

۱۵۰۹ بازدید
آخرین به‌روزرسانی: ۰۱ مهر ۱۴۰۲
زمان مطالعه: ۱۴ دقیقه
برنامه نویسی تابعی (Functional Programming) و مفاهیم مقدماتی آن — به زبان ساده

بسیاری از ما برنامه‌نویسی را از ابتدا با مفاهیم برنامه‌نویسی شیءگرا آموخته‌ایم؛ اما بهتر است با مفهوم پیچیدگی سیستم نیز آشنا باشیم.

منظور از پیچیدگی سیستم آن چیزی است که موجب می‌شود درک یا اصلاح نرم‌افزار دشوار شود.

- John Outerhout

برخی از مفاهیم برنامه‌نویسی تابعی مانند «تغییرناپذیری» (immutability) و «تابع محض» (pure function) وجود دارند که برای ساخت تابع‌های بدون عوارض جانبی بسیار مفید هستند و بدین ترتیب قابلیت نگهداری سیستم و چند مزیت دیگر ارتقا می‌یابد.

در این مقاله در مورد برنامه‌نویسی تابعی و برخی مفاهیم مهم آن به همراه ارائه نمونه کد توضیح می‌دهیم.

برنامه‌نویسی تابعی چیست؟

بر اساس تعریف ویکی‌پدیا، برنامه‌نویسی تابعی یک پارادایم برنامه‌نویسی یا سبکی برای ایجاد ساختار و عناصر برنامه‌های رایانه‌ای است که با محاسبه به صورت یک ارزیابی تابع‌های ریاضیاتی برخورد می‌کند و از وضعیت‌های در حال تغییر و داده‌های تغییرپذیر اجتناب می‌کند.

تابع‌های محض

نخستین مفهوم بنیادی که هنگام یادگیری برنامه‌نویسی تابعی باید بیاموزیم موضوع «تابع‌های محض» (Pure functions) است. اما منظور از تابع محض چیست و چه چیزی یک تابع را محض می‌کند؟ و از کجا بدانیم یک تابع، محض است یا نه؟

تعریف دقیق محض بودن به صورت زیر است:

  • تابع محض در صورت ارائه آرگومان‌های ثابت، همواره نتیجه ثابتی ارائه می‌کند (این خصوصیت قطعیت یا deterministic نیز نامیده می‌شود)
  • تابع محض هیچ گونه عوارض جانبی قابل مشاهده‌ای ندارد.

تابع محض برای ورودی‌های ثابت، خروجی‌های ثابتی ارائه می‌کند

تصور کنید می‌خواهیم تابعی پیاده‌سازی کنیم که مساحت یک دایره را محاسبه کند.

یک تابع غیر محض شعاع (radius) دایره را به عنوان پارامتر می‌گیرد و سپس (radius * radius * PI) را محاسبه می‌کند. در زبان برنامه‌نویسی «کلوژر» (Clojure) عملگرها در ابتدا می‌آیند و از این رو فرمول (radius * radius * PI) به صورت (* radius radius PI) ارائه می‌شود:

(def PI 3.14)

(defn calculate-area
  [radius]
  (* radius radius PI))

(calculate-area 10) ;; returns 314.0

دلیل غیر محض بودن این تابع آن است که از یک متغیر سراسری استفاده می‌کند که به صورت پارامتری به تابع ارسال نمی‌شود. اکنون تصور کنید برخی از ریاضیدان‌ها استدلال کنند که مقدار عدد PI در واقع برابر با 42 است و مقدار این متغیر سراسری تغییر یابد.

در این حالت تابع غیر محض ما نتیجه‌ای برابر با 10 * 10 * 42 = 4200 ارائه می‌کند. در واقع برای پارامتر ثابت شعاع = 10 ما نتایج متفاوتی را داریم. این وضعیت با استفاده از روش زیر قابل اصلاح است:

(def PI 3.14)

(defn calculate-area
  [radius, PI]
  (* radius radius PI))

(calculate-area 10 PI) ;; returns 314.0

اینک ما همواره مقدار PI را به عنوان یک پارامتر به تابع ارسال می‌کنیم بنابراین در این صورت ما تنها به پارامترهای ارسال شده به تابع دسترسی داریم و نه هیچ «شیء خارجی» (external object). در این حالت:

  • ما برای پارامترهای radius = 10 و PI = 3.14 همواره نتیجه مشابه 314.0 را داریم.
  • برای پارامترهای radius = 10 و PI = 42 همواره نتیجه مشابه 4200 را داریم.

خواندن فایل‌ها

اگر وظیفه تابع ما خواندن فایل‌ها باشد، نمی‌تواند یک تابع محض باشد چون محتوای فایل‌ها می‌توانند تغییر یابند:

(defn characters-counter
  [text]
  (str "Character count: " (count text)))

(defn analyze-file
  [filename]
  (characters-counter (slurp filename)))

(analyze-file "test.txt")

تولید عدد تصادفی

هر تابعی که مبتنی بر تولید کننده عدد تصادفی باشد نمی‌تواند یک تابع محض باشد.

(defn year-end-evaluation
  []
  (if (> (rand) 0.5)
    "You get a raise!"
"Better luck next year!"))

تابع محض هیچ عوارض جانبی قابل مشاهده‌ای ندارد

نمونه‌هایی از عوارض ناخواسته شامل اصلاح یک شیء سراسری یا یک پارامتر ارسالی با ارجاع هستند. اینک می‌خواهیم تابعی را پیاده‌سازی کنیم که یک مقدار صحیح دریافت کند و مقدار افزایش یافته به میزان 1 واحد را بازگرداند.

(def counter 1)

(defn increase-counter
  [value]
  (def counter (inc value))) ;; please don't do this

(increase-counter counter) ;; 2
counter ;; 2

ما مقدار counter را داریم و تابع محض ما این مقدار را دریافت می‌کند و counter را به میزان 1 واحد بالاتر تعیین می‌کند. دقت کنید که تغییرپذیری در برنامه‌نویسی تابعی امری نامطلوب است.

حال یک متغیر سراسری را تغییر می‌دهیم؛ اما چگونه می‌توان آن را یک تابع محض کرد؟ کافی است این مقدار را به میزان 1 واحد افزایش دهیم. به همین سادگی:

(def counter 1)

(defn increase-counter
  [value]
  (inc value))

(increase-counter counter) ;; 2
counter ;; 1

می‌بینید که تابع محض ما به نام increase-counter مقدار 2 را بازمی‌گرداند؛ اما مقدار counter همچنان همان است. این تابع مقدار افزایش یافته را بدون تغییر دادن مقدار متغیر بازگشت می‌دهد.

اگر از این قواعد ساده تبعیت کنیم، درک برنامه‌ها آسان‌تر خواهد شد. اینک هر تابعی مجزا عمل می‌کند و نمی‌تواند روی بخش‌های دیگر سیستم تأثیر بگذارد.

تابع‌های محض پایدار، سازگار و قابل پیش‌بینی هستند. با ارائه پارامترهای ثابت، تابع محض همواره نتیجه یکسانی بازمی‌گرداند و دیگر لازم نیست که به موقعیت‌هایی فکر کنیم که پارامترهای یکسان نتایج متفاوتی بازمی‌گردانند، زیرا چنین موقعیت‌هایی هرگز پیش نخواهند آمد.

مزیت‌های تابع‌های محض

بدیهی است که تست چنین کدهایی ساده‌تر است. دیگر لازم نیست موقعیت‌های مختلف را شبیه‌سازی کنیم و از این رو می‌توانیم تابع‌های محض را با چارچوب‌های مختلف «تست واحد» (unit test) کنیم.

  • با فرض وجود پارامتر A انتظار داریم که تابع مقدار B را بازگرداند.
  • با فرض وجود پارامتر C انتظار داریم که تابع مقدار D را بازگرداند.

نمونه ساده‌ای از تابع محض، تابعی است که مجموعه‌ای از اعداد را دریافت کرده و هر یک از اعداد این مجموعه را یک واحد افزایش می‌دهد:

(defn increment-numbers
  [numbers]
(map inc numbers))

ما در کد فوق مجموعه numbers را دریافت می‌کنیم و با استفاده از map در تابع inc هر یک از این اعداد را یک واحد افزایش می‌دهیم و فهرست جدید اعداد افزایش یافته را بازمی‌گردانیم:

(= [2 3 4 5 6] (increment-numbers [1 2 3 4 5]));; true

برای ورودی [1 2 3 4 5] یک خروجی به صورت [2 3 4 5 6] قابل انتظار است.

تغییرناپذیری (Immutability)

منظور از تغییرناپذیری عدم تغییر در طی زمان یا عدم توانایی برای تغییر است.

وقتی داده‌ها تغییرناپذیر باشند، حالت آن‌ها پس از ایجاد نمی‌تواند تغییری داشته باشد. اگر بخواهیم یک شیء تغییرناپذیر را تغییر دهیم، قادر به چنین کاری نخواهیم بود. در عوض باید یک شیء با یک مقدار جدید ایجاد کرد.

در جاوا اسکریپت به طور معمول از حلقه for استفاده می‌شود. این گزاره for برخی متغیرهای تغییرپذیر دارد:

var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;

for (var i = 0; i < values.length; i++) {
  sumOfValues += values[i];
}

sumOfValues // 15

در هر بار تکرار مقدار i و حالت sumOfValue تغییر می‌یابند. اما تغییرپذیری در چرخه تکرارها چگونه قابل مدیریت است. اگر به کلوژر باز گردیم:

(defn sum
  [values]
  (loop [vals values
         total 0]
    (if (empty? vals)
      total
      (recur (rest vals) (+ (first vals) total)))))

(sum [1 2 3 4 5]) ;; 15

می‌بینیم که تابعی به نام sum داریم که یک بردار از مقادیر عددی دریافت می‌کند. recur بارها به loop بازمی‌گردد تا زمانی که یک بردار خالی به دست آید. در هر بار تکرار این مقدار به تجمیع کننده total اضافه می‌شود.

در واقع ما با بهره‌گیری از رویه بازگشتی متغیر خود را تغییرناپذیر حفظ می‌کنیم. اگر دقت کنید متوجه می‌شوید که ما از Rduce برای پیاده‌سازی این تابع استفاده کرده‌ایم. این موضوع را در بخش تابع‌های درجه بالاتر بیشتر توضیح خواهیم داد.

ساخت حالت‌هایی برای یک شیء نیز بسیار متداول است تصور کنید رشته‌ای دارید و می‌خواهیم این رشته را به صورت یک url slug درآورید. در برنامه‌نویسی شیءگرا در روبی (Ruby) به این منظور یک کلاس مانند UrlSlugify ایجاد می‌شود. و این کلاس یک متد !slugify دارد که وظیفه آن تبدیل ورودی رشته به یک url slug است.

class UrlSlugify
  attr_reader :text
  
  def initialize(text)
    @text = text
  end

  def slugify!
    text.downcase!
    text.strip!
    text.gsub!(' ', '-')
  end
end

UrlSlugify.new(' I will be a url slug   ').slugify! # "i-will-be-a-url-slug"

در کد فوق ما این رویه را به خوبی پیاده‌سازی کرده‌ایم. در این جا یک برنامه‌نویسی عملی داریم که همه کارهای مورد نیاز در فرایند slugify را اجرا می‌کند، یعنی ابتدا همه حروف را به حالت کوچک درمی‌آورد و سپس فاصله‌های بی‌استفاده را حذف کرده و در نهایت فاصله‌های باقی مانده را با خط تیره پر می‌کند. اما دقت کنید که ما ورودی خود را در طی این فرایند تغییر می‌دهیم.

ما می‌توانیم این تغییرپذیری را با اجرای ترکیب تابع یا تغییر دادن تابع مدیریت کنیم. به بیان دیگر نتیجه یک تابع به عنوان ورودی تابع دیگر استفاده شود و دیگر نیاز نیست که رشته ورودی اصلی تغییری پیدا کند:

(defn slugify
  [string]
  (clojure.string/replace
    (clojure.string/lower-case
      (clojure.string/trim string)) #" " "-"))

(slugify " I will be a url slug   ")

در کد فوق موارد زیر را داریم:

  • trim: فاصله‌های خالی را از دو انتهای رشته حذف می‌کند.
  • Lower-case: تمام حروف رشته را به حالت کوچک تبدیل می‌کند.
  • replace: همه موردی که در یک رشته با عبارت جایگزین تطبیق داشته باشند را جایگزین می‌کند.

با ترکیبی از هر سه تابع فوق می‌توان یک رشته را slugify کرد.

وقتی از ترکیب کردن تابع‌ها صحبت می‌کنیم، می‌توانیم از تابع comp برای ترکیب هر سه تابع استفاده کنیم. به کد زیر نگاه کنید:

(defn slugify
  [string]
  ((comp #(clojure.string/replace % #" " "-")
         clojure.string/lower-case
         clojure.string/trim)
    string))

(slugify " I will be a url slug   ") ;; "i-will-be-a-url-slug"

شفافیت ارجاعی (Referentially Transparent)

در این بخش یک تابع square را پیاده‌سازی کرده‌ایم:

(defn square
  [n]
(* n n))

این تابع (محض) با داشتن ورودی‌های یکسان، همواره همان خروجی ثابت را خواهد داشت:

(square 2) ;; 4
(square 2) ;; 4
(square 2) ;; 4
;; ...

تابع square با ارسال مقدار 2 به عنوان پارامتر، همواره مقدار 4 بازمی‌گرداند و از این رو اکنون می‌توانیم آن را با 4 جایگزین کنیم. به همین سادگی، تابع ما اینک دارای شفافیت ارجاعی است.

شفافیت ارجاعی = داده‌های تغییرناپذیر + تابع محض

با استفاده از این مفهوم می‌توانیم کارهای جالبی مانند به‌خاطرسپاری تابع (memoization) انجام دهیم. تصور کنید تابع زیر را داریم:

(+ 3 (+ 5 8))

می‌دانیم که (+ 5 8) برابر با 13 است. این تابع همواره نتیجه‌ای برابر با 13 دارد و از این رو می‌توان نوشت:

(+ 3 13)

و این عبارت نیز همیشه برابر با 16 است. می‌توان یک عبارت را با ثابت عددی جایگزین کرد و آن را memorize کرد.

تابع‌ها به عنوان موجودیت‌های درجه اول

ایده تابع به عنوان یک موجودیت درجه اول این است که با تابع‌ها به عنوان مقدار رفتار کنیم و از آن‌ها به عنوان داده استفاده کنیم. در کلوژر استفاده از defn برای تعریف کردن تابع‌ها امری رایج است؛ اما این کلیدواژه تنها یک جایگزین برای (def foo (fn ...)) است. fn خود تابع را بازمی‌گرداند. Defn یک var بازمی‌گرداند که به شیء تابع اشاره دارد.

تابع به عنوان یک موجودیت درجه اول می‌تواند:

  • از ثابت‌ها و متغیرها، مورد ارجاع قرار گیرد.
  • به عنوان یک پارامتر به تابع‌های دیگر ارسال شود.
  • به عنوان یک خروجی از تابع دیگری بازگشت یابد.

کل ایده به صورت رفتار با تابع‌ها به عنوان مقدار و ارسال تابع‌ها مانند داده است. بدین ترتیب می‌توان تابع‌های مختلف را برای ایجاد تابع‌های جدید با رفتار تازه ترکیب کرد.

تصور کنید تابعی مانند زیر داریم که دو مقدار را جمع می‌زند و سپس مقدار حاصل را دو برابر می‌کند:

(defn double-sum
  [a b]
(* 2 (+ a b)))

اینک تابعی که مقادیر را تفریق کرده و نتیجه را دو برابر کند به صورت زیر است:

(defn double-subtraction
  [a b]
(* 2 (- a b)))

این تابع‌ها منطق مشابهی دارند؛ اما عملیات مختلف انجام می‌دهند. اگر با تابع‌ها به صورت مقدار رفتار کنیم و آن‌ها را به صورت آرگومان ارسال کنیم، می‌توانیم تابعی بسازیم که تابع عملگر را دریافت کرده و آن را درون تابع استفاده می‌کند:

(defn double-operator
  [f a b]
  (* 2 (f a b)))

(double-operator + 3 1) ;; 8
(double-operator - 3 1) ;; 4

اینک تابع f را داریم و می‌توانیم از آن برای پردازش a و b استفاده کنیم. بدین ترتیب تابع‌های + و – را برای ترکیب با تابع double-operator ارسال می‌کنیم و رفتار جدیدی را ایجاد می‌کنیم.

تابع‌های درجه بالاتر

وقتی از تابع‌های درجه بالاتر صحبت می‌کنیم، منظور ما تابعی است که یکی از خصوصیات زیر را دارد:

  • یک یا چند تابع به عنوان آرگومان می‌گیرد یا
  • یک تابع به عنوان مقدار بازگشتی برگرداند.

تابع double-operator که در بخش فوق پیاده‌سازی کردیم، یک تابع درجه بالاتر است، چون یک تابع دیگر را به عنوان آرگومان دریافت کرد و مورد استفاده قرار می‌دهد. احتمالاً قبلاً در مورد filter, map و reduce چیزهایی شنیده‌اید. در ادامه آن‌ها را بررسی می‌کنیم.

Filter

فرض کنید مجموعه‌ای وجود دارد که می‌خواهیم عناصرش را بر اساس یک خصوصیت فیلتر کنیم. تابع فیلتر یک مقدار true یا flase دریافت می‌کند تا تعیین کند که عنصر باید یا نباید در مجموعه حاصل گنجانده شود. اساساً اگر عبارت callback به صورت true باشد، تابع فیلتر آن عنصر را در مجموعه حاصل می‌گنجاند. در غیر این صورت چنین نخواهد کرد. یک مثال ساده از این حالت زمانی است که مجموعه‌ای از اعداد صحیح داریم و می‌خواهیم تنها اعداد زوج را فیلتر کنیم.

رویکرد حتمی

یک روش حتمی برای انجام این کار به صورت زیر است:

  • یک بردار خالی به نام evenNumbers ایجاد کنید
  • روی بردار numbers حلقه‌ای تعریف کنید
  • اعداد زوج را به بردار evenNumbers ارسال کنید
var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var evenNumbers = [];

for (var i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 == 0) {
    evenNumbers.push(numbers[i]);
  }
}

console.log(evenNumbers); // (6) [0, 2, 4, 6, 8, 10]

می‌توان از تابع درجه بالاتر filter نیز برای دریافت تابع even? استفاده کرد و فهرستی از اعداد زوج را بازگشت داد:

(defn filter-array
  [x coll]
  (filter #(> x %) coll))

(filter-array 3 [10 9 8 2 7 5 1 3 0]) ;; (2 1 0)

یک مسئله جالب در این زمینه مسئله فیلتر کردن خروجی است. ایده مسئله آن است که یک آرایه مفروض از اعداد صحیح را فیلتر کنیم و تنها مقادیری را که کمتر از یک مقدار تعیین شده x هستند در خروجی ارائه کنیم.

یک راه‌حل حتمی جاوا اسکریپت برای این مسئله چیزی مانند زیر است:

var filterArray = function(x, coll) {
  var resultArray = [];

  for (var i = 0; i < coll.length; i++) {
    if (coll[i] < x) {
      resultArray.push(coll[i]);
    }
  }

  return resultArray;
}

console.log(filterArray(3, [10, 9, 8, 2, 7, 5, 1, 3, 0])); // (3) [2, 1, 0]

ما کاری که تابع‌مان می‌خواهد انجام دهد را دقیقاً مشخص می‌کنیم، روی مجموعه تکرار می‌کنیم و آیتم کنونی مجموعه را با x مقایسه کرده و اگر شرایط مورد نظر را داشته باشد، آن عنصر را به resultArray ارسال می‌کنیم.

رویکرد اعلانی

اما ما به یک رویکرد اعلانی برای حل این مسئله علاقه‌مند هستیم و بنابراین از تابع درجه بالاتر filter استفاده می‌کنیم. یک را حل اعلانی در کلوژر می‌تواند به صورت زیر باشد:

(defn filter-array
  [x coll]
  (filter #(> x %) coll))

(filter-array 3 [10 9 8 2 7 5 1 3 0]) ;; (2 1 0)

این ساختار ممکن است در وهله نخست تا حدودی عجیب باشد؛ اما درک آن آسان است. #(> x%) صرفاً یک تابع ناشناس است که x را دریافت کرده و آن را با هر عنصر در مجموعه مقایسه می‌کند. % نشان‌دهنده پارامتر تابع ناشناس است که در این مورد عنصر کنونی درون filter است.

این کار به وسیله map نیز قابل اجرا است. تصور کنید یک map از افراد مختلف بر اساس name و age آن‌ها داریم و می‌خواهیم تنها افرادی را فیلتر کنیم که مقدار مشخصی از سن دارند. در این مثال افرادی که بیش از 21 سال دارند را فیلتر کرده‌ایم:

(def people [{:name "TK" :age 26}
             {:name "Kaio" :age 10}
             {:name "Kazumi" :age 30}])

(defn over-age
  [people]
  (filter
    #(< 21 (:age %))
    people))

(over-age people) ;; ({:name "TK", :age 26} {:name "Kazumi", :age 30})

توضیح کد

  • یک فهرست از افراد (با خصوصیت‌های name و age آن‌ها داریم).
  • تابع ناشناس #(< 21 (:age%)) را داریم. به خاطر داشته باشید که % نماینده عنصر کنونی در مجموعه است که در این مورد یک map از افراد است. اگر از (:age {:name "TK":age 26}) استفاده کنیم مقدار سن که در این مورد 26 است را بازمی‌گرداند.
  • همه افراد را بر اساس این تابع ناشناس فیلتر می‌کنیم.

Map

ایده نگاشت یا map تبدیل یک مجموعه است. متد map یک مجموعه را با به‌کارگیری یک تابع روی همه عناصرش و ساخت یک مجموعه جدید از مقادیر بازگشتی، تبدیل می‌کند. اگر مجموعه people فوق را در نظر بگیریم. این بار نمی‌خواهیم افراد را بر اساس سن فیلتر کنیم؛ بلکه می‌خواهیم یک فهرست از رشته‌ها مانند TK is 26 years old داشته باشیم. بنابراین رشته نهایی ممکن است به صورت:name is:age years old باشد که:name و:age خصوصیاتی از هر عنصر در مجموعه people هستند. در روش حتمی جاوا اسکریپت، کد آن به صورت زیر خواهد بود:

var people = [
  { name: "TK", age: 26 },
  { name: "Kaio", age: 10 },
  { name: "Kazumi", age: 30 }
];

var peopleSentences = [];

for (var i = 0; i < people.length; i++) {
  var sentence = people[i].name + " is " + people[i].age + " years old";
  peopleSentences.push(sentence);
}

console.log(peopleSentences); // ['TK is 26 years old', 'Kaio is 10 years old', 'Kazumi is 30 years old']

در روش اعلانی در کلوژر کد به صورت زیر است:

(def people [{:name "TK" :age 26}
             {:name "Kaio" :age 10}
             {:name "Kazumi" :age 30}])

(defn people-sentences
  [people]
  (map
    #(str (:name %) " is " (:age %) " years old")
    people))

(people-sentences people) ;; ("TK is 26 years old" "Kaio is 10 years old" "Kazumi is 30 years old")

ایده کلی کار تبدیل یک مجموعه مفروض به یک مجموعه جدید است. یکی دیگر از مسائلی که می‌توان از این ایده استفاده کرد، مسئله به‌روزرسانی لیست است که در آن می‌خواهیم صرفاً مقادیر یک مجموعه مفروض را با قدر مطلق‌شان به‌روزرسانی کنیم.

برای نمونه ورودی [5 4- 3 2 1] باید در خروجی به صورت [5 4 3 2 1] ارائه شود، چون قدر مطلق 4- به صورت 4 است. یک راه‌حل ساده می‌تواند به‌روزرسانی در جای هر مقدار در مجموعه باشد:

var values = [1, 2, 3, -4, 5];

for (var i = 0; i < values.length; i++) {
  values[i] = Math.abs(values[i]);
}

console.log(values); // [1, 2, 3, 4, 5]

ما می‌توانیم از تابع Math.abs برای تبدیل مقدار به قدر مطلق و اجرای به‌روزرسانی درجا استفاده کنیم. این یک روش تابعی برای پیاده‌سازی این راه‌حل نیست. ابتدا در مورد تغییرناپذیری (immutability) نکاتی را مطرح می‌کنیم. می‌دانیم که تغییرناپذیری برای سازگار و قابل پیش‌بینی‌تر ساختن تابع‌ها امری ضروری محسوب می‌شود. ایده کار، ساخت یک مجموعه جدید با مقادیر قدر مطلق است.

نکته دوم این است که می‌توانیم از map برای تبدیل همه داده‌ها استفاده کنیم. ایده اولیه ما ساخت یک تابع به نام to-absolute برای مدیریت تنها یک مقدار است:

(defn to-absolute
  [n]
  (if (neg? n)
    (* n -1)
    n))

(to-absolute -1) ;; 1
(to-absolute 1)  ;; 1
(to-absolute -2) ;; 2
(to-absolute 0) ;; 0

در این کد مقادیر در صورت منفی بودن به صورت مثبت درمی‌آیند. در غیر این صورت نیازی به تبدیل نیست. اینک که می‌دانیم چطور می‌توانیم قدر مطلق را برای یک مقدار محاسبه کنیم، می‌توانیم از این تابع برای ارسال یک آرگومان به تابع map استفاده کنیم. می‌دانیم که تابع‌های درجه بالاتر می‌توانند تابع دیگری را به عنوان آرگومان دریافت کرده و مورد استفاده قرار دهند. Map نیز چنین تابعی است.

(defn update-list-map
  [coll]
  (map to-absolute coll))

(update-list-map [])               ;; ()
(update-list-map [1 2 3 4 5])      ;; (1 2 3 4 5)
(update-list-map [-1 -2 -3 -4 -5]) ;; (1 2 3 4 5)
(update-list-map [1 -2 3 -4 5]) ;; (1 2 3 4 5)

کد فوق واقعاً عملکرد زیبایی دارد.

Reduce

ایده تابع Reduce دریافت یک تابع و یک مجموعه و بازگشت دادن یک مقدار ایجاد شده با ترکیب کردن آیتم‌ها است. یک نمونه رایج در این مورد دریافت مقدار کلی یک سفارش است. تصور کنید در یک وب‌سایت فروشگاهی هستید و محصول‌های Product 1, Product 2, Product 3 و Product 4 را به سبد خرید خود اضافه می‌کنید (سفارش می‌دهید). اینک می‌خواهیم هزینه کلی سبد خرید را محاسبه کنیم.

در روش حتمی باید روی فهرست سفارش‌ها حلقه‌ای تعریف کنیم و مبلع هر سفارش را به صورت یک مجموع کل جمع بزنیم.

var orders = [
  { productTitle: "Product 1", amount: 10 },
  { productTitle: "Product 2", amount: 30 },
  { productTitle: "Product 3", amount: 20 },
  { productTitle: "Product 4", amount: 60 }
];

var totalAmount = 0;

for (var i = 0; i < orders.length; i++) {
  totalAmount += orders[i].amount;
}

console.log(totalAmount); // 120

با استفاده از reduce می‌توانیم یک تابع بسازیم که مجموع کل (amount sum) را مدیریت کرده و آن را به صورت یک آرگومان به تابع reduce ارسال کند.

(def shopping-cart
  [{ :product-title "Product 1" :amount 10 },
   { :product-title "Product 2" :amount 30 },
   { :product-title "Product 3" :amount 20 },
   { :product-title "Product 4" :amount 60 }])

(defn sum-amount
  [total-amount current-product]
  (+ (:amount current-product) total-amount))

(defn get-total-amount
  [shopping-cart]
  (reduce sum-amount 0 shopping-cart))

(get-total-amount shopping-cart) ;; 120

در این جا shopping-cart، تابع sum-amount که مقدار total-amont کنونی را دریافت می‌کند و شیء current-product که باید به مجموع اضافه شود را داریم. تابع get-total-amount برای reduce کردن سبد خرید (shopping-cart) با استفاده از مجموع کل (sum-amount) و با آغاز 0 استفاده می‌شود.

روش دیگر برای دریافت مجموع کل ترکیب map و reduce است. در این روش از map برای تبدیل سبد خرید (shopping-cart) به مجموعه‌ای از مقادیر amount استفاده می‌شود و سپس از تابع reduce با تابع + استفاده می‌شود.

(def shopping-cart
  [{ :product-title "Product 1" :amount 10 },
   { :product-title "Product 2" :amount 30 },
   { :product-title "Product 3" :amount 20 },
   { :product-title "Product 4" :amount 60 }])

(defn get-amount
  [product]
  (:amount product))

(defn get-total-amount
  [shopping-cart]
  (reduce + (map get-amount shopping-cart)))

(get-total-amount shopping-cart) ;; 120

get-amount شیء محصول را دریافت کرد و تنها مقدار amount را بازمی‌گرداند. بنابراین در این لحظه مقدار [10 30 20 60] را داریم و سپس reduce به ترکیب همه آیتم‌ها با جمع زدن آن‌ها با هم اقدام می‌کند. در ادامه به بررسی طرز کارکرد تابع‌های درجه بالاتر می‌پردازیم. می‌خواهیم نمونه‌ای از چگونگی ترکیب هر سه تابع در مثال خود ارائه کنیم. وقتی از سبد خرید (shopping cart) صحبت می‌کنیم، فهرست زیر از محصول‌ها را در سفارش خود داریم:

(def shopping-cart
  [{ :product-title "Functional Programming" :type "books"      :amount 10 },
   { :product-title "Kindle"                 :type "eletronics" :amount 30 },
   { :product-title "Shoes"                  :type "fashion"    :amount 20 },
{ :product-title "Clean Code" :type "books" :amount 60 }])

می‌خواهیم مقدار کلی مبلغ همه کتاب‌ها را در سبد خرید خود به دست آوریم. الگوریتم کار به صورت زیر است:

  • نوع کتاب‌ها را فیلتر می‌کنیم.
  • سبد خرید را با استفاده از map به مجموعه‌ای از مبالغ تبدیل می‌کنیم.
  • همه آیتم‌ها را با استفاده از reduce با هم جمع می‌کنیم.
(def shopping-cart
  [{ :product-title "Functional Programming" :type "books"      :amount 10 },
   { :product-title "Kindle"                 :type "eletronics" :amount 30 },
   { :product-title "Shoes"                  :type "fashion"    :amount 20 },
   { :product-title "Clean Code"             :type "books"      :amount 60 }])

(defn by-books
  [product]
  (= (:type product) "books"))

(defn get-amount
  [product]
  (:amount product))

(defn get-total-amount
  [shopping-cart]
  (reduce
    +
    (map
      get-amount
      (filter by-books shopping-cart))))
      
(get-total-amount shopping-cart) ;; 70

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۶ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
freecodecamp
۱ دیدگاه برای «برنامه نویسی تابعی (Functional Programming) و مفاهیم مقدماتی آن — به زبان ساده»

سلام
میشه گفت کسانی برنامه نویسی رو دنبال میکنند جاوااسکریپت رو تقریبا بلدند یا باید بلد باشند اما جاوا رو نه بهتر بود تمام تمرکزتون روی جاوا اسکرپت میذاشتید و مثالهاتون رو یکدست از اون میاوردید

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *