در این مقاله از راکت قصد دارم درباره برنامه نویسی فانکشنال در جاوا اسکریپت صحبت کنم؛ زبان جاوا اسکریپت این اجازه رو به ما میدهد که از پارادایمهای مختلف مثل OOP، procedural و فانکشنال استفاده کنیم. تا قبل از ورژن ۸ ،ما فانکشنهای first-class رو در جاوااسکریپ نداشتیم گرچه که میتوانیم first-class فانکشنهارو با کلاسهای anonymous شبیهسازی کنیم. این first-class فانکشنها همان چیزی هستند که برنامه نویسی فانکشنال را در جاوا اسکریپت ممکن میسازند. در فریمورکیهایی مثل ریاکت یا انگولار ،شما با استفاده از ساختار داده تغییرناپذیر (immutable data structures) افزایش عملکرد خواهید داشت. تغییر ناپذیری یا Immutability یکی از اصول اصلی برنامه نویسی فانکشنال است. شما با این اصل در کنار فانکشنهای خالص(pure functions) استدالال برنامهها و دیباگ آنها را راحت میکنید.
قبل از اینکه درباره برنامه نویسی فانکشنال صحبت کنیم و بفهمیم که چی هست، باید درباره چیزهایی که داخل این متد برنامه نویسی وجود ندارند صحبت کنیم، در حقیقت باید درباره تمام ساختارهای این زبان که تاکنون استفاده میکردیم صحبت کنیم و در نهایت با آنها خداحافظی کنیم.
حلقهها
- while
- do...while
- for
- for...of
- for...in
تعریف متغییر با var or let
Void functionها
Object mutation (برای مثال: o.x = 5;)
Array mutator methods
- copyWithin
- fill
- pop
- push
- reverse
- shift
- sort
- splice
- unshift
Map mutator methods
- clear
- delete
- set
تنظیم mutator methods
- add
- clear
- delete
حالا فکر میکنین بدون اینا شدنیه؟ این دقیقاً چیزیه که میخواییم دربارش صحبت کنیم:
فانکشنهای خالص یا Pure functions
صرفا به خاطر اینکه برنامهی شما فانکشن دارد، شما یک برنامهی فانکشنال ندارید. برنامه نویسی فانکشنال بین فانکشنهای خالص و ناخالص تمایز قائل میشود و این شما را ترغیب به داشتن فانکشنهای خالص میکند. یک فاکشن خالص باید دو ویژگی که در ادامه برای شما آوردهام را داشته باشد:
- شفافیت مرجع (Referential transparency): فانکشن همیشه مقداری را که برمیگرداند، برای همهی آرگومانها یکسان است. این به این معنیست که فانکشن نمیتواند به هیچ حالت تغییرپذیری وابسته باشد.
- بدون تاثیرات جانبی (Side-effect free): فانکشن نمیتواند باعث ایجاد هیچ تاثیر جانبی باشد. تأثیرات جانبی ممکن است شامل ورودی/خروجی ( به عنوان مثال نوشتن در کنسول یا فایل لاگ) تغییر یک آبجکت mutable (تغییر پذیر)، تعریف مجدد یک متغییر و … باشد.
خب اجازه دهید با یک مثال برای شما این مسأله را باز کنم. ابتدا با یک فانکش ضرب که مثالی از یک تابع خالص است شروع میکنیم. این همیشه خروجی یکسانی را برای ورودی مشابه برمیگرداند و هیچ گونه تأثیر جانبی نیز ایجاد نمیکند.
function multiply(a, b) {
return a * b;
}
مثالهایی که در ادامه هست نمونههایی از فانکشنهای ناخالص هستند. فانکشن canRide به متغییر heightRequirement که گرفته شده بستگی دارد. متغییرهای گرفته شده لزوماً یک فانکشن خالص نمیسازند اما mutable ها (یا re-assignable ها) هستند. در این مورد از let استفاده شده که به معنای این است که میتواند reassigne شود. فانکشن ضرب ناخالص است؛ چرا که با ورود آن به کنسول تأثیرات جانبی ایجاد میشود.
let heightRequirement = 46;
// Impure because it relies on a mutable (reassignable) variable.
function canRide(height) {
return height >= heightRequirement;
}
// Impure because it causes a side-effect by logging to the console.
function multiply(a, b) {
console.log('Arguments: ', a, b);
return a * b;
}
لیست زیر شامل چندین فانکشن داخلی جاوا اسکریپت است که ناخالص هستند. آیا میتوانید مشخص کنید که کدام یک از این دو ویژگی را برآورده نمیکنند؟
- console.log
- element.addEventListener
- Math.random
- Date.now
- $.ajax (where $ == the Ajax library of your choice)
زندگی در دنیایی که کاملاً با فانکشنهای خالص باشد، خوب خواهد بود؛ اما همانطور که از لیست بالا متوجه شدید، هر برنامهی معنا داری شامل فانکشنهای ناخالص هم خواهد بود. بیشتر اوقات ما نیاز داریم که یک Ajax call ایجاد کنیم، تاریخ فعلی را چک کنیم یا یک عدد رندوم دریافت کنیم. یک قانون کلی و خوب برای این کار این است که از قانون ۲۰/۸۰ پیروی کنید: ۸۰٪ فانکشنهای شما خالص باشد و ۲۰ درصد باقیمانده بر حسب ضروریات ناخالص خواهد بود.
فانکشنهای خالص چندین مزیت دارند:
استدلال و دیباگ آن آسانتر است زیرا به یک حالت تغییر پذیر بستگی ندارد. برای جلوگیری از محاسبه مجدد آن در آینده، مقدار بازگشتی میتواند کش شود تا از محاسبه مجدد در آینده جلوگیری شود.
تست آنها نیز آسانتر است؛ چراکه هیچ گونه وابستگی (از قبیل لاگین، Ajax ، دیتابیس و .. ) ندارد که نیاز به mock یا (تقلید) داشته باشد. اگر فانکشنی که مینویسید یا استفاده میکنید void است( به عنوان مثال هیچ مقدار برگشتی ندارد) این نشانگرخالص بودن آن است.، پس اگر فانکشن هیچ مقدار برگشتی ندارد در این صورت یا هیچ عملیاتی را اجرا نمیکند و یا باعث برخی از تأثیرات جانبی میشود. در همین راستا، اگر فانکشنی را فراخوانی میکنید اما از مقدار برگشتی آن استفاده نمیکنید، احتمالاً برای انجام برخی از تأثیرات جانبی به آن تکیه میکنید و این یک فانکشن ناخالص است.
تغییرناپذیری (Immutability)
بیایید به مفهوم captured variable ها برگردیم. در بالا فانکشن canRide را بررسی کردیم. و به صورت قطعی گفتیم که یک فانکشن ناخالص است، زیرا heightRequirement میتواند reassigne شود. در اینجا یک مثال ساختگی از نحوه reassigne آن با نتایج غیرقابل پیشبینی وجود دارد:
let heightRequirement = 46;
function canRide(height) {
return height >= heightRequirement;
}
// Every half second, set heightRequirement to a random number between 0 and 200.
setInterval(() => heightRequirement = Math.floor(Math.random() * 201), 500);
const mySonsHeight = 47;
// Every half second, check if my son can ride.
// Sometimes it will be true and sometimes it will be false.
setInterval(() => console.log(canRide(mySonsHeight)), 500);
بگذارید دوباره تأکید کنم که captured variable ها لزوماً فانکشن را ناخالص نمیکنند. ما میتوانیم تابع canRide را بازنویسی کنیم تا با تغییر در نحوه اعلان متغییر heightRequirement، خالص شود.
const heightRequirement = 46;
function canRide(height) {
return height >= heightRequirement;
}
اعلان متغییر با const به این معنی است که هیچ فرصتی برای تعیین مجدد آن وجود ندارد و اگر تلاشی برای تعیین مجدد آن انجام شود موتور ما در زمان اجرا خطا میدهد. با این حال اگر به جای یک عدد ساده، یک آبجکت داشته باشیم که تمام ثابتهای ما را ذخیره کند، چه میشود؟
const constants = {
heightRequirement: 46,
// ... other constants go here
};
function canRide(height) {
return height >= constants.heightRequirement;
}
ما از const استفاده کردیم و متغییرها نمیتوانند تغییر مجدد پیدا کنند، اما هنوز هم مشکلی وجود دارد، آبجکت میتواند تغییر کند. همانطور که در کد زیر مشخص است، برای به دست آوردن تغییر ناپذیری واقعی باید از تعیین مجدد متغییر جلوگیری کنید و همچنین به یک ساختار دادهی غیرقابل تغییر نیاز دارید. زبان جاوااسکریپت متد Object.freeze را برای جلوگیری از تغییر یک آبجکت در اختیار ما قرار میدهد.
'use strict';
// CASE 1: The object is mutable and the variable can be reassigned.
let o1 = { foo: 'bar' };
// Mutate the object
o1.foo = 'something different';
// Reassign the variable
o1 = { message: "I'm a completely new object" };
// CASE 2: The object is still mutable but the variable cannot be reassigned.
const o2 = { foo: 'baz' };
// Can still mutate the object
o2.foo = 'Something different, yet again';
// Cannot reassign the variable
// o2 = { message: 'I will cause an error if you uncomment me' }; // Error!
// CASE 3: The object is immutable but the variable can be reassigned.
let o3 = Object.freeze({ foo: "Can't mutate me" });
// Cannot mutate the object
// o3.foo = 'Come on, uncomment me. I dare ya!'; // Error!
// Can still reassign the variable
o3 = { message: "I'm some other object, and I'm even mutable -- so take that!" };
// CASE 4: The object is immutable and the variable cannot be reassigned. This is what we want!!!!!!!!
const o4 = Object.freeze({ foo: 'never going to change me' });
// Cannot mutate the object
// o4.foo = 'talk to the hand' // Error!
// Cannot reassign the variable
// o4 = { message: "ain't gonna happen, sorry" }; // Error
تغییر ناپذیری مربوط به تمام ساختار دادههایی است که شامل آرایهها، مپها و ستها هستند. و این به این معناست که ما نمیتوانیم mutator method هایی مثل array.prototype.push را فراخوانی کنیم چراکه آن آرایه موجود را تغییر میدهد. به جای اینکه آیتم جدیدی را به آرایهی موجود اضافه کنیم، میتوانیم یک آرایه جدید با همان آیتمهای آرایه اصلی به علاوهی یک آیتم اضافی ایجاد کنیم. در حقیقت، همهی mutator method ها میتوانند با تابعی که یک آرایهی جدید با تغییرات دلخواه را برمیگرداند، جایگزین شود.
'use strict';
const a = Object.freeze([4, 5, 6]);
// Instead of: a.push(7, 8, 9);
const b = a.concat(7, 8, 9);
// Instead of: a.pop();
const c = a.slice(0, -1);
// Instead of: a.unshift(1, 2, 3);
const d = [1, 2, 3].concat(a);
// Instead of: a.shift();
const e = a.slice(1);
// Instead of: a.sort(myCompareFunction);
const f = R.sort(myCompareFunction, a); // R = Ramda
// Instead of: a.reverse();
const g = R.reverse(a); // R = Ramda
// Exercise for the reader:
// copyWithin
// fill
// splice
در حین استفاده از map و set نیز همین مورد وجود دارد. ما میتوانیم با برگرداندن یک map و set جدید با تغییرات دلخواه، از mutator method ها اجتناب کنیم.
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
]);
// Instead of: map.set(4, 'four');
const map2 = new Map([...map, [4, 'four']]);
// Instead of: map.delete(1);
const map3 = new Map([...map].filter(([key]) => key !== 1));
// Instead of: map.clear();
const map4 = new Map();
const set = new Set(['A', 'B', 'C']);
// Instead of: set.add('D');
const set2 = new Set([...set, 'D']);
// Instead of: set.delete('B');
const set3 = new Set([...set].filter(key => key !== 'B'));
// Instead of: set.clear();
const set4 = new Set();
دوست دارم این نکته را اضافه کنم که اگر از TypeScript استفاده میکنید و قصد تغییر هر آبجکتی را دارید، میتوانید از اینترفیسهای Readonly<T>, ReadonlyArray<T>, ReadonlyMap<K, V> و ReadonlySet<T> برای گرفتن خطای زمان کامپایل استفاده کنید. اگر Object.freeze را در یک آبجکت خالی یا یک آرایه فراخوانی کنید، کامپایلر به صورت خودکار نتیجه میگیرد که این فقط read-only یا فقط خواندنی است. به دلیل چگونگی نمایش داخلی mapها و setها، فراخوانی Object.freeze در این نوع ساختار داده به همان صورت کار نخواهد کرد. اما این به اندازه کافی آسان است که به کامپایلر بگویید دوست دارید آنها فقط خواندنی باشند.
بسیار خوب، بنابراین ما میتوانیم به جای تغییر، آبجکتهای جدید بسازیم. اما آیا این بر عملکرد تأثیر میگذارد؟ بله، میتواند تأثیر داشته باشد. حتماً تست پرفرمانس را روی اپ خود انجام دهید. اگر به تقویت پرفرمانس نیاز داشتید، استفاده از Immutable.js را در نظر داشته باشید. Immutable.js با استفاده از ساختار دادههای ماندگار، لیستها، استکها، مپها، ستها، و دیگر ساختار دادهها را پیادهسازی میکند. این همان تکنیکی است که توسط زبانهای برنامه نویسی فانکشنال مثل Clojure و Scala استفاده میشود.
// Use in place of `[]`.
const list1 = Immutable.List(['A', 'B', 'C']);
const list2 = list1.push('D', 'E');
console.log([...list1]); // ['A', 'B', 'C']
console.log([...list2]); // ['A', 'B', 'C', 'D', 'E']
// Use in place of `new Map()`
const map1 = Immutable.Map([
['one', 1],
['two', 2],
['three', 3]
]);
const map2 = map1.set('four', 4);
console.log([...map1]); // [['one', 1], ['two', 2], ['three', 3]]
console.log([...map2]); // [['one', 1], ['two', 2], ['three', 3], ['four', 4]]
// Use in place of `new Set()`
const set1 = Immutable.Set([1, 2, 3, 3, 3, 3, 3, 4]);
const set2 = set1.add(5);
console.log([...set1]); // [1, 2, 3, 4]
console.log([...set2]); // [1, 2, 3, 4, 5]
در قسمت بعدی این مقاله دربارهی Function composition و Higher-order function ها صحبت خواهیم کرد. امیدوارم تا اینجا از این بحث برنامه نویسی فانکشنال در جاوا اسکریپت را به خوبی فهمیده باشید و بتوانید از آن استفاده کنید.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید