این قسمت از مقاله به ایده اصلی برنامه نویسی تابعی و دلیل وجودی آن خواهد پرداخت. هیچ شکی نیست که بزرگترین چالش در توسعه نرم افزارهای بزرگ، پیچیدگی آن است. تغییرات همیش اجتناب ناپذیر هستند. به خصوص زمانی که صحبت از پیاده سازی امکان جدیدی باشد، پیچیدگی اضافه خواهد شد. در نتیجه منجر به سخت شدن فهمیدن کد میشود، زمان توسعه را بالاتر میبرد و باگهای ناخواسته را به وجود خواهد آورد. همچنین تغییر هر چیزی در دنیای نرم افزار بدون به وجود آوردن رفتارهای ناخواسته و یا اثرات جانبی، تقریبا غیر ممکن است. در نهایت همه این موارد میتوانند سرعت توسعه را پایین برده و حتی باعث شکست پروژههای نرم افزاری شوند. سبکهای کد نویسی دستوری (Imperative) مانند برنامه نویسی شیء گرا، میتوانند به کاهش این پیچیدگیها تا حد خوبی کمک کنند. البته در صورتیکه به طور صحیحی پیاده شوند. در واقع با ایجاد Abstraction در این مدل برنامه نویسی، پیچیدگیها را مخفی میکنیم.
سیر تکاملی الگوهای برنامه نویسی
برنامه نویسی شیء گرا در خون برنامه نویسهای سی شارپ جاری است؛ ما معمولا ساعتها درباره اینکه چگونه میتوانیم با استفاده از ارث بری و ترتیب پیاده کلاسها، یک هدف خاص برسیم، بر روی کپسوله سازی تمرکز میکنیم و انتزاع (Abstraction) و چند ریختی ( Polymorphism ) را برای تغییر وضعیت برنامه استفاده میکنیم. در این مدل همیشه احتمال این وجود دارد که چند ترد به صورت همزمان به یک ناحیه از حافظه دسترسی داشته باشند و تغییری در آن به وجود بیاورند و باعث به وجود آمدن شرایط Race Condition شوند. البته همگی به خوبی میدانیم که میتوانیم یک برنامهی کاملا Thread-Safe هم داشته باشیم که به خوبی مباحث همزمانی و همروندی را مدیریت کند؛ اما یک مساله اساسی در مورد کارآیی باقی میماند. گرچه Parallelism به ما کمک میکند که کارآیی برنامه خود را افزایش دهیم، اما refactor کردن کدهای موجود، به حالت موازی، کاری سخت و پردردسر خواهد بود.
برنامه نویسی تابعی، یک الگوی برنامه نویسی است که از یک ایده قدیمی (قبل از اولین کامپیوترها !) برگرفته شدهاست؛ زمانیکه دو ریاضیدان، یک تئوری به نام lambda calculus را معرفی کردند، که یک چارچوب محاسباتی میباشد؛ عملیاتی ریاضی را انجام میدهد و نتیجه را محاسبه میکند، بدون اینکه تغییری را در وضعیت دادهها و وضعیت، به وجود بیاورد. با این کار، فهمیدن کدها آسانتر خواهد بود و اثرات جانبی را کمتر خواهد کرد، همچین نوشتن تستها سادهتر خواهند شد.
جالب است اگر زبانهای برنامه نویسی را که از برنامه نویسی تابعی پشتیبانی میکنند، بررسی کنیم، مانند Lisp , Clojure, Erlang, Haskel، هر کدام از این زبانها جنبههای مختلفی از برنامه نویسی تابعی را پوشش میدهند. #F یک عضو از خانواده ML میباشد که بر روی دات نت فریمورک در سال 2002 پیاده سازی شده. ولی جالب است بدانید بیشتر زبانهای همه کاره مانند #C به اندازه کافی انعطاف پذیر هستند تا بتوان الگوهای مختلفی را توسط آنها پیاده کرد. از آنجایی که اکثرا ما از #C برای توسعه نرم افزارهایمان استفاده میکنیم، ترکیب ایدههای برنامه نویسی تابعی میتواند راهکار جالبی برای حل مشکلات ما باشد.
قبلا درباره توابع ریاضی صحت کردیم. در زبانهای برنامه نویسی هم ایده همان است؛ ورودیهای مشخص و خروجی مورد انتظار، بدون تغییری در حالت برنامه. به این مفاهیم شفافیت و صداقت توابع میگوییم که در ادامه با آن بیشتر آشنا میشویم. به این نکته توجه داشته باشید که منظور از تابع در #C فقط Method نیست؛ Func , Action , Delegate هم نوعی تابع هستند.
به طور ساده با نگاه کردن به ورودیهای تابع و نام آنها باید بتوانیم کاری را که انجام میدهد، حدس بزنیم. یعنی یک تابع باید بر اساس ورودیهای آن کاری را انجام دهد و نباید یک پارامتر Global آن را تحت تاثیر قرار دهد. پارامترهای Global میتوانند یک Property در سطح یک کلاس باشند، یا یک شیء که وضعیت آن تحت کنترل تابع نیست؛ مانند شی DateTime. به مثال زیر توجه کنید:
public int CalculateElapsedDays(DateTime from) { DateTime now = DateTime.Now; return (now - from).Days; }
آیا میتوانید این تابع را شفاف کنیم؟ بله!
چطور؟ به سادگی! با تغییر پارامترهای ورودی:
public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;
صداقت یک تابع یعنی یک تابع باید همه اطلاعات مربوط به ورودیها و خروجیها را پوشش دهد. به این مثال دقت کنید:
public int Divide(int numerator, int denominator) { return numerator / denominator; }
آیا این همه مواردی را که از آن انتظار داریم پوشش میدهد؟ احتمالا خیر!
اگر دو عدد صحیح را به این تابع بفرستیم، احتمالا مشکلی پیش نخواهد آمد. اما همانطور که حدس میزنید اگر پارامتر دوم 0 باشد چه اتفاقی خواهد افتاد؟
var result = Divide(1,0);
چگونه مشکل را حل کنیم؟ تایپ ورودی را به شکل زیر تغییر دهیم:
public static int Divide(int numerator, NonZeroInt denominator) { return numerator / denominator.Value; }
Functions as first-class values
ترجمه فارسی این کلمه ما را از معنی اصلی آن خیلی دور میکند؛ احتمالا یک ترجمه سادهی آم میتواند «تابع، ارزش اولیه کلاس» باشد!
وقتی توابع first-class values باشند، یعنی میتوانند به عنوان ورودی سایر توابع استفاده شوند، میتوانند به یک متغیر انتساب داده شوند، دقیقا مثل یک مقدار. برای مثال:
Func<int, bool> isMod2 = x => x % 2 == 0; var list = Enumerable.Range(1, 10); var evenNumbers = list.Where(isMod2);
توابع مرتبه بالا! یک یا چند تابع را به عنوان ورودی میگیرند و یک تابع را به عنوان نتیجه بر میگرداند. در مثال بالا Extension Method ، Where یک تابع High-Order میباشد.
پیاده سازی Where احتمالا به شکل زیر میباشد:
public static IEnumerable<T> Where<T>(this IEnumerable<T> ts, Func<T, bool> predicate) { foreach (T t in ts) if (predicate(t)) yield return t; }
2. ملاک تشخیص اینکه چه آیتمهایی در لیست باید وجود داشته باشند، به عهده متدی میباشد که آن را فراخوانی میکند.
در این مثال، تابع Where، تابع ورودی را به ازای هر المان، در لیست فراخوانی میکند. این تابع میتواند طوری طراحی شود که تابع ورودی را به صورت شرطی اعمال کند. آزمایش این حالت به عهده شما میباشد. اما به صورت کلی انتظار میرود که قدرت توابع High-Order را درک کرده باشید.