سعید نوری
سعید نوری
خواندن ۱۱ دقیقه·۷ سال پیش

پس شما می‌خواهید برنامه‌نویس تابع‌گرا (فانکشنال) شوید؟ (قسمت اول)

اولین قدم‌ها برای یادگیری برنامه‌نویسی فانکشنال مهم‌ترین قدم‌ها هستند و در برخی اوقات سخت‌ترین قدم‌ها. البته اگر از راه درست وارد شوید چندان هم سخت نخواهد بود.


یادگیری رانندگی

وقتی برای اولین بار شروع به یادگیری رانندگی می‌کنیم در نگاه اول وقتی به رانندگی دیگران نگاه می‌کنیم کار ساده‌ای به نظر می‌رسد، اما این کار وقتی خودمان اولین قدم‌ها را برمی‌داریم سخت‌تر می‌شود.

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

با در دست داشتن گواهی‌نامه، ما در هر فرصت ممکن ماشین را برمی‌داریم و شروع به رانندگی می‌کنیم، با هر دور، رانندگی ما بهتر و بهتر می‌شود و اعتماد به نفسمان بالا می‌رود. سپس یک روزی می‌رسد که عمر ماشین‌مان تمام می‌شود و ما مجبور می‌شویم یک ماشین دیگر بخریم، یا ماشین فردی دیگر را برانیم.

وقتی ما پشت رُل یک ماشین جدید می‌نشینیم چه حسی دارد؟ آیا این حس شبیه حسی است که ما برای اولین بار پشت رُل نشستیم؟ البته که نه، در بار اول‌مان همه چیز ناآشنا بود در حالی که این بار خیلی چیزها آشنا هستند.

وقتی می‌خواهیم ماشین جدیدی را برانیم سوال‌های ساده‌ای را از خودمان می‌پرسیم مثلا: کلید استارت کجاست؟ کلید چراغ‌ها کجا هستند؟ چگونه راهنما بزنیم و چگونه آینه را تنظیم کنیم؟ بعد از این کار به آرامی شروع به حرکت می‌کنیم. اما چرا این بار در مقایسه با بار اول کارمان اینقدر راحت است؟ دلیلش این است که ماشین جدید به میزان بسیار زیادی شبیه ماشین قدیمی است، ماشین جدید هم تمام چیز‌های پایه‌ای که یک ماشین نیاز دارد را دارد و همه‌ی آن چیزها مکان‌ قرارگیری‌شان تقریبا در همان حوالی مکان‌شان در ماشین قبلی است.

زبان‌های برنامه‌نویسی را هم تقریبا همینطوری یاد می‌گیریم. یادگیری اولین زبان از همه سخت‌تر است، ولی هنگامی که بر اولی مسلط شدید یادگیری زبان‌های بعدی آسان‌تر می‌شود.

هنگامی که شما شروع به یادگیری زبان دوم می‌کنید ممکن است چنین سوالاتی از خود بپرسید: چگونه یک ماژول بسازم؟ چگونه در یک آرایه جستجو کنم؟ پارامتر‌های لازم برای تابع substring چه چیز‌هایی هستند؟ شما مطمئن هستید که می‌توانید زبان جدید را یاد بگیرید چون خیلی از مفاهیمش شبیه زبان قدیمی هستند.


راندن اولین سفینه فضایی

حالا تصور کنید که شما پشت رُل یک سفینه فضایی نشسته‌اید، اینجا دانستن رانندگی ماشین کمک چندان به شما نخواهد کرد. شما باید دوباره از صفر شروع به یادگیری کنید.

یادگیری برنامه‌نویسی تابع‌گرا بعد از یادگیری برنامه‌نویسی رویه‌گرا یا شی‌گرا هم همچین حالتی دارد. شما باید انتظار چیزهای بسیار متفاوتی داشته باشید و خیلی از چیزهایی که در مورد برنامه‌نویسی می‌دانید به کار شما نمی‌آیند.

برنامه‌نویسی یعنی فکر کردن و برنامه‌نویسی تابع‌گرا شما را مجبور می‌کند بسیار متفاوت‌تر از قبل فکر کنید، آنقدر متفاوت که احتمالا هرگز به مدل فکر کردن قدیمی باز نمی‌گردید.


هر آنچه می‌دانید را فراموش کنید

برنامه‌نویسی تابع‌گرا مثل شروع کردن از صفر است (البته نه کاملا، ولی به مقدار زیادی). بعضی از مفاهیم مانند قبل هستند ولی بهتر است شما آمادگی داشته باشید که همه چیز را از پایه شروع کنید.

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

