مهندس نرمافزار
از Higher-order function تا filter و reduce
تو این نوشته میخوام در مورد نحوهٔ کارکرد Filter و Reduce بنویسم، اما قبل از پرداختن به شیوهٔ کارکرد Filter و Reduce نیاز هست با یه سری از کلمات کلیدی توی پارادایم برنامهنویسی فانکشنال آشنا شیم.
First-class Function
به طور خلاصه اگه بتونین
یک تابع رو به عنوان مقدار به متغییر بدین
تابع رو به عنوان ارگومان به تابع دیگهای بدین
خروجی یک تابع، تابع باشه
در صورتی که زبان برنامهنویسی که باهاش کار میکنید همچین قابلیتهایی داشت در واقع میشه گفت first-class citizen هست. اسم دیگهای از این قابلیت که ممکنه بیشتر شنیده باشید anonymous functions یا lambda expression هست، که اکثریت زبانهای برنامهنویسی همچین قابلیتی رو دارن.
Higher Order Function
هرگاه تابعی، تابع دیگری از ارگومان ورودیش بگیره یا تابعی رو به عنوان مقدار خروجی برگشت بده، یه تابع Higher order یا به اختصار (HOF) هست.
در مقابل HOF مفهوم First-Order Function داریم که نقطه مخالف HOF هست یعنی تابعی که نه تابعی رو به عنوان ارگومان میگیره نه تابعی رو به عنوان خروجی برگشت میده. و این امر واضحیست که برای نوشتن Higher Order Function زبان برنامهنویستون باید قابلیت نوشتن تابع به صورت First class citizen داشته باشه. نوشتن HOF کمک میکنه که منطق عملیاتتون به صورت abstract نوشته بشه. یعنی چی؟ خب باید با مثال جلو بریم.
Filter
فرض کنید یه ارایه از اعداد مختلف دارین و میخواین تمام اعداد فرد رو به عنوان یه ارایهٔ دیگه بدست بیارید.
const getOddNumbers = arr => {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] % 2 != 0) result.push(arr[i]);
}
return result;
};
getOddNumbers([1, 2, 3, 4, 5]); // [1, 3, 5]
خب خیلیم عالی!
حالا فرض کنید نیاز پیدا میکنید که لیست اعداد زوج هم بدست بیارین، راه حلتون برای این کار نوشتن ۹۹٪ همون کد، با یه تغییره؟ اصلا فرض کنید میخواین لیست همهٔ اعداد بخشپذیر بر ۵ رو بدست بیارین بازم اکثریت اون کدها تکرار میشن و فقط بخش اصلی یعنی تشخیص اضافه کردن چه چیزی به ارایهٔ جدید تغییر میکنه. حالا همین کارو با نوشتن یه HOF انجام میدیم.
const filterList = divisibleFunc => arr => {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (divisibleFunc(arr[i])) result.push(divisibleFunc(arr[i]));
}
return result;
};
const divisibleBy = item => value => {
if (value % item == 0) return value;
};
const divisibleBy2 = divisibleBy(2);
const filterListDivisibleBy2 = filterList(divisibleBy2);
filterListDivisibleBy2([1, 2, 3, 4, 5]); // [ 2, 4 ]
تو این کد ما یه تابع کلی داریم که کارش اینکه روی خونههای ارایهمون حرکت کنه، و تابعی که به عنوان ارگومان بهش پاس داده شده روی تک تک اعضای ارایه امتحان میکنه اگه جواب تابع غیر false بود، اون خونه از ارایه رو توی یه ارایه جدید میزاره در نهایت هم ارایه جدیدو به عنوان خروجی برگشت میده.
پایینتر هم یه تابع کلی به اسم divisibleBy نوشتیم که ۲ مقدار ورودی میگیره و چک میکنه مقدار بر ایتم بخشپذیر هست و در صورت درست بودن شرط، مقدار رو برگشت میده.
حالا ما یه تابع میسازیم به اسم divisibleBy2 که عدد ۲ رو به عنوان ورودی به divisibleBy میده و مقدار بازگشتی اون یه تابع دیگه هست که یه مقدار (value) به عنوان ارگومان قبول میکنه و چک میکنه ببینه با ۲ پخشپذیر هست یا نه. در نهایت تابع بدست اومده رو به عنوان ورودی به عنوان تابع ارگومان میده به filterList و خب باز یه تابع برمیگرده که یه ورودی میگیره که ارایه باشه، این خروجی به متغییر به اسم filterListDivisibleBy2 مقداردهی میکنیم و در نهایت با دادن ارایهٔ ورودی بهش خروجی رو میبینیم.
میتونیم ۳ خط آخر به شکل کوتاه شده هم بنویسیم، به این صورت:
filterList(divisibleBy(2))([1, 2, 3, 4, 5]);
ممکنه با خودتون بگین خب این همه دردسر برای چی؟! روش اول که سادهتر بود. تو این روش ما یه filterList داریم که یه فانکشن میگیره و روی تک تک خونههای ارايه شرطشو انجام میده و در صورت درست بودنش یه مقدار برگشتی داره و در نهایت یه لیست میسازه، این فانکشن تو هر نرمافزار دیگهای که نیاز به همچین کاری باشه میتونه بدون هیچ تغییری قابل استفاده باشه! تازه! اگه با جاوااسکریپت کار کرده باشید احتمالا با این تابع خیلی سر و کار داشتید در واقع filterList که نوشتیم، متد filter پروتوتایپ Array جاوااسکریپت هست با یک تفاوت کوچیک، مقداره ورودیهای ما به divisibleFunc با مقدار ارگومانهای filter جاوااسکریپت یکم فرق دارن.
شکل دیگهای که میشه نوشت به این صورت:
filterList(value => {
if (value % 2 == 0) return value;
})([1, 2, 3, 4, 5]);
توجه کنید که HOF یک ویژگی خاص تو جاوااسکریپت یا هر زبان دیگه نیست، در واقع HOF یک مفهوم کلیست برای نوشتن منطق کارهاتون به صورت انتزاعیتر که تو برنامهنویسی فانکشنال بیشتر شناخته شدست و ازش استفاده میکنن. نکته مهمی که به نظر من در مورد یادگیری برنامهنویسی یا مهندسی نرمافزار وجود داره، درک همین مفاهیم اصلی هستند که شما رو برنامهنویس بهتر میکنه و هیچ راهحل همه فن حریف برای همه شرایط وجود نداره، برنامهنویس کسیست که با شناخت و درک مسئله و شرایط تصمیم و راهحل بهتر انتخاب کنه، گاهی ممکنه مسئله با دیدگاه شئگرایی بهتر حل شه، گاهی با فانکشنال و ... یا حتی تلفیقی از راهحلهای موجود، بهترین انتخاب باشه.
همونطور که گفتم این مفهوم هرجای میتونه مورد استفاده قرار بگیره به شرطی که اون زبان قابلیت first-class function داشته باشه، برای مثال مدتی قبل گذرا یه تیکه کد جاوا مخصوص پلتفرم اندروید رو چک میکردم که همش یه کاری تکراری انجام میداد با این تفاوت که فقط یه نقطه از اون کد تغییر پیدا میکرد، نقطه شروع این بود که یه فانکشن بنویسم که یه فانکشن و یه ورودی دیگه به عنوان ارگومان میگرفت و داخل بدنهش، ورودی دوم رو به عنوان پارامتر به فانکشنی که از ارگومان اولی گرفته بودم پاس میداد. حالا میتونستم هر فانکشنی بنویسم و که همیشه هم یه ورودی بهش داده میشه. تو توییت پایینی میتونین بیشتر در موردش بخونین.
خب اگه تا اینجا رو گرفته باشید در واقع شما مفهوم higher-order-function رو درک کردید و تابع پرکاربرد filter هم پیادهسازی کردین.
میریم سراغ اصل مطلب یعنی Reduce
Reduce
ابزار reduce (fold هم شناخته میشه) به معنی کمکنندست ولی در واقع میتونیم ترکیبکننده هم بهش بگیم، معمولا ۳ ارگومان میگیره یکیش ارایهس یکیش تابع ریدوسر و بعدی هم مقدار اولیس (دلخواهی)، reduce روی اعضای ارایهٔ حرکت میکنه و تابع ریدوسر روی اعضا انجام میده و خروجی اون رو توی متغییر به اسم accumulator میریزه، همین کارو تا وقتی که اعضای ارایه تموم بشن ادامه میده و در نهایت مقدار accumulator رو برگشت میده.
با مثال ببینیمش قبل از استفاده از reduce مثال غیرفانکشنالش رو بنویسیم، میخوایم مقدار خونههای ارایه رو باهم جمع کنیم.
const summation = arr => {
let acc = 0;
for (let i = 0; i < arr.length; i++) acc += arr[i];
return acc;
};
خب حالا بازم فرض کنید نیاز دارین ضرب همهٔ خونههای ارایه رو بدست بیارید، بازم اکثریت کد تکرار میشه به جز بخش اصلی یعنی logic که شما میخواید رو خونههای ارایه انجام بشه و فقط یه خروجی داشته باشه. واسه همچین شرایطی reduce استفاده میشه. البته reduce به آچر فرانسه مشهوره :دی کارهای زیادی رو میشه با reduce انجام داد حتی عملیاتی مثل map و یا filter میشه با reduce انجام داد.
خب پیاده سازی reduce به این صورت هست.
const reduce = function(arr, reduceFn, accumulator = 0) {
for (let i = 0; i < arr.length; i++) {
accumulator = reduceFn(accumulator, arr[i], i, arr);
}
return accumulator;
};
و برای استفاده از این تابع برای مثال جمع اعداد ارایه اینطوری عمل میکنیم.
reduce([1, 2, 3, 4, 5], (acc, value) => acc + value); //15
تفاوت متد reduce پروتوتایپ Array فقط در ارگومان اول هست، اونجا دسترسی به خود ارایه از طریق this هست.
همونطور که گفته شده reduce مثل چاقوی سویسی هست و کارهای زیاده رو میشه باهاش انجام داد نمونههای دیگه هم پیادهسازی توابع compose و pipe با reduce هست.
در انتها من ویدیوی در مورد نحوهٔ استفاده از متد reduce برای درس طراحی صفحات وب ضبط کرده بودم که قرار میدم.
* تو ویدیو اشاره میکنم که راه فانکشنال برای پیادهسازی reduce بجای استفاده از for باید از map استفاده کرد، که اشتباهه! باید میگفتم استفاده از recursion هست.
چند تا مقاله برای کاربردهای مختلف در استفاده از reduce خونده بودم، لینکهاشو میزارم شاید مفید بود.
منابع:
مطلبی دیگر از این انتشارات
متد کاربردی Object.select
مطلبی دیگر از این انتشارات
آشنایی با Symfony با ساختن یه وبلاگ به صورت پروژه ای
مطلبی دیگر از این انتشارات
اولین قدم با اسکریپی