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

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


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

ترکیب توابع

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

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

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

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

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

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

var add10 = function(value) { return value + 10; }; var mult5 = function(value) { return value * 5; };

کد بالا زیادی شلوغ است، اجازه دهید آن را با استاندارد جدید ES6 برای جاوا اسکریپت و با استفاده از fat arrow بازنویسی کنیم:

var add10 = value => value + 10;
var mult5 = value => value * 5;

حالا بهتر شد! حالا اجازه دهید تصور کنیم که ما یک تابع می‌خواهیم که یک عدد رابگیرد عدد ۱۰ را به آن اضافه کند و نتیجه را در ۵ ضرب کند، می‌توانیم این تابع را اینجوری بنویسیم:

var mult5AfterAdd10 = value => 5 * (value + 10)

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

پس به جای نوشتن این تابع بگذارید از توابع add10 و mult5 برای ساختن تابع جدیدمان استفاده کنیم:

var mult5AfterAdd10 = value => mult5(add10(value));

ما از توابع موجود برای ساخت تابع mult5AfterAdd10 استفاده کرده‌ایم. اما راه بهتری هم وجود دارد.

در ریاضی f ∘ g به معنای ترکیب توابع است وخوانده می‌شود: « f با g ترکیب شده است» یا به عبارت رایج‌تر: « f بعد از g». پس (f ∘ g)(x) برابر است با صدا زدن f بعد از g بر روی متغیر x یا به صورت ساده تر:

f(g(x))

در مثال بالا ما داریم mult5 add10 یا mult5 بعد از add10، به همین دلیل نام تابع را mult5AfterAdd10 گذاشته‌ایم.

و این دقیقا چیزی است که انجام داده‌ایم، ما تابع mult5 را بعد از تابع add10 بر روی متغیر value صدا زده‌ایم یا به عبارت ساده‌تر:

mult5(add10(value))

از آنجا که جاوا اسکریپت به صورت داخلی از ترکیب توابع پشتیبانی نمی‌کند، اجازه دهید مثال‌مان را با زبان Elm بازنویسی کنیم:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

علامت >> عملگر جا دهی است که عملیات ترکیب توابع را در زبان Elm انجام می‌دهد. این علامت کمک می‌کند که از جریان داده‌ها یک درک تصویری داشته باشیم: ابتدا متغیر value به تابع add10 پاس داده می‌شود سپس نتیجه به تابع mult5 فرستاده می‌شود.

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

شما از این طریق می‌توانید هر تعداد تابع که دوست داشته باشید ترکیب کنید:

f x =
    (g << h << s << r << t) x

در کد بالا متغیر x به تابع t فرستاده می‌شود، سپس نتیجه به تابع r فرستاده می‌شود، سپس نتیجه به تابع s فرستاده می‌شود، و به همین منوال...

اگر شما این کار را در جاوا اسکریپت انجام می‌دادید نتیجه این شکلی می‌شد:

g(h(s(r(t(x)))))

یک کابوس وحشتناک از پراتنزها!


علامت‌گذاری بدون پارامتر (Point-Free Notation)

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

مترجم: مثل اینکه این غربی‌های کافر! به ورودی‌های یک تابع علاوه بر پارامتر، پوینت (point) هم میگن. یعنی پوینت در اینجا همون پارامتره.

در تابع mult5AfterAdd10 می‌توانید ببنید که پارامتر value دو بار عنوان شده است، یک بار در لیست پارامترهای ورودی و یک بار هم در جایی که استفاده شده است:

-- This is a function that expects 1 parameter
mult5AfterAdd10 value =
    (mult5 << add10) value

اما این پارامتر غیر ضروری است زیرا تابع add10 (سمت راست‌ترین تابع از ترکیب توابع بالا) هم همین پارامتر را می‌پذیرد. کد زیر نمونه بدون پارامتر کد بالا است:

-- This is also a function that expects 1 parameter
mult5AfterAdd10 =
    (mult5 << add10)

استفاده از نمونه بدون پارامتر سودمندی‌های بسیاری دارد:

نخست، مجبور نیستیم پارامترهای زائد را عنوان کنیم، و از آنجا که مجبور نیستیم آنها را عنوان کنیم، مجبور نیستیم برای آنها دنبال اسم مناسب بگردیم.

دوم، این مدل نوشتن برای خواندن راحت‌تر است و کدمان کمتر شلوغ می‌شود.

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


دردسر در بهشت

تا اینجا دیدیم که ترکیب توابع چگونه عمل می‌کند و همچنین روش علامت‌گذاری بدون پارامتر را آموختیم. حالا اجازه دهید از این چیزهایی که آموخته‌ایم در یک مثال متفاوت استفاده کنیم تا ببینیم که این شگردها چگونه عمل می‌کنند. تصور کنید که تابع add10 را با تابع add زیر جایگزین کرده‌ایم:

add x y =
    x + y
mult5 value =
    value * 5

حالا تابع mult5After10 را با این دو تابع جدید چگونه می‌نویسیم؟

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

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

-- This is wrong !!!!
mult5AfterAdd10 =
    (mult5 << add) 10 

اما این راه‌حل غلط است زیرا تابع add دو تا پارامتر می‌گیرد.

اگر این مسئله برایتان با زبان Elm واضح نیست سعی کنید آن را با جاوا اسکریپت بنویسید:

var mult5AfterAdd10 = mult5(add(10)); // this doesn't work

این کد اشتباه است ولی چرا؟

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

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

بگذارید دوباره سعی کنیم:

var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free

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

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

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

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

خب یک راه حل پیدا کردیم، این راه‌حل اصطلاحا Currying نامیده می‌شود.


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

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

منبع:+



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