یادگیری برنامه‌نویسی تابع‌گرا بسیار طول خواهد کشید پس صبور باشید.

پس اجازه دهید از دنیای سرد برنامه‌نویسی امری ( Imperative Programming) خارج شویم و یک غوطه نرم در آب‌های گرم برنامه نویسی تابع‌گرا بزنیم.

آنچه که در ادامه می‌آید مفاهیمی از برنامه‌نویسی تابع‌گرا هستد که به درک کلی شما از برنامه‌نویسی تابع‌گرا قبل از اینکه یادگیری اولین زبان برنامه‌نویسی تابع‌گرا را شروع کنید کمک می‌کنند.

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


خلوص

وقتی برنامه‌نویسان تابع‌گرا در مورد خلوص حرف می‌زنند در واقع منظورشان توابع خالص (Pure Functions) است. توابع خالص توابع بسیار ساده‌ای هستند، آنها فقط بر روی پارامتر‌های ورودی‌شان عمل می‌کنند. در مثال زیر می توانید یک نمونه از یک تابع خالص را در جاوااسکریپت ببیند:

var z = 10;
function add(x, y) {
return x + y;
}

دقت کنید که تابع add هیچ کاری با متغیر z ندارد، نه از z می‌خواند و نه در z می‌نویسد. این تابع فقط از پارامترهای ورودی‌ش یعنی x و y می خواند و مجموع آنها را برمی‌گرداند. این تابع یک تابع خالص است، اگر این تابع با z عملیات انجام می‌داد دیگر یک تابع خالص نبود.

در زیر یک مثال دیگر داریم:

function justTen() {
return 10;
}

تابع justTen یک تابع خالص است و فقط یک ثابت بر‌می‌گرداند چون ما به آن هیچ پارامتر ورودی نداده‌ایم و چون خالص است به هیچ متغیر دیگری هم دسترسی ندارد. از آنجا که توابع خالصی که هیچ پارامتر ورودی ندارند کار خاصی انجام نمی‌دهند درنتیجه چندان هم مفید نیستند. در نتیجه بهتر بود که تابع justTen بصورت یک متغیر ثابت تعریف شود.

اکثر توابع خالص مفید باید حداقل یک پارامتر ورودی داشته باشند.

حالا این تابع را در نظر بگیرید:

function addNoReturn(x, y) {
var z = x + y
}

توجه کنید که این تابع هیچ چیزی را برنمی‌گرداند. این توابع مقدار متغیرهای x , y را جمع می‌زند و حاصل را در z می‌گذارد ولی آن را برنمی‌گرداند. این یک تابع خالص است چون فقط بر پارامترهای ورودی‌ش تکیه دارد، اما از آنجا که چیزی را برنمی‌گرداند یک تابع بلااستفاده است.

همه‌ی توابع خالص مفید باید چیزی را برگردانند.

بگذارید دوباره به سراغ تابع add اول‌مان برویم:

function add(x, y) {
return x + y;
}
console.log(add(1, 2)); // prints 3
console.log(add(1, 2)); // still prints 3
console.log(add(1, 2)); // WILL ALWAYS print 3

دقت کنید که همیشه حاصل (1,2)add عدد ۳ است، البته این دور از انتظار هم نیست زیرا تابع add یک تابع خالص است. اگر تابع add به متغیری خارج از محدوده‌اش دسترسی می‌داشت شما هرگز نمی‌توانستید رفتارش را پیش‌بینی کنید.

توابع خالص برای هر ورودی مشخص همیشه یک خروجی مشخص تولید می‌کنند.

از آنجا که توابع خالص نمی‌توانند هیچ متغیر خارجیی را تغییر دهند همه‌ی توابع زیر غیر خالص هستند:

writeFile(fileName);
updateDatabaseTable(sqlCmd);
sendAjaxRequest(ajaxRequest);
openSocket(ipAddress);

همه‌ی این توابع چیزی دارند که اثرات جانبی (Side Effects) نامیده می‌شود. هنگامی که شما این توابع را صدا می‌زنید آنها یک فایل یا یک جدول از پایگاه داده را تغییر می‌دهند، یا به یک سرور داده می‌فرستند و یا از سیستم عامل یک سوکت را درخواست می‌کنند. آنها کارهای بیشتری از فقط خواندن متغیر‌های ورودی و برگرداندن خروجی را انجام می‌دهند، درنتیجه شما هیچوقت نمی‌توانید پیش بینی کنید که این توابع چه چیزی را برمی‌گردانند.

توابع خالص دارای اثرات جانبی نیستند.

در زبان‌های برنامه‌نویسی امری (imperative) مانند جاوا اسکریپت، سی‌شارپ و جاوا اثرات جانبی همه جا هستند، این پدیده دیباگ برنامه شما را سخت می‌کند چون یک متغیر ممکن است در هر جایی از برنامه شما تغییر کند. درنتیجه وقتی که شما یک باگ در برنامه به این دلیل دارید که مقدار یک متغیر در زمان نامناسب تغییر کرده است، کجا را برای رفع باگ می‌گردید؟ همه‌ جا؟ خب این خوب نیست.

در اینجا شما ممکن است بپرسید: چگونه من همه‌ی قسمت‌ها را فقط با توابع خالص بنویسم؟

در برنامه‌نویسی تابع‌گرا شما فقط تابع خالص نمی‌نویسید.

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

تغییرناپذیری

آیا وقتی که برای اولین بار همچین قطعه کدی را دید را به خاطر می‌آورید:

var x = 1;
x = x+1;

و کسی که به شما برنامه‌نویسی یاد می‌داد گفت که هر آنچه که سر کلاس ریاضی یاد گرفته‌اید فراموش کنید؟ در ریاضی x هرگز نمی‌تواند برابر با x+1 شود. اما در برنامه‌نویسی امری این گزاره با معنی است: مقدار x را بگیرید به آن عدد ۱ را اضافه کنید و حاصل را در x بگذارید.

در برنامه‌نویسی تابع‌گرا عبارت x=x+1 غیرمجاز است. درنتیجه شما باید هر آنچه که سر کلاس ریاضی یاد گرفتید و در برنامه‌نویسی امری فراموش کردید دوباره به یاد بیاورید!

در برنامه‌نویسی تابع‌گرا چیزی بنام متغیر وجود ندارد.

مقادیر ذخیره شده به خاطر سازگاری با گذشته همچنان متغیر (variable) نامیده می‌شوند اما آنها در واقع ثابت (constant) هستند، یعنی هنگامی که x مقداری را گرفت برای تمام طول حیاتش مقدارش همان است.

نگران نباشید، x معمولا یک متغیر محلی است درنتیجه طول حیاتش کوتاه است، اما تا زمانی که زنده است قابل تغییر نیست.

در زیر یک مثال در مورد ثابت‌ها با زبان Elm می‌بینید. Elm یک زبان تابع‌گرای خالص برای توسعه وب است.

‍‍‍addOneToSum y z =
let
x = 1
in
x + y + z

اگر شما با سینتکس ML-Style آشنا نیستید اجازه بدهید آن برای شما توضیح دهم: addOneToSum یک تابع است که دو پارامتر می‌گیرد: x و y. داخل بلوک let متغیر x به عدد ۱ مقید می‌شود، یعنی برای تمام طول حیاتش مقدارش برابر با ۱ می‌شود. مدت زمان حیات این متغیر هنگامی که از تابع خارج می‌شویم به پایان می‌رسد، یا به عبارت دقیق‌تر هنگامی که بلوک let ارزیابی می‌شود.

در داخل بلوک in حاصل محاسبه می‌تواند شامل مقادیری باشد که در بلوک let تعریف شده‌اند (همان x). نتیجه‌ی محاسبه یعنی x+y+z برگردانده می‌شود، یا به صورت دقیق‌تر 1 + y + z برگردانده می‌شود چون x=1.

یک بار دیگر من می‌توانم بشنوم که شما می‌گویید: آخر چگونه می‌توانم همه‌ی کارها را بدون متغیرها انجام دهم؟

اجازه دهید در مورد اینکه چه موقع می‌خواهیم مقدار متغیرها را تغییر دهیم کمی فکر کنیم. عموما در دو حالت ما مقدار متغیرها را تغییر می‌دهیم:۱- تغییر دادن یک مقدار از یک متغیر چندمقداره مثل تغییر دادن یک مقدار از یک رکورد یا یک شی، ۲- تغییر دادن یک متغیر یک‌مقداره مثل تغییر دادن مقدار شمارنده حلقه (همان ++i).

