در قسمت اول یعنی Pure Functions بصورت خاص به شرح توابع Pure در جاوااسکریپت پرداختیم و در این مطلب به شرح مفاهیم پایه برنامه نویسی فانکشنال بصورت خلاصه میپردازیم که شامل عناوین زیر است.
- Pure Functions
- Function Composition
- Shared State
- Race condition
- Immutability
- Side Effects
- Higher Order Functions
- Functors - Containers - Streams
- Declarative vs Imperative
Pure Functions
توابعی هستند که شرایط زیر را داشته باشند:
- به ازای ورودی یکسان همیشه خروجی یکسان تولید کنند.
- اثرات جانبی یا Side effects نداشته باشند.
Function Composition
به فرایند ترکیب دو تابع برای تولید یک تابع جدید گفته میشود. f.g
(نقطه به معنای "ترکیب میشود با:") برابر با در جاوا اسکریپت است. مثال
const inc = n => n + 1;
inc(double(2)); // 5
Shared State
به هر متغیر، شیئ یا فضای حافظه که در یک محدودهای به اشتراک گذاشته شده ویا پراپرتی یک شیئای که بین محدودههای مختلف (scopes) ارسال میشود. یک محدودهی مشترک میتواند شامل global scope یا closure scopes باشد.
Race condition
فرض کنید یک برنامه نوشتید که زمان تایپ در وردی نتایج جستوجو را با یک API call دریافت کرده نمایش میدهد حال فرض کنید جواب درخواستهایی که زمان تایپ یک کلمه ارسال شده است دیرتر از جواب درخواستی که پس از تکمیل نوشتن کلمه مورد نظر برسد، در این شرایط نتیجهای مورد نظر صحیح نمیباشد و کاملا وابسته به توالی و سرعت رسیدن جواب درخواستهای API هست، به این شرایطی که جواب قطعی وجود ندارد و نتیجه به سرعت پاسخگویی چند عامل مختلف وابسته است race condition میگویند.
Immutability
یک شیئ immutable یا غیر قابل تغییری شیئای است که بعد از اینکه ساخته شد غیر قابل تغییر است.
immutability یک مفهووم اصلی در برنامهنویسی فانکشنال است زیرا بدون آن جریان داده در برنامه از دست میرود و با تغییرات در state تاریخچهی آن از دست میرود.
نکته: const
در جاوااسکریپت نباید با immutability اشتباه گرفته شود. const
شیئهای immutable تولید نمیکند بلکه صرفا بعد از مقدار دهی یک شیئ به یک متغیر دیگر نمیتوان مجددا شیئ جدیدی به آن متغیر متصل کرد ولی همچنان پراپرتیهای آن شیئ قابل تغییر میماند.
شیئهای immutable بصورت کلی غیر قابل تغییر هستند. یک مقدار immutable زمانی ساخته میشود که یک شیئ بصورت عمیق freeze شدهاست (به عبارتی همهی پراپرتیهای یک آبجکت در هر سطحی که باشند غیر قابل تغییر باشند.). جاوااسکریپت یک متد وجود دارد که یک شیئ را در یک-سطح freeze میکند.
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object
این شیئ بصورت سطحی immutable است. به مثال زیر توجه کنید که این شیئ در واقع قابل تغییر یا mutable است.
const a = Object.freeze({
foo: { greeting: 'Hello' },
bar: 'world',
baz: '!'
});
a.foo.greeting = 'Goodbye';
console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
// 'Goodbye, world!'
کتابخانههای جاوااسکریپتی مختلفی مثال Immutable.js و Mori وجود دارد که از مزیت درختها استفاده میکنند.
Side Effects
به هرگونه تاثیر خارجی یک تابع بجز آن مقداری که به عنوان خروجی بر میگرداند میگویند، این تاثیر میتواند نوشتن یک لاگ در کنسول باشد یا یک درخواست API Get ساده.
انواع Side Effects
- ایجاد تغییر در هر متغیر یا object عمومی یا global
- لاگ کردن در کنسول با استفاده از console.log
- نوشتن یا نمایش چیزی در صفحهی نمایش
- نوشتن اطلاعات در یک فایل
- ارسال اطلاعات از طریق شبکه
- اجرا کردن هر فرایند خارجی یا عملیات I/O
- فراخوانی هر تابع خارجی دارای Side Effects
قابلیت استفادهی مجدد با Higher Order Functions
higher order functions توابعی هستند که عملیاتی را روی دیگر توابع انجام میدهند بصورتی که یا آنها را به عنوان ورودی میگیرند یا آن توابع را بر میگردانند و یا هر دو حالت. higher order functions اغلب برای موارد زیر استفاده میشوند:
- برای انتزاع و مجزا کردن اکشنها، effects و کنترل جریانهای هم زمان با استفاده از callback functions، promises و monads و غیره
- ساخت ابزارهای کلی یا generic که روی انواع مختلفی از دادهها عملیاتی را انجام میدهند.
- برای اعمال کردن تابع روی بخشی از ورودیها یا تولید یک curried function برای استفادهی مجدد از آن ویا ترکیب دو تابع
- دریافت یک لیستی از توابع به عنوان ورودی و برگرداندن یک نوع ترکیب خاص از آنها
جاوا اسکریپت دارای first class function است که به آن اجازه میدهد با توابع همانند دادهها رفتار کند، آنها را به متغیرها assign کند، آنها را به عنوان ورودی توابع قرار دهد یا آنها را به عنوان خروجی برگرداند.
Functors - Containers - Streams
ساختار دادهای functor یک نوع ساختار دادهای میباشد که میتوان یک نوع نگاشت از روی آن تولید کرد. مانند:[1,2,3].map(x => x*2))
به عبارت دیگر یک container ای است که شامل interface میباشد که از آن طریق میتوان یک تابع را روی مقادیری که نگه میدارد اعمال کرد. بصورت کلی کلمه functor یادآور کلمهی mappable یا قابل نگاشت بودن است.
برای مثال در مورد Array.prototype.map()
، Array
یک container بحساب میآید و علاوه بر Array هر نوع دیگر از ساختار دادهها که API نگاشت کردن یا mapping را پیادهسازی کرده باشند یک functor به حساب میآیند.
استفاده از انتزاعاتی مانند functors و higher order function به منظور ساخت توابع کمکی کلی یا generic که میتوانند تغییراتی در هر تعدادی از انواع مختلف دادهها ایجاد کنند، در برنامهنویسی فانکشنال مهم هستند.
آرایهها و functors تنها مفاهیمی نیستند که شامل یک container و مقادیری که در آن نگه داشته میشود میباشد. برای مثال آرایه یک لیستی از آیتم هاست و stream یک لیستی است که در طول یک بازه زمانی شکل گرفتهاست. بنابراین میتوان از ابزارهای کمکی مشابهی که برای آرایهها و functors تولید کردیم برای پردازش stream های رسیده از رویدادها نیز استفاده نماییم.
Declarative vs Imperative
برنامه نویسی فانکشنال یک پارادایم یا الگوی Declarative است ، به این معنی که منطق برنامه بدون توصیف صریح کنترل جریان بیان میشود.
- Imperative بیانگر How to do things یا چگونه انجام دادن.
برنامههایی که با استفاده از خطوط کد به شرح کنترل جریان یا همان مراحل خاصی که برای رسیدن به نتایج مطلوب مورد استفاده قرار میگیرد imperative هستند.
- Declarative بیانگر What to do یا چه کاری انجام دادن.
برنامههایی که فرایند کنترل جریان را خلاصه میکند و در ازای آن با استفاده از خطوط کد به شرح جریان داده میپردازد.
مثال زیر یک نگاشت imperative را نشان میدهد که یک آرایه را دریافت کرده و خروجی آن یک آرایه جدید است که هر آیتم آن حاصل دوبرابر شدن آیتمهای آرایه ورودی است.
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
نگاشت declarative زیر همان کار برنامه بالا را انجام میدهد با این تفاوت که با استفاده از ابزار فانکشنال Array.prototype.map()
کنترل جریان را خلاصه میکند، که باعث میشود جریان داده واضحتر بیان شود.
const doubleMap = numbers => numbers.map(n => n * 2);
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
کدهای Imperative اغلب از statements استفاده میکنند. یک statement قسمتی از کد است که یک فعالیتی را انجام میدهد مانند: if - for - switch - throw و غیره.
کدهای Declarative بیشتر به expressions متکی هستند. یک expression قسمتی از کد است که مقداری را بر میگرداند. expressions معمولا شامل ترکیبی از توابع و مقادیر و عمگرها هستند که خروجی آنها تولید یک مقدار از نتیجه آن ترکیب است. مانند مثالهای زیر
2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
'a' + 'b' + 'c'
{...a, ...b, ...c}