آشنایی با مفهوم توابع درجه اول در برنامه‌نویسی تابعی
در مقاله «برنامه‌نویسی تابعی چیست و چه کاربردی دارد؟» با پارادیم برنامه‌نویسی تابع‌گرا آشنا شدیم. یک پارادایم برنامه‌نویسی منحصر به‌فرد که روی تغییرناپذیری مقادیر تاکید خاصی دارد. در این شماره با مفهوم توابع درجه اول در برنامه‌نویسی تابع‌گرا آشنا می‌شویم.

برای مطالعه مقاله «برنامه‌نویسی تابعی چیست و چه کاربردی دارد؟» اینجا کلیک کنید.


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

مفهوم تابع درجه اول به این معنا است که برنامه‌نویس با توابع همانند مقادیر کار می‌کند. به عبارت دیگر، از توابع به عنوان نوع‌های داده‌ای استفاده می‌شود. در زبان کلوژر از کلیدواژه 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

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

توابع با درجه بالاتر

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

Filter

فرض کنید مجموعه‌ای دارید و می‌خواهید عناصرش بر مبنای یک ویژگی فیلتر شوند. تابع فیلتر یک مقدار درست یا نادرست دریافت می‌کند تا مشخص کند یک عنصر باید در مجموعه قرار بگیرد یا خیر. اگر 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 even-numbers

  [coll]

  (filter even? coll))

(even-numbers [0 1 2 3 4 5 6 7 8 9 10]) ;; (0 2 4 6 8 10)

یک نکته جالب در این ارتباط فیلتر کردن خروجی است. ایده این است که یک آرایه مفروض از اعداد صحیح را فیلتر کنیم و تنها مقادیری که کمتر از مقدار 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 است. یک راه‌حل اعلانی Clojure می‌تواند به شرح زیر باشد:

(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 و تابع ناشناس به صورت  ((%age:)>)#  داریم. % بیانگر عنصر جاری در مجموعه است که در قطعه کد فوق یک map از افراد است. اگر از تابع {: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']

روش اعلانی که Clojure ارائه می‌کند، به صورت زیر است:

(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")

ایده اصلی این است که یک مجموعه را به مجموعه جدیدی تبدیل کنیم. یکی دیگر از مسائلی که بر مبنای ایده فوق قابل حل است، به‌روزرسانی یک فهرست است که قرار است تنها مقادیر یک مجموعه با قدر مطلق‌شان به‌روزرسانی شوند. به‌طور مثال، ورودی [1 2 3 -4 5] باید در خروجی [1 2 3 4 5] باشد، زیرا قدرمطلق -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 برای تبدیل مقادیر به قدر مطلق و اجرای به‌روزرسانی‌های در محل استفاده کنیم، اما مکانیزم فوق یک روش تابع‌گرا برای حل مسئله نیست و باید به اصل تغییرناپذیری دقت کنیم. همان‌گونه که می‌دانیم تغییرناپذیری با هدف سازگاری و قابل پیش‌بینی‌تر شدن ساخت توابع استفاده می‌شود. به همین دلیل باید مجموعه جدیدی با مقادیر قدر مطلق ایجاد کنیم. البته این امکان وجود دارد که از 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 یک تابع و یک مجموعه را دریافت می‌کند و مقداری که ترکیبی از عناصر ساخته شده را بر می‌گرداند. یک مثال روشن در این ارتباط دریافت مقدار کلی یک سفارش است. فرض کنید، در یک سایت فروشگاهی قرار است محصولات Product1، Product2، 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) را داریم، تابع sun-amount مقدار جاری total-amount را دریافت می‌کند و شی current-product را به sum اضافه می‌کند. تابع get-total-amount برای محاسبه سبد خرید shopping-cart با استفاده از مجموع کل sum-amount و با مقدار آغازین 0 از reduce استفاده می‌کند. روش دیگر برای دریافت مجموع کل ترکیب دو تابع Map و reduce است. در روش فوق از map برای تبدیل سبد خرید به مجموعه‌ای از مقادیر استفاده می‌شود و از تابع 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 با ترکیب همه آیتم‌ها اقدام به جمع‌زدن آن‌ها می‌کند.

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

(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 با هم جمع می‌کنیم.

الگوریتم فوق به شرح زیر پیاده‌سازی می‌شود:

let shoppingCart = [

  { productTitle: "Functional Programming", type: "books", amount: 10 },

  { productTitle: "Kindle", type: "eletronics", amount: 30 },

  { productTitle: "Shoes", type: "fashion", amount: 20 },

  { productTitle: "Clean Code", type: "books", amount: 60 }

]

const byBooks = (order) => order.type == "books";

const getAmount = (order) => order.amount;

const sumAmount = (acc, amount) => acc + amount;

function getTotalAmount(shoppingCart) {

  return shoppingCart

    .filter(byBooks)

    .map(getAmount)

    .reduce(sumAmount, 0);

}

getTotalAmount(shoppingCart); // 70

کلام آخر

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

ماهنامه شبکه را از کجا تهیه کنیم؟
ماهنامه شبکه را می‌توانید از کتابخانه‌های عمومی سراسر کشور و نیز از دکه‌های روزنامه‌فروشی تهیه نمائید.

ثبت اشتراک نسخه کاغذی ماهنامه شبکه     
ثبت اشتراک نسخه آنلاین

 

کتاب الکترونیک +Network راهنمای شبکه‌ها

  • برای دانلود تنها کتاب کامل ترجمه فارسی +Network  اینجا  کلیک کنید.

کتاب الکترونیک دوره مقدماتی آموزش پایتون

  • اگر قصد یادگیری برنامه‌نویسی را دارید ولی هیچ پیش‌زمینه‌ای ندارید اینجا کلیک کنید.

ایسوس

نظر شما چیست؟