برنامه‌نویسی تابع‌گرا برای تغییر یک مقدار از یک رکورد یک کپی از رکورد را که در این کپی آن مقدار تغییر کرده را می سازد. این مدل برنامه‌یسی این کار را با استفاده از ساختمان داده‌هایی که در اختیار دارد به صورتی انجام می‌دهد که نیاز نباشد همه‌ی قسمت‌های رکورد کپی شوند.

برنامه‌نویسی تابع‌گرا برای تغییر مقدار متغیرهای تک‌مقداره نیز از همین روش استفاده می‌کند و متغیر را کپی می‌کند. و البته بدون استفاده از حلقه‌ها.

البته این بدان معنی نیست که در برنامه‌نویسی تابع‌گرا ما نمی‌توانیم حلقه بزنیم بلکه به این معنی است که در این مدل برنامه‌نویسی ساختارهایی مانند for,while,do,repeat نداریم.

برنامه‌نویسی تابع‌گرا از بازگشت ( recursion) برای حلقه زدن استفاده می‌کند.

در این مثال شما دو روش می‌بینید که می‌توانید در جاوااسکریپت برای حلقه زدن استفاده کنید:

// simple loop construct
var acc = 0;
for (var i = 1; i <= 10; ++i)
acc += i;
console.log(acc); // prints 55
// without loop construct or variables (recursion)
function sumRange(start, end, acc) {
if (start > end)
return acc;
return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0)); // prints 55

توجه کنید که چگونه بازگشت (روش تابع‌گرا) با استفاده از صدا زدن خودش با یک استارت جدید (start + 1) و یک انباره جدید (acc + start) به همان نتیجه‌ای رسیده است که حلقه for رسیده است. بازگشت مقادیر قبلی را تغییر نداده است، به جای آن از مقادیر جدیدی استفاده می‌کند که از روی مقادیر قبلی محاسبه شده‌اند.

متاسفانه در جاوااسکریپت دیدن این چیزها سخت است به دو دلیل: ۱- سینتکس جاوااسکریپت شلوغ و درهم برهم است، ۲- شما احتمالا قبلا هیچوقت به صورت بازگشتی فکر نکرده‌اید.

در زبان Elm خواندن و در نتیجه فهم بازگشت آسان‌تر است:

sumRange start end acc =
if start > end then
acc
else
sumRange (start + 1) end (acc + start)

در زیر وقتی که مثال بالا اجرا می‌شود را می‌توانید ببینید:

sumRange 1 10 0 = -- sumRange (1 + 1) 10 (0 + 1)
sumRange 2 10 1 = -- sumRange (2 + 1) 10 (1 + 2)
sumRange 3 10 3 = -- sumRange (3 + 1) 10 (3 + 3)
sumRange 4 10 6 = -- sumRange (4 + 1) 10 (6 + 4)
sumRange 5 10 10 = -- sumRange (5 + 1) 10 (10 + 5)
sumRange 6 10 15 = -- sumRange (6 + 1) 10 (15 + 6)
sumRange 7 10 21 = -- sumRange (7 + 1) 10 (21 + 7)
sumRange 8 10 28 = -- sumRange (8 + 1) 10 (28 + 8)
sumRange 9 10 36 = -- sumRange (9 + 1) 10 (36 + 9)
sumRange 10 10 45 = -- sumRange (10 + 1) 10 (45 + 10)
sumRange 11 10 55 = -- 11 > 10 => 55
55

شما ممکن است فکر کنید که حلقه for آسان‌تر درک می‌شود، این مسئله قابل بحث است و به خاطر این است که این حلقه به چشم شما آشناست. حلقه‌های غیربازگشتی به تغییر‌پذیری ( Mutability) احتیاج دارند که چیز خوبی نیست.

من نمی‌خواهم اینجا همه‌ی مزایای تغییرناپذیری را شرح دهم. شما می‌توانید برای مطالعه بیشتر به بخش Global Mutable State از این مقاله مراجعه کنید.

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

همچنین اگر برنامه شما بصورت مولتی-ترد نوشته شود، تردها عملیات همدیگر را مختل نمی‌کنند، چون اگر یک ترد بخواهد مقدار یک متغیر را تغییر دهد مجبور است یک کپی از روی آن بسازد.

تغییرناپذیری کد شما را ساده‌تر و امن‌تر می‌کند.


ادامه دارد...

منبع: +









برنامه نویسی فانکشنالفانکشنال
علاقه‌مند به فناوری، توسعه دهنده پایتون و بک‌اند در آینده‌ای دور یا نزدیک!
شاید از این پست‌ها خوشتان بیاید