Functional Programming یا برنامه نویسی تابعی - قسمت اول
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هفت دقیقه

 آشنایی

این قسمت از مقاله به ایده اصلی برنامه نویسی تابعی و دلیل وجودی آن خواهد پرداخت. هیچ شکی نیست که بزرگترین چالش در توسعه نرم افزار‌های بزرگ، پیچیدگی آن است. تغییرات همیش اجتناب ناپذیر هستند. به خصوص زمانی که صحبت از پیاده سازی امکان جدیدی باشد، پیچیدگی اضافه خواهد شد. در نتیجه منجر به سخت شدن فهمیدن کد می‌شود، زمان توسعه را بالاتر می‌برد و باگ‌های ناخواسته را به وجود خواهد آورد. همچنین تغییر هر چیزی در دنیای نرم افزار بدون به وجود آوردن رفتار‌های ناخواسته و یا اثرات جانبی، تقریبا غیر ممکن است. در نهایت همه این موارد می‌توانند سرعت توسعه را پایین برده و حتی باعث شکست پروژه‌های نرم افزاری شوند. سبک‌های کد نویسی دستوری (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 هم نوعی تابع هستند.


شفافیت توابع (Referential Transparency)

به طور ساده با نگاه کردن به ورودی‌های تابع و نام آن‌ها باید بتوانیم کاری را که انجام میدهد، حدس بزنیم. یعنی یک تابع باید بر اساس ورودی‌های آن کاری را انجام دهد و نباید یک پارامتر Global آن را تحت تاثیر قرار دهد. پارامتر‌های Global میتوانند یک Property در سطح یک کلاس باشند، یا یک شیء که وضعیت آن تحت کنترل تابع نیست؛ مانند شی DateTime. به مثال زیر توجه کنید:
public int CalculateElapsedDays(DateTime from)
{
   DateTime now = DateTime.Now;
   return (now - from).Days;
}
این تابع شفاف نیست. چرا؟ چون امروز، یک خروجی را میدهد و فردا یک خروجی دیگر را! به بیان دیگر وابسته به یک شیء سراسری DateTime.Now است.
آیا میتوانید این تابع را شفاف کنیم؟ بله!
چطور؟ به سادگی! با تغییر پارامتر‌های ورودی:
 public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;
در مثال بالا، ما وابستگی به یک شیء سراسری را از بین بردیم.


صداقت توابع (Function Honesty)

صداقت یک تابع یعنی یک تابع باید همه اطلاعات مربوط به ورودی‌ها و خروجی‌ها را پوشش دهد. به این مثال دقت کنید:
public int Divide(int numerator, int denominator)
{
   return numerator / denominator;
}
آیا این تابع شفاف است؟ بله.
آیا این همه مواردی را که از آن انتظار داریم پوشش میدهد؟ احتمالا خیر!

اگر دو عدد صحیح را به این تابع بفرستیم، احتمالا مشکلی پیش نخواهد آمد. اما همانطور که حدس میزنید اگر پارامتر دوم 0 باشد چه اتفاقی خواهد افتاد؟
var result = Divide(1,0);
قطعا خطای Divide By Zero را خواهیم گرفت. امضای این تابع به ما اطلاعاتی درباره خطاهای احتمالی نمی‌دهد.

چگونه مشکل را حل کنیم؟ تایپ ورودی را به شکل زیر تغییر دهیم:
public static int Divide(int numerator, NonZeroInt denominator)
{
   return numerator / denominator.Value;
}
NonZeroInt یک نوع ورودی اختصاصی است که خودمان طراحی کرده‌ایم که تمام مقادیر را به جز صفر، قبول میکند.

به طور کلی تمرین زیادی لازم داریم تا بتوانیم با این مفاهیم به طور عمیق آشنا شویم. در این مقاله قصد دارم جنبه‌های ابتدایی برنامه نویسی تابعی مانند  Functions as first class values ، High Order Functions و Pure Functions را مورد بررسی قرار دهم.

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);
در این مثال، تابع، First-class value می‌باشد؛ چون شما می‌توانید آن را به یک متغیر نسبت دهید و به عنوان ورودی به تابع بعدی بدهید. در مدل برنامه نویسی تابعی، تلقی شدن توابع به عنوان مقدار، ضروری است. چون به ما امکان تعریف توابع High-Order را میدهد.


High-Order Functions (HOF)

توابع مرتبه بالا! یک یا چند تابع را به عنوان ورودی می‌گیرند و یک تابع را به عنوان نتیجه بر میگرداند. در مثال بالا 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;
}
1. وظیفه چرخیدن روی آیتم‌های لیست، مربوط به Where می‌باشد.
2. ملاک تشخیص اینکه چه آیتم‌هایی در لیست باید وجود داشته باشند، به عهده متدی می‌باشد که آن را فراخوانی میکند.

در این مثال، تابع Where، تابع ورودی را به ازای هر المان، در لیست فراخوانی میکند. این تابع می‌تواند طوری طراحی شود که تابع ورودی را به صورت شرطی اعمال کند. آزمایش این حالت به عهده شما می‌باشد. اما به صورت کلی انتظار می‌رود که قدرت توابع High-Order را درک کرده باشید.


Pure Functions

توابع خالص در واقع توابع ریاضی هستند که دو مفهوم ابتدایی که قبلا درباره آن‌ها صحبت کردیم را دنبال می‌کنند؛ شفافیت و صداقت توابع. توابع خالص نباید هیچوقت اثر جانبی (side effect) ای داشته باشند. این یعنی نباید یک global state را تغییر دهند و یا از آن‌ها به عنوان پارامتر ورودی استفاده کنند. توابع خالص به راحتی قابل تست شدن هستند. چون به ازای یک ورودی، یک خروجی ثابت را بر میگردانند. ترتیب محاسبات اهمیتی ندارد! این‌ها بازیگران اصلی یک برنامه تابعی می‌باشد که می‌توانند برای اجرای موازی، محاسبه متاخر ( Lazy Evaluation ) و کش کردن ( memoization ) استفاده شوند.

در ادامه این سری مقالات، به پیاده سازی‌ها و الگوهای رایج برنامه نویسی تابعی با #C بیشتر خواهیم پرداخت.
  • #
    ‫۳ سال و ۱ ماه قبل، پنجشنبه ۱۴ مرداد ۱۴۰۰، ساعت ۱۶:۴۱
    خیلی ممنونم که این بحث رو مطرح کردید. ولی چیزی که واقعا متوجه نمیشم اینه که چرا باید برنامه نویسی تابعی رو رقیب یا جایگزین برنامه نویسی شی گرا دونست. به نظر من مفاهیمی که این دو روش ارائه میکنن مکمل هم هستن. به عبارتی مطالب مطرح شده میتونه به عنوان رهنمون هایی در برنامه نویسی شی گرا تلقی بشه.
    البته در منابع خارجی که مطالعه میکردم تفاوت اصلی رو در این دونسته بودن که در برنامه نویسی تابعی داده‌ها در شی نگهداری نمیشه و باید در تابع نگه داری و جا بجا بشه! البته نمیدونمن چطور این کار به صورت مطلق میسر هست!
    در کل من فکر میکنم اگه قرار باشه این کار به صورت مطلق در تمام حالات انجام بشه خودش به ضد مفاهیمی که در این رهنمون‌ها تاکید شده تبدیل خواهد شد.
    فکر نمیکنم بدون استفاده از مفاهیم شی گرایی بشه یه پروژه واقعی و بزرگ رو با استفاده از صرفا مفاهیم برنامه نویسی تابعی به صورت بهتری پیاده کرد!
    خیلی ممنون میشم اگه اساتید این بحث رو ادامه بدن و توضیحاتی بدن که آیا من دچار بدفهمی شدم یا خیر.
    • #
      ‫۳ سال و ۱ ماه قبل، جمعه ۱۵ مرداد ۱۴۰۰، ساعت ۰۲:۰۶
      تفاوت‌ها که زیاده. برنامه نویسی تابعی یه ویژگی خیلی مهم به اسم Immutable  داره. یعنی مقدار متغیرها رو نمیشه تغییر داد. این ویژگی هم به این دلیل هستش که برنامه دیگه نگران thread safety نباشه و بتونه از حداکثر هسته‌های پردازنده و موازی سازی استفاده کنه. کلا برای برنامه نویسی تابعی باید برنامه تون و مساله ای که می‌خواید حل کنید امکان مدلسازی ریاضی رو داشته باشه که حتما کار سختی هستش و نمیشه همه چی رو به راحتی مدسازی ریاضی کرد. در کل هدف oop و functioanal و فلسفه استفاده شون متفاوته و مقایسه اونها درست بنظر نمیرسه.
    • #
      ‫۳ سال قبل، جمعه ۱۵ مرداد ۱۴۰۰، ساعت ۲۰:۰۸
      رقیب دونستن این‌ها اشتباهه!

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

      دلیل این که ما در مورد این قضیه داریم صحبت میکنیم اینه که ما از یه زبونی مثل C# که اصطلاحا Multi-paradigm هست داریم استفاده میکنیم برای کد نویسی که ناخواسته مجبوریم اصول شی گرایی رو توش رعایت کنیم

      ولی زمانی که شما با یه زبونی مثل haskell یا حتی f# کار میکنید متوجه میشید که این دو تا میتونن کلا مستقل باشن

      • #
        ‫۳ سال قبل، جمعه ۱۵ مرداد ۱۴۰۰، ساعت ۲۳:۲۲
        آیا میتونید پروژه بزرگی نام ببرید با کاربردهای معمول، که توش صرفا از روش فانکشنال استفاده شده باشه؟ آیا در گیت هاب میشه نمونه هایی پیدا کرد؟ مثلا CMS مطرحی داریم که کاملا بر اساس برنامه نویسی فانکشنال باشه؟ یعنی میخوام بگم چقدر اینکه کسی دغدغه است این باشه که باید شی گرا یاد بگیره یا فانکشنال معنی داره؟ از نظر من دست کم نو آموز‌ها نباید چنین دغدغه ای داشته باشن!

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

        • #
          ‫۳ سال قبل، شنبه ۱۶ مرداد ۱۴۰۰، ساعت ۰۳:۲۲
          در مورد موارد مطرحی که باشه یا نه به این راحتی نمیشه نظر داد و باید کد‌ها رو بررسی کرد  ، یا حداقل من نمیشناسم که صرفا فانکشنال باشه قطعا اگه سورس کد هایی که با زبان haskell یا f# یا سایر زبان هایی که پارادایم اون‌ها فانکشنال هست رو نگاه کنیم میتونیم موارد زیادی رو پیدا کنیم

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




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