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

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

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


یادآوری دوستانه

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

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


بازتولید (refactoring)

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

function validateSsn(ssn) { if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn)) console.log('Valid SSN'); else console.log('Invalid SSN'); } function validatePhone(phone) { if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone)) console.log('Valid Phone Number'); else console.log('Invalid Phone Number'); }


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

به جای اینکه تابع validateSsn را کپی پیست و ویرایش کنیم تا تابع validatePhone را بنویسیم، ما باید فقط یک تابع بنویسیم و چیزهایی که را که ویرایش کرده‌ایم به عنوان پارامترهای ورودی‌ش در نظر بگیریم. در این مثال ما می‌توانیم متغیر ورودی (value)، عبارات با قاعده (regular expression) و پیامی (message) که قرار است نشان داده شود (یا حداقل بخش پایانی پیام) را به عنوان پارامتر ورودی در نظر بگیریم. کد بازتولید شده به صورت زیر است:

function validateValue(value, regex, type) { if (regex.exec(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }


پارامترهای ssn و phone در کد قبلی حالابه صورت value درآمده است، عبارات با قاعده به صورت regex نشان داده شده و در نهایت قسمت آخر پیام یعنی 'SSN' و 'Phone Number' به صورت type نوشته شده است.

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

اما اگر کد شما شبیه مثال زیر باشد چه؟:

function validateAddress(address) { if (parseAddress(address) console.log('Valid Address'); else console.log('Invalid Address'); } function validateName(name) { if (parseFullName(name)) console.log('Valid Name'); else console.log('Invalid Name'); }

اینجا parseAddress وparseFullName توابعی هستند که یک رشته را می‌گیرند و اگر مقدارش درست باشد true را برمی‌گردانند.

چگونه می‌توانیم این توابع را بازتولید کنیم؟

خب ما می‌توانیم از value به جای address و name استفاده کنیم و از type به جای رشته‌های 'Address' و 'Name' استفاده کنیم، ولی اینجا یک تابع داریم که عبارات با قاعده را تجزیه وتحلیل می‌کند. اگر می‌توانستیم توابع را به عنوان پارامتر ارسال کنیم...


توابع مرتبه بالاتر (Higher-Order Functions)

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

در برنامه‌نویسی تابع‌گرا هر تابع یک شهروند درجه اول (first-class citizen) از زبان است، به عبارت دیگر هر تابع یک مقدار است.

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

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

function validateValueWithFunc(value, parseFunc, type) { if (parseFunc(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }

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

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

حالا ما می‌توانیم تابع مرتبه بالاترمان را برای چهار تابع قبلی صدا بزنیم. در مثال زیر به این دلیل که تابع Regex.exec اگر تطابقی با عبارت با قاعده پیدا کند مقدار true را بر‌می‌گرداند همه چیز درست کار می‌کند:

validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN'); validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');

این خیلی بهتر از این است که چهار تابع تقریبا مشابه داشته باشیم.

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

var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec; var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec; validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');

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

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

function makeRegexParser(regex) { return regex.exec; } var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/); var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/); validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');

در کد بالا تابع makeRegexParser یک عبارت با قاعده را می‌گیرد و تابع exec را برمی‌گرداند. تابع exec یک رشته می‌گیرد که تابع validateValueWithFunc برایش می‌فرستد.

متغیرهای parsePhone و parseSsn هم دقیقا مانند قبل هستند، یعنی همان تابع exec عبارات با قاعده.

در زیر یک مثال دیگر از توابع مرتبه بالا را می‌بینید که یک تابع دیگر را برمی‌گرداند:

function makeAdder(constantValue) { return function adder(value) { return constantValue + value; }; }

ما اینجا یک تابع به نام makeAdder داریم که مقدار constantValue را می‌گیرد و تابع adder را برمی‌گرداند. تابع adder ثابت constantValue را می‌گیرد و با هر مقداری که بهش پاس داده شود جمع می‌زند و نتیجه را برمی‌گرداند.

در زیر مثالی از به کار گیری این تابع نشان داده شده است:

var add10 = makeAdder(10); console.log(add10(20)); // prints 30 console.log(add10(30)); // prints 40 console.log(add10(40)); // prints 50

ما با فرستادن یک مقدار ثابت به makeAdder یک تابع به نام add10 ساخته‌ایم. makeAdder یک تابع برمی‌گرداند که هر چه بهش پاس داده شود با مقدار ثابت ۱۰ جمع می زند.

توجه کنید که تابع adder حتی بعد از اینکه تابع makeAdder بازگشت می‌کند به مقدار constantValue دسترسی دارد، این به این دلیل است که وقتی تابع adder ساخته می‌شود ثابت constantValue در محدوده دسترسی‌اش (scope) است.

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

این رفتار بستار یا کلوژر (Closure) نامیده می‌شود.


کلوژرها

در زیر مثالی می‌بینید که در آن توابع از کلوژرها استفاده می‌کنند:

function grandParent(g1, g2) { var g3 = 3; return function parent(p1, p2) { var p3 = 33; return function child(c1, c2) { var c3 = 333; return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3; }; }; }

در این مثال تابع child هم به متغیر‌های خودش دسترسی دارد، هم به متغیر‌های parent و هم به متغیر‌های grandParent.

تابع parent فقط به متغیرهای خودش و grandParent دسترسی دارد.

و تابع grandParent فقط به متغیرهای خودش دسترسی دارد.

( برای درک بهتر هرم موجود در تصویر بالا را مشاهده کنید.)

در مثال زیر می‌توانید چگونگی استفاده از کلوژر را ببینید:

var parentFunc = grandParent(1, 2); // returns parent() var childFunc = parentFunc(11, 22); // returns child() console.log(childFunc(111, 222)); // prints 738 // 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738

در مثال بالا parentFunc محدوده parent را برقرار نگه می‌دارد زیرا grandParent تابع parent را برمی‌گرداند.

به طور مشابه، childFunc محدوده child را برقرار نگه می‌دارد زیرا که parentFunc که همان parent است تابع child را برمی‌گرداند.

مترجم: خودم هم نفهمیدم چی شد؟ :)

وقتی یک تابع ساخته می‌شود همه‌ی متغیرهای در زمان ساخته شدنش که در محدوده‌اش هستند مادامی که زنده است برایش قابل دسترسی هستند. یک تابع تا وقتی که ارجاعی به آن وجود داشته باشد زنده است. به عنوان مثال تابع child تا هنگامی که متغیر childFunc به آن ارجاع می‌دهد زنده است.

یک کلوژر یک محدوده[دسترسی به متغیر] برای یک تابع است که به وسیله یک ارجاع به آن تابع برقرار نگه داشته می‌شود.

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

خوشبختانه متغیرها در برنامه‌نویسی تابع‌گرا تغییرناپذیر هستند و این ویژگی این منبع باگ و سردرگمی (مشکل بالا) را برطرف می‌کند.


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

لینک قسمت اول: +

منبع: +





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