برنامه نویسی فانکشنال در جاوا اسکریپت - بخش سوم

ترجمه و تالیف : فاطمه شیرزادفر
تاریخ انتشار : 26 آبان 99
خواندن در 3 دقیقه
دسته بندی ها : جاوا اسکریپت

در این مقاله از راکت قصد دارم درباره برنامه نویسی فانکشنال در جاوا اسکریپت صحبت کنم؛ زبان جاوا اسکریپت این اجازه رو به ما می‌دهد که از پارادایم‌های مختلف مثل OOP، procedural و فانکشنال استفاده کنیم. در دو بخش قبلی این مقاله بخشی از برنامه نویسی فانکشنال در جاوا اسکریپت را بررسی کردیم در این مقاله به ادامه آن می‌پردازیم و این مبحث را به پایان می‌رسانیم.

فانکشن‌های Higher-order

ما از قبل می‌دانیم که جاوا اسکریپت دارای فانکشن‌های First-class  می‌باشد، که می‌تواند مانند هر مقدار دیگری pass شود. پس تعجب‌آور نیست که بتوانیم یک فانکشن را به‌ فانکشن دیگر pass کنیم؛ همچنین می‌توانیم یک فانکشن را از فانکشن دیگر return کنیم. و بفرمایید! حالا فانکشن‌هایhigher order داریم. شما احتمالاً از قبل با چندین فانکشن higher order که در Array.prototype موجود هستند آشنا هستید. برای مثال، filter، map و reduce و موارد دیگر. یکی از راه‌های دیگر، برای فکر کردن به فانکشن higher-order این است: تابعی است که یک return function را می‌پذیرد( که معمولا)callback function نامیده می‌شود. بیایید مثالی از استفاده‌ی فانکشن‌های built-in higher-order را بررسی کنیم:

const vehicles = [
  { make: 'Honda', model: 'CR-V', type: 'suv', price: 24045 },
  { make: 'Honda', model: 'Accord', type: 'sedan', price: 22455 },
  { make: 'Mazda', model: 'Mazda 6', type: 'sedan', price: 24195 },
  { make: 'Mazda', model: 'CX-9', type: 'suv', price: 31520 },
  { make: 'Toyota', model: '4Runner', type: 'suv', price: 34210 },
  { make: 'Toyota', model: 'Sequoia', type: 'suv', price: 45560 },
  { make: 'Toyota', model: 'Tacoma', type: 'truck', price: 24320 },
  { make: 'Ford', model: 'F-150', type: 'truck', price: 27110 },
  { make: 'Ford', model: 'Fusion', type: 'sedan', price: 22120 },
  { make: 'Ford', model: 'Explorer', type: 'suv', price: 31660 }
];

const averageSUVPrice = vehicles
  .filter(v => v.type === 'suv')
  .map(v => v.price)
  .reduce((sum, price, i, array) => sum + price / array.length, 0);

console.log(averageSUVPrice); // 33399

توجه داشته باشید که ما متدها را در یک array object فراخوانی کردیم، که مشخصه برنامه نویسی شی‌گرا است. اگر می‌خواستیم این را کمی فانکشنال تر کنیم، می‌‌توانستیم از فانکشن‌های آماده شده توسط Ramda یا lodash/fp به جای آن استفاده کنیم. همچنین می‌توانستیم از فانکشن‌های مرکب یا همان function composition که قبلاً درباره آن صحبت کردیم، استفاده کنیم. توجه داشته باشید که اگر از R.compose استفاده کردیم باید ترتیب فانکشن‌ها را معکوس کنیم، چراکه این از راست به چپ به فانکشن‌ اعمال می‌شود ( برای مثال، از پایین به بالا)؛ به هرحال اگر می‌خواهید آن‌ها از چپ به راست اعمال شوند ( برای مثال از بالا به پایین)، می‌توانید از R.pipe استفاد کنید. هر دو مثال با استفاده از Ramda است؛ توجه داشته باشید که Ramda دارای یک mean function  است که می‌توان به جای reduce از آن استفاده کرد.

const vehicles = [
  { make: 'Honda', model: 'CR-V', type: 'suv', price: 24045 },
  { make: 'Honda', model: 'Accord', type: 'sedan', price: 22455 },
  { make: 'Mazda', model: 'Mazda 6', type: 'sedan', price: 24195 },
  { make: 'Mazda', model: 'CX-9', type: 'suv', price: 31520 },
  { make: 'Toyota', model: '4Runner', type: 'suv', price: 34210 },
  { make: 'Toyota', model: 'Sequoia', type: 'suv', price: 45560 },
  { make: 'Toyota', model: 'Tacoma', type: 'truck', price: 24320 },
  { make: 'Ford', model: 'F-150', type: 'truck', price: 27110 },
  { make: 'Ford', model: 'Fusion', type: 'sedan', price: 22120 },
  { make: 'Ford', model: 'Explorer', type: 'suv', price: 31660 }
];

// Using `pipe` executes the functions from top-to-bottom. 
const averageSUVPrice1 = R.pipe(
  R.filter(v => v.type === 'suv'),
  R.map(v => v.price),
  R.mean
)(vehicles);

console.log(averageSUVPrice1); // 33399

// Using `compose` executes the functions from bottom-to-top.
const averageSUVPrice2 = R.compose(
  R.mean,
  R.map(v => v.price),
  R.filter(v => v.type === 'suv')
)(vehicles);

console.log(averageSUVPrice2); // 33399

مزیت برنامه نویسی فانکشنال این است که داده (مثل vehicles) را به طور واضحی از منطق‌( مثل فانکشن‌های filter، map و reduce) جدا می‌کند. این را با کد شی‌گرا مقایسه کنید که داده و فانکشن‌ها را به فریم آبجکت‌ها با متدها ترکیب می‌کند.

Currying

به طور معمول، currying یک پروسه از گرفتن یک فانکشن است که n آرگومان می‌پذیرد و آن را به n فانکشن تبدیل می‌کند که هرکدام یک آرگومان دریافت می‌کنند. تعداد آٰرگومان‌های یک فانکشن تعداد آرگومان‌هایی است که می‌پذیرد. فانکشنی که یک آرگومان قبول کند unary، دو آرگومان binary، سه آرگومان ternary و n آرگومان n-ary است. بنابراین می‌توانیم currying را، پروسه‌ی گرفتن یک فانکشن n-ary و تبدیل آن به فانکشن‌های n unary تعریف کنیم.

بیایید با یک مثال ساده شروع کنیم، حل معادله خط یک تابع برداری یا همان وکتوری. جبر خطی را به یادآورید، جواب معادله دو بردار [a, b, c] و [x, y, z] برابر است با ax + by + cz .

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

console.log(dot(v1, v2)); // 1(4) + 3(-2) + (-5)(-1) = 4 - 6 + 5 = 3

فانکشن dot باینری است چرا که دو آرگومان می‌پذیرد. اما همانطور که در مثال می‌بینیم، می‌توانیم آن را به صورت دستی به دو فانکشن unary تبدیل کنیم. توجه کنید که curriedDot یک تابع unary است که یک بردار را دریافت می‌کند و سپس یک فانکشن unary دیگر را return می‌کند که آن، بردار دوم را دریافت می‌کند.

function curriedDot(vector1) {
  return function(vector2) {
    return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
  }
}

// Taking the dot product of any vector with [1, 1, 1]
// is equivalent to summing up the elements of the other vector.
const sumElements = curriedDot([1, 1, 1]);

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

خوشبختانه، ما مجبور نیستیم هر یک از فانکشن هایمان را به صورت دستی تبدیل کنیم. کتابخانه‌هایی مثل Ramda و lodash فانکشن‌هایی دارند که این کار را برای ما انجام می‌دهند. در حقیقت، آن‌ها یک نوع ترکیبی از currying را انجام می‌دهند، که شما همزمان هم می‌توانید فانکشن را با یک آرگومان فراخوانی کنید، و هم می‌توانید به طور همزمان همه آرگومان ها را pass کنید. دقیقاً مثل ورژن اصلی آن.

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

// Use Ramda to do the currying for us!
const curriedDot = R.curry(dot);

const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements(v1)); // -1
console.log(sumElements(v2)); // 1

// This works! You can still call the curried function with two arguments.
console.log(curriedDot(v1, v2)); // 3

هم Ramda و هم lodash به شما این امکان را می‌دهند که از یک آرگومان و تعیین آن رد شوید و بعداً آن را مشخص کنید. آن‌ها این کار را با استفاده از placeholder انجام می‌دهند. از آن‌جا که ضرب برداری قابلیت جابه جایی دارد، پس تفاوت نمی‌کند که به چه ترتیبی بردارها را به فانکشن pass کنیم. بیایید از یک مثال متفاوت برای نمایش placeholder استفاده کنیم. Ramda از دو underscore (ـ) به عنوان placeholder استفاده می‌کند.

const giveMe3 = R.curry(function(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
});

const giveMe2 = giveMe3(R.__, R.__, 'French Hens');   // Specify the third argument.
const giveMe1 = giveMe2('Partridge in a Pear Tree');  // This will go in the first slot.
const result = giveMe1('Turtle Doves');               // Finally fill in the second argument.

console.log(result);
// 1: Partridge in a Pear Tree
// 2: Turtle Doves
// 3: French Hens

و آخرین نکته قبل از اینکه بحث currying را ببندیم، partial application است. partial application و currying اغلب دست در دست هم هستند، گرچه که مفاهیم جداگانه‌ای دارند. یک curried فانکشن حتی اگر هیچ آرگومانی هم نگیرد ولی هنوز هم curried فانکشن است. از طرف دیگر،Partial application وقتی است که یک فانکشن چیزهایی را دریافت کرده‌ است، اما نه همه‌ی آرگومان‌ها را. Currying اغلب برای انجام partial application مورد استفاده قرار می‌گیرد اما خب این تنها راه نیست.

زبان جاوا اسکریپت دارای مکانیزم داخلی یا built-in برای انجام partial application بدون currying است. این کار با استفاده از متد function.prototype.bind انجام می‌شود. یکی از روش‌های منحصر به فرد این متد این است که شما نیاز دارید تا مقدار آن را به عنوان اولین آرگومان pass کنید. اگر از برنامه نویسی شی‌گرا استفاده نمی‌کنید، پس می‌توانید آن را null بگیرید.

function giveMe3(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
}

const giveMe2 = giveMe3.bind(null, 'rock');
const giveMe1 = giveMe2.bind(null, 'paper');
const result = giveMe1('scissors');

console.log(result);
// 1: rock
// 2: paper
// 3: scissors

خب و تمام شد!

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

منبع

گردآوری و تالیف فاطمه شیرزادفر
آفلاین
user-avatar

تجربه کلمه‌ای هست که همه برای توصیف اشتباهاتشون ازش استفاده میکنن، و من همیشه دنبال اشتباهات جدیدم! برنامه‌نویس هستم و لینوکس‌ دوست

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید