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

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

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

Currying

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

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

یک تابع کاری شده (Curried Function) تابعی است که در هر لحظه فقط یک پارامتر می‌گیرد.


مترجم: ترجمه اصطلاح Currying کار ساده‌ای نیست. این اصطلاح اولین بار به افتخار ریاضیدان امریکایی Haskell Curry که ابداع کننده این روش است استفاده شده است. همچنین این کلمه به معنای ادویه زدن هم هست که در برنامه‌نویسی بی‌معناست. به همین دلیل من تصمیم گرفتم به جای Currying از اصطلاح کاری کردن استفاده کنم و به جای Curried از اصطلاح کاری شده استفاده کنم.

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

در جاوا اسکریپت ما می‌توانیم با بازنویسی تابع add این کار را انجام دهیم:

var add = x => y => x + y

این نسخه از add تابعی است که یکی از پارامتر‌هایش را الان می‌گیرد و دیگری را بعدا. اگر بخواهیم با جزئیات بگوییم: تابع add یک پارامتر می‌گیرد که همان x است و یک تابع برمی‌گرداند، این تابع یک پارامتر می‌گیرد (y) و مقدار x+y را برمی‌گرداند.

حالا ما می‌توانیم از این نسخه از تابع add برای ساختن تابع mult5AfterAdd10 استفاده کنیم:

var compose = (f, g) => x => f(g(x)); var mult5AfterAdd10 = compose(mult5, add(10));

تابع compose دو تا پارامتر می‌گیرد: f و g ، و سپس یک تابع برمی‌گرداند که این تابع یک پارامتر می‌گیرد (x) و ترکیب توابع f و g را بر روی پارامتر x را برمی گرداند.

پس ما دقیقا چکار کردیم؟ ما تابع add قدیمی‌مان را به یک تابع کاری شده تبدیل کردیم، این کار باعث می‌شود تابع add بسیار انعطاف‌پذیر شود زیرا اولین پارامتر(عدد ۱۰) می‌تواند جلو‌جلو به این تابع فرستاده شود و پارامتر دوم هنگامی که تابع mult5AfterAdd10 صدا زده شد فرستاده شود.

در اینجا شما ممکن است بخواهید بدانید که در زبان Elm چگونه می‌شود که تابع add را بازنویسی کرد. خب شما مجبور به این کار نیستید زیرا در زبان Elm و دیگر زبان‌های تابع گرا همه‌ی توابع به صورت اتوماتیک کاری شده هستند.

در نتیجه تابع add مانند قبل نوشته می‌شود:

add x y = x + y

و این هم تابع mult5AfterAdd10 که باید اینجوری نوشته شود:

mult5AfterAdd10 = (mult5 << add 10)

از لحاظ سینتکس زبان Elm بر زبان‌های امری مثل جاوا اسکریپت برتری دارد زیرا این زبان برای مفاهیم تابع‌گرا مثل ترکیب توابع یا کاری کردن بهینه شده است.


کاری کردن و بازتولید (Currying and Refactoring)

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

به عنوان مثال توابع زیر را در نظر بگیرید، این توابع به ابتدا و انتهای رشته‌ها براکت یا براکت دوتایی اضافه می‌کنند:

bracket str = "{" ++ str ++ "}" doubleBracket str = "{{" ++ str ++ "}}"

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

bracketedJoe = bracket "Joe" doubleBracketedJoe = doubleBracket "Joe"

ما می‌توانیم نسخه عمومی شده از توابع بالا را بسازیم:

generalBracket prefix str suffix = prefix ++ str ++ suffix

اما حالا هر بار که بخواهیم از تابع generalBracket استفاده کنیم مجبوریم براکت‌ها را به آن پاس دهیم:

bracketedJoe = generalBracket "{" "Joe" "}" doubleBracketedJoe = generalBracket "{{" "Joe" "}}"

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

با توجه به این حقیقیت که همه‌ی توابع کاری شده هستند، با چینش دوباره پارامتر‌های تابع generalBracket ما می‌توانیم توابع bracket و doubleBracket را تولید کنیم:

generalBracket prefix suffix str = prefix ++ str ++ suffix bracket = generalBracket "{" "}" doubleBracket = generalBracket "{{" "}}"

توجه کنید که ما با قرار دادن پارامترهایی که به نظر می‌رسد تغییر نمی‌کنند ( prefix ,suffix) در ابتدای لیست پارامترها و با قرار دادن پارامتر‌هایی که تغییر خواهند کرد (str) در انتهای لیست توانسته‌ایم که نسخه اختصاصی شده (specialized) از تابع generalBracket را بسازیم.

چینش پارامترها برای به کار گیری کامل currying مهم است.

همچنین توجه کنید که توابع bracket و doubleBracket به صورت بدون پارامتر نوشته شده‌اند. هر دوی توابع bracket و doubleBracket توابعی هستند که منتظر دریافت پارامتر نهایی‌شان هستند.

حالا ما می‌توانیم از آنها مانند قبل استفاده کنیم:

bracketedJoe = bracket "Joe" doubleBracketedJoe = doubleBracket "Joe"

اما ما این بار از یک تابع عمومی شده‌ی کاری شده استفاده کرده‌ایم (generalBracket ).


توابع عمومی در برنامه‌نویسی تابع‌گرا

بگذارید نگاهی بیندازیم به سه تابع عمومی که در زبان‌های برنامه‌نویسی تابع‌گرا استفاده می‌شوند.

اما ابتدا اجازه دهید نگاهی بیندازیم به کد جاوا اسکریپت زیر:

for (var i = 0; i < something.length; ++i) { // do stuff }

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

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

اجازه بدهید با تغییر دادن یک آرایه به نام things شروع کنیم:

var things = [1, 2, 3, 4]; for (var i = 0; i < things.length; ++i) { things[i] = things[i] * 10; // MUTATION ALERT !!!! } console.log(things); // [10, 20, 30, 40]

اَه! تغییرپذیری! (Mutability)

بگذارید دوباره سعی کنیم، این بار ما آرایه things را تغییر نمی‌دهیم:

var things = [1, 2, 3, 4]; var newThings = []; for (var i = 0; i < things.length; ++i) { newThings[i] = things[i] * 10; console.log(newThings); // [10, 20, 30, 40]

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

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

اجازه بدهید کد بالا را برداریم و در یک تابع بگذاریم. ما این تابع عمومی‌مان را map می‌نامیم زیرا هر مقدار از آرایه قدیمی را به یک مقدار از آرایه جدید نگاشت (map) می‌کند:

var map = (f, array) => { var newArray = []; for (var i = 0; i < array.length; ++i) { newArray[i] = f(array[i]); } return newArray; };

دقت کنید که تابع f عبور داده شده است در نتیجه تابع map می تواند هر عملی که ما می‌خواهیم با هر یک از اعضای آرایه array انجام دهد.

حالا ما می‌توانیم کد قبلی‌مان را برای استفاده از تابع map بازنویسی کنیم:

var things = [1, 2, 3, 4]; var newThings = map(v => v * 10, things);

دیگر حلقه for در کار نیست، و برای خواندن بسیار راحت‌تر است.

خب از لحاظ تکنیکی حلقه for هنوز در تابع map وجود دارد اما حداقل ما دیگر مجبور نیستیم آن کد تکراری را بنویسیم.

حالا اجازه دهید یک تابع عمومی دیگر برای فیلتر (filter) کردن اعضای یک آرایه بنویسیم:

var filter = (pred, array) => { var newArray = []; for (var i = 0; i < array.length; ++i) { if (pred(array[i])) newArray[newArray.length] = array[i]; } return newArray; };

دقت کنید که چگونه با تابع تطابق (pred) اعضای آرایه را فیلتر می‌کنیم. اگر این تابع مقدار true را برگرداند عضو را نگه می‌داریم و اگر مقدار false را برگرداند عضو جاری را دور می‌ریزیم.

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

var isOdd = x => x % 2 !== 0; var numbers = [1, 2, 3, 4, 5]; var oddNumbers = filter(isOdd, numbers); console.log(oddNumbers); // [1, 3, 5]

استفاده از این تابع فیلتر بسیار راحت تر از فیلتر کردن دستی با استفاده از یک حلقه for است.

آخرین تابع عمومی تابع کاهش (reduce) نامیده می‌شود. عموما از این تابع برای کاهش دادن اعضای یک لیست به یک تک‌مقدار استفاده می‌شود، اما این تابع می‌تواند کارهای خیلی بیشتری انجام دهد.

در زبان‌های برنامه‌نویسی تابع‌گرا این تابع معمولا fold نامیده می‌شود.

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

var reduce = (f, start, array) => { var acc = start; for (var i = 0; i < array.length; ++i){ acc = f(array[i], acc); // f() takes 2 parameters } return acc; });

تابع reduce سه پارامتر می‌گیرد: یک تابع (f)، یک مقدار برای نقطه شروع (start) و یک آرایه (array).

دقت کنید که تابع کاهش دهنده (f) دو تا پارامتر می‌گیرد:عضو جاری آرایه و یک متغیر انباره acc. این تابع از این پارامترها برای تولید یک انباره جدید در هر تکرار استفاده می‌کند. انباره حاصل از آخرین تکرار برگشت داده می‌شود.

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

var add = (x, y) => x + y; var values = [1, 2, 3, 4, 5]; var sumOfValues = reduce(add, 0, values); console.log(sumOfValues); // 15

توجه کنید که تابع add دو پارامتر می‌گیرد و آنها را با هم جمع می‌زند. تابع کاهش هم یک تابع دو پارامتره را به عنوان پارامتر می‌گیرد. یعنی تابع add و تابع کاهش به خوبی با هم کار می‌کنند.

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

هر کدام از این توابع عمومی (map,filter,reduce) به ما کمک می‌کنند عملیات دستکاری بر روی آرایه‌ها را انجام دهیم بدون اینکه مجبور باشیم کدهای تکراری حلقه‌های for را بنویسیم.

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


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

لینک قسمت‌های اول، دوم و سوم: +و+و+

منبع: +

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