در مجموعه مقالات “برای مصاحبهی کاری در سمت برنامه نویس وب، باید چه مواردی را بدانید؟” سعی کردهایم سوالات متداول دربارهی زبانهای برنامه نویسی وب را که در مصاحبههای واقعی از شما میپرسند را بررسی و آموزش دهیم. این سوالات در دستههای متفاوت و بزرگی قرار دارند که در هر دسته به مهمترین ویژگیها میپردازیم. برای مثال در دستهی “ از جاوا اسکریپت چه میدانید؟” ، هدف ما آموزش کامل این زبان نیست بلکه صرفا بررسی نکات و ویژگیهای منحصر به فرد این زبان است. پس با اولین قسمت از این سری مقالات با موضوع functional programing در جاوا اسکریپت، همراه چ یاب باشید:
در سالهای گذشته functional programing برای برنامه نویس وب که با زبان جاوا اسکریپت کار میکنند به موضوع داغی تبدیل شده است طوریکه تا قبل از این افراد زیادی موقع آموزش زبانهای برنامه نویسی وب، از Functional programing چیزی نشنیده بودند، اما این روزها بیشتر کدهای زبانهای فانکشنال، با اتکا به این تکنیک نوشته میشود.
تعریف Functional programming :
Functional programming (یا به اختصار FP)، پروسهی تولید نرمافزار توسط برنامه نویس وب با استفاده از ترکیب توابع است. در این فرآیند از ایجاد shared state, mutable dataو side effects جلوگیری میشود.
FP در واقع یک الگوی برنامهنویسی است به این معنی که برنامه نویس وب باید موقع آموزش و استفاده از زبانهای برنامه نویسی وب دربارهی نحوهی ساخت یک برنامه، بر اساس یکسری اصول بنیادی (fundamental) که از تغییر حالت و دادههای متغییرها جلوگیری میکند، تفکر کند.
از دیگر الگوهای برنامهنویسی میتوان به برنامهنویسی شیگرا و برنامهنویسی رویهای(procedural programming) اشاره کرد. البته به کارگیری این تکنیکها به زبانهای برنامه نویسی وب ای که استفاده میکنید بستگی دارد.
functional programing بسیاری از کارها مثل تست، نگهداری و تغییر در کد برنامه را برای برنامه نویس وب آسان کرده است اما اگر با این روش آشنا نباشید، در ابتدای کار ممکن است شما را بترساند. باید بگویم نگران نباشید، برای یاد گرفتن این الگو از جای خوبی شروع کردهاید و صد البته که باید این مژده را به شما بدهم که در صورت یادگیری این الگو و به کارگیری آن، از برنامهنویسی جاوا اسکریپت لذت چندصدبرابر خواهید برد. پس بدون ترس و نگرانی و با کفشهای آهنی، شروع کنید.
قبل از اینکه بتوانید مبحث تخصصی مثل functional programing را درک کنید، بهتر است در ابتدا واژگان مرتبط با این الگو را با هم بررسی کنیم. در واقع اینها مفاهیم اصلی در FP هستند.
واژگان اصلی Functional Programing :
- Pure functions
- Function composition
- Avoid shared state
- Avoid mutating state
- Avoid side effects
-
Pure function
به تابعی pure function میگویند که :
- برای ورودیهای مشابه، خروجی یکسانی برگرداند
- هیچ تاثیر جانبی(side-effects ) نداشته باشد
مثال:
میخواهیم تابعی بنویسیم که محیط یک دایره را حساب کند. این تابع را درحالت impure و pure محاسبه میکنیم:( قبل از اینکه به کدها نگاه کنید، پیشنهاد میکنم به عنوان یک برنامه نویس وب تواناییهای خودتان را بسنجید. قبل از اینکه در روز مصاحبه تواناییهای شما را بسنجند!)
let PI = 3.14;
const calculateArea = (radius) => radius * radius * PI;
calculateArea(10); // returns 314.0
چرا این تابع impure است؟ چون ازglobal objectای استفاده میکند که به عنوان پارامتر به تابع ارسال نشده است. حالا فرض کنید بنا به دلایلی مقدار global object را به ۴۲ تغییر میدهیم. خروجی تابع ما به ازای پارامتر یکسان(radius=10) متفاوت و برابر با ۴۲۰۰ میشود. پس اجازه بدهید این مشکل را با کمک functional programing حل کنیم:
let PI = 3.14;
const calculateArea = (radius, pi) => radius * radius * pi;
calculateArea(10, PI); // returns 314.0
همانطور که مشاهده میکنید مقدار PI را به عنوان پارامتر تابع به آن پاس دادیم. پس برای مثال اگر PI=3.14 و radius=10 باشد، نتیجه همواره برابر ۳۱۴ است و اگر PI=42 باشد، نتیجه همواره برابر ۴۲۰۰ خواهد بود.
مثال:
در ادامه میخواهیم اطلاعات یک فایل را بخوانیم. اگر عملکرد تابع ما وابسته به محتوای فایلی باشد که میخواند، نتیجه بسته به محتوای فایل متغییر خواهد بود. پس در این حالت نیز تابع ما Pure نخواهد بود
const charactersCounter = (text) => `Character count: ${text.length}`;
function analyzeFile(filename) {
let fileContent = open(filename);
return charactersCounter(fileContent);
}
مثال :
تغییر global object یا متغییری که با refrence به تابع پاس داده شده است منجر به side effect میشود. برای مثال میخواهیم تابعی پیاده سازی کنیم که یک عدد int میگیرد و یک واحد آن را افزایش میدهد:
let counter = 1;
function increaseCounter(value) {
counter = value + 1;
}
increaseCounter(counter);
console.log(counter); // 2
در مثال بالا ما یک متغییر counter داریم. این متغییر را به عنوان پارامتر به تابع impure پاس میدهیم، این تابع مقدار جدیدی به counter میدهد. همانطور که مشاهده میکنید اگر مقدار counter را بعد از فراخوانی تابع و به طور مستقیم چاپ کنیم، مقدار counter عوض شده است! پس تابع ما روی متغییر گلوبال تاثیر گذاشته است.
let counter = 1;
const increaseCounter = (value) => value + 1;
increaseCounter(counter); // 2
console.log(counter); // 1
اگر به کد بالا دقت کنید، متوجه خواهید شد با کمک functional programing از ایجاد side effect جلوگیری کردیم، در نتیجه مقدار counter هنگامی که تابع را فراخوانی میکنیم و هنگامیکه به طور مستقیم آن را در کنسول چاپ میکنیم، متفاوت خواهد بود. یعنی تابع ما روی مقدار اصلی counter تاثیری نگذاشته است.
-
Function composition
به فرآیندی گفته میشود که در آن دو یا چند تابع را با هم ترکیب میکنند تا در نهایت تابع جدیدی ایجاد شود.
-
Avoid shared state
Shared state یا حالت اشتراکی، هر متغیر، شی (object ) یا فضای حافظه است که در اسکوپ مشترک وجود دارد یا به عنوان ویژگی(property) یک شی بین اسکوپها ارسال میشود. به زبان ساده حالت اشتراکی مثل باجهی تلفن همگانی است! افراد زیادی به آن دسترسی دارند و مختص یک فرد خاص نیست. global scope و Closure scope مثالهایی از حالت اشتراکی در برنامهنویسی هستند.
FP از ایجاد حالت مشترک، که در بسیاری از موارد دردسرساز است و حتی امنیت برنامه را به خطر میاندازد، جلوگیری میکند در عوض روی ساختار دادههای غیر قابل تغییر(immutable data structures ) و محاسبات خالص(pure calculations ) برای ایجاد دادههای جدید از دادههای موجود، تمرکز دارد.
بزرگترین مشکلی که حالت اشتراکی ایجاد میکند این است که برای پیبردن به عملکرد و نتیجه یک تابع باید کل تاریخچه متغیرهای اشتراکی که از آنها استفاده میکند و یا بر روی آنها اثر میگذارد را بررسی کنیم.
برای اینکه این مطلب بهتر جا بیوفتد، اجازه بدید با یک مثال توضیح بدهیم. یکی از مشکلات رایجی که حالت اشتراکی ایجاد میکند این است که تغییر ترتیب فراخوانی توابع، نتیجه نهایی را تغییر میدهد.
مثال:
// With shared state, the order in which function calls are made
// changes the result of the function calls.
const x = {
val: 2
};
const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x1();
x2();
console.log(x.val); // 6
// This example is exactly equivalent to the above, except...
const y = {
val: 2
};
const y1 = () => y.val += 1;
const y2 = () => y.val *= 2;
// ...the order of the function calls is reversed...
y2();
y1();
// ... which changes the resulting value:
console.log(y.val); // 5
در واقع با FP از ایجاد همچین مشکلی، جلوگیری میکنیم:
const x = {
val: 2
};
const x1 = x => Object.assign({}, x, { val: x.val + 1});
const x2 = x => Object.assign({}, x, { val: x.val * 2});
console.log(x1(x2(x)).val); // 5
const y = {
val: 2
};
// Since there are no dependencies on outside variables,
// we don't need different functions to operate on different
// variables.
// this space intentionally left blank
// Because the functions don't mutate, you can call these
// functions as many times as you want, in any order,
// without changing the result of other function calls.
x2(y);
x1(y);
console.log(x1(x2(y)).val); // 5
در مثال بالا از Object.assign() استفاده کردیم و یک شی خالی به عنوان اولین پارامتر به آن ارسال کردهایم تا ویژگی x را در شی خالی کپی کند نه اینکه مقدار x را تغییر بدهد. این روش معادل این است که یک شی را از ابتدا(from scratch ) و بدون استفاده از متد Object.assign() بسازیم. باید تذکر داد که این یک الگوی رایج برای ایجاد یک کپی از state موجود به جای تغییر و دستکاری آن است(منظورمان کد اول است).
شاید نیاز باشد دربارهی خط console.log(x1(x2(x))).val بیشتر توضیح بدهیم. در اینجا ثابت x به عنوان پارامتر ورودی به تابع x2 ارسال شده است. خروجی این فراخوانی به عنوان آرگومان ورودی به تابع x1 ارسال شده است و در نهایت مقدار آن را چاپ میکند.
صد البته اگر شما ترتیب فراخوانی توابع را تغییر دهید و ((x2(x1(x را اجرا کنید، نتیجه متفاوت خواهد بود اما مسئلهی مهم و قابل ذکر این است که دیگر نگران تغییر مقدار متغییرها در خارج از توابع نیستیم چون این متغییرها دستخوش تغییر نمیشوند و این یک دستاورد بزرگ است.
-
Avoid mutating state
immutable object به شیای گفته میشود که بعد از اینکه ایجاد شد امکان تغییر آن وجود ندارد و در مقابل mutable object به شیای گفته میشود که بعد از ایجاد، امکان اصلاح آن وجود دارد.
Immutability (تغییرناپذیری) یک مفهوم اصلی در FP است، چون بدون این مفهوم جریان دادهها(data flow ) در برنامه از بین میرود. در واقع تاریخچه یک state را از دست میدهیم و نمیتوانیم بفهمیم یک state طی چه تغییراتی، مقدار فعلی خود را بدست آورده است.
مثال:
var values = [1, 2, 3, 4, 5];
var sumOfValues = 0;
for (var i = 0; i < values.length; i++) {
sumOfValues += values[i];
}
sumOfValues // 15
در کد بالا، در هر دور از حلقه for، مقدار state i و somOfValue را تغییر میدهیم. سوالی که پیش میآید این است: چطور میتوانیم در حلقهها از ایجاد mutable state ها جلوگیری کنیم؟ با کمک recursion
let list = [1, 2, 3, 4, 5];
let accumulator = 0;
function sum(list, accumulator) {
if (list.length == 0) {
return accumulator;
}
return sum(list.slice(1), accumulator + list[0]);
}
sum(list, accumulator); // 15
list; // [1, 2, 3, 4, 5]
accumulator; // 0
در کد بالا تابع sum یک وکتور از اعداد را دریافت میکند. این تابع تا زمانیکه لیست اعداد خالی شود، خودش را فراخوانی میکند و در هر بار فراخوانی مقدار عدد جاری به accumulator اضافه میشود. در واقع با کمک recursion میتوانیم متغییرهایمان را immutable (تغییر ناپذیر) نگهداریم. همانطور که میبینید در نهایت list و accumulator تغییر نکردند.
** همین تابع را با کمک reduce میتوانیم بنویسیم، که در مبحث higher order functions آن را پوشش خواهیم داد.
البته باید بگوییم در جاوااسکریپت نباید مفهوم تغییر ناپذیری را با const اشتباه بگیرید. با کمک const میتوانیم یک نام متغییر ایجاد و به آن مقدار بدهیم. باید توجه داشته باشید، مقدار ثابتی که با کمک const تعریف کردهاید، بعد از تعریف قابل تغییر نیست. فرض کنید با کمک const یک شی ایجاد کردهایم. در این صورت خود شی به طور مستقیم قابل تغییر نیست اما ویژگیهای آن را میتوانیم تغییر دهیم. پس با این تعاریف const شی غیرقابل تغییر (immutable objects) ایجاد نمیکند.
مثال:
const person = {
firstName:"John",
lastName:"Doe"
};
person =["Elham","Bigdeli"];
console.log(person);//error
person.firstName="Elham";
person.lastName="Bigdeli";
console.log(person);//it's ok
اشیا غیرقابل تغییر(Immutable objects ) به هیچ وجه قابل تغییر نیستند. جاوااسکریپت با کمک freeze() امکان ایجاد Immutable objects را به ما میدهد.
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object
اما اشیای freeze شده صرفا در سطح غیرقابل تغییر هستند. برای اینکه این مطلب بهتر جا بیوفتد لطفا به کد زیر نگاه کنید:
مثال:
const a = Object.freeze({
foo: { greeting: 'Hello' },
bar: 'world',
baz: '!'
});
a.foo.greeting = 'Goodbye';
console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
پس با این حساب freeze هم نمیتواند کل شی را غیرقابل تغییر کند. مگر اینکه در تعریف شی، آن را در تمام سطوح freeze کنیم!
باید بدانید در بسیاری از زبانهای برنامهنویسی، یک ساختمان داده خاص و غیر قابل تغییر به نام trie وجود دارد. Trie به طور موثر جسم را freeze میکند، به این معنی که فارغ از سطحی که property قرار دارد، اجازه و امکان تغییر آن وجود ندارد. چندین کتابخانه در جاوااسکریپت وجود دارد که از trie استفاده میکنند، مثل Immutable.js و Mori .
-
Avoid side effects
عارضه جانبی(side effects) مساوی است با ایجاد هر تغییری در state برنامه که در خارج تابع فراخوانی شده، آن تغییر قابل مشاهده باشد. Side effects شامل موارد زیر است:
- تغییر هر متغییر خارجی یا ویژگی شی( به عنوان مثال تغییر متغییر global باعث ایجاد تغییر در تمام مواردی میشود که از آن استفاده کردهاند یا تغییر یک متغییر در تابع پدر باعث تغییر در توابه بچه در Scope chain میشود)
- گرفتن ورودی از کنسول
- نوشتن در screen
- نوشتن در فایل
- دنبال کردن هر external process
- فراخوانی هر تابع دیگر که Side effects دارد
برنامهنویسها در FP از Side effects اجتناب میکنند که باعث میشود کد برنامه راحتتر درک شود و همچنین تست آن نیز راحتتر است.
*** زبانهای برنامهنویسی که functional هستند، با کمک mondas توابع را از شر side effect خلاص میکنند.
قابلیت استفاده مجدد از طریق توابع مرتبه بالاتر
با دانستن یک نکتهی طلایی خودتان را در کدزنی جاوااسکریپت ایمن کنید:
در جاوااسکریپت و تمام زبانهای برنامهنویسی functional ، توابع value هستند به همین خاطر میتوانیم توابع را به متغییرهای دیگر assign کنیم یا توابع را به عنوان پارامتر ورودی به توابع دیگر ارسال کنیم یا یک تابع را به عنوان خروجی برگردانیم. به این توابع first class functions میگویند. همین باعث میشود بتوانیم توابع را با هم ترکیب کنیم تا یک تابع با عملکرد جدید ایجاد کنیم.
مثال:
تصور کنید یک تابع داریم که مجموع ۲ پارامتر ورودیاش را ۲ برابر میکند:
const doubleSum = (a, b) => (a + b) * 2;
همچنین تابع دیگری داریم که تفاضل ۲ پارامتر ورودیاش را ۲ برابر میکند:
const doubleSubtraction = (a, b) => (a - b) * 2;
حالا همین توابع را با کمک ویژگیهایی که بالاتر برای first class functionsها ذکر کردیم، پیاده سازی میکنیم:
const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;
const doubleOperator = (f, a, b) => f(a, b) * 2;
doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
higher order function
higher order function به تابعی گفته میشود که یک یا چند تابع دیگر را به عنوان پارامتر ورودی دریافت میکند و یک تابع، مقدار(value ) یا هردو را به عنوان خروجی برمیگرداند. برای مثال تابع doubleOperator که بالاتر پیاده سازی کردیم، یک higher order function است. از higher order function های معروف و کاربری در جاوا اسکریپت میتوانیم به map,reduce و Filter اشاره کنیم که اگر فرصتی باقی بود، در مقالهی دیگری به آنها میپردازیم. این توابع در بسیاری از مشکلات عصای دست برنامه نویس وب هستند!
چندکلام با چ یاب:
در این مقاله تمام سعیام بر این بود که از طریق مثالهای ساده، با مفاهیم FP در زبانهای برنامه نویسی وب آشنایی پیدا کنید. قطعا این مبحث جای کار زیادی دارد و پیشنهاد میکنم برای حرفهای شدن و درک بهتر دست به کد شوید و مثالهای زیادی را با کمک این تکنیکها حل کنید.
اگر در این باره سوال یا تجربهای دارید، خوشحال میشم با ما و سایر خوانندگان چ یاب مطرح کنید.
راستی! بنظر شما مقالهی بعدی دربارهی چه موضوعی باشد؟
برای اطلاع از آخرین اخبار و آموزشهای ما میتونید در کانال تلگرام چ یاب عضو شید.
ارسال پاسخ
نمایش دیدگاه ها