مروری کوتاه و تاریخی بر زبان‌های برنامه‌نویسی Functional

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

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

آلن تورینگ و ماشین مکانیکی‌اش معروف به ماشین تورینگ
آلن تورینگ و ماشین مکانیکی‌اش معروف به ماشین تورینگ

شما برای انجام محاسبات در ماشین تورینگ به آن «دستور» می‌دهید که نوارهایش را جا‌به‌جا کند و یا روی نوار‌ها چیزی بنویسد تا در نهایت مقادیر روی نوار‌ها نتیجه مورد نظرتان را تولید کند. از همین رو، ماشین تورینگ یک ماشین «دستوری» است. فرمان‌های شما می‌تواند به شکل «این را روی نوار فعلی بنویس؛ نوار را به چپ ببر؛ مقدار روی نوار را بخوان؛ ...» باشد. در ادامه تحقیقات معلوم شد که این ماشین، یک ماشین محاسباتی بسیار قدرتمند است که توانایی محاسبه همه الگوریتم‌های محاسباتی را داراست. به عبارت دیگر، این ماشین می‌تواند هر محاسبه‌ای که کامپیوتر شخصی شما قادر به حساب کردن آن است را محاسبه کند. از این رو به این ماشین، و همه مدل‌های محاسباتی که در این حد قدرت دارند، Turing-complete می‌گویند.

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

آلونزو چرچ و محاسبات لامبدا
آلونزو چرچ و محاسبات لامبدا

با اینکه مدل نشانه‌گذاری ریاضیاتی چرچ بسیار ساده به نظر می‌رسید، ولی در عین حال نیز بسیار قدرت‌مند ظاهر شد و قابلیت «توصیف» محاسبات مختلف را دارا بود. این باعث شد که ذهن دانشمندان مختلف به این سوال معطوف شود که آیا مدل ریاضیاتی چرچ که با «ارزیابی عبارت‌های ریاضی» محاسبات را انجام ‌می‌دهد، قدرتی به اندازه ماشین مکانیکی تورینگ که با «اجرای دستورات» محاسبه می‌کند دارد؟ یا خیر؟

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

این دو رویکرد در محاسبات در نهایت به دو جریان از طراحی زبان‌های برنامه‌نویسی منجر شد. 1- زبان‌های برنامه‌نویسی «دستوری» که شما در هر لحظه به زبان دستور می‌دهید که چه کاری را انجام دهد، و 2- زبان‌های برنامه‌نویسی فانکشنال در گروه زبان‌های «توصیفی» که مبتنی بر محاسبات لامبدای چرچ توسعه یافت. به تعبیری، محاسبات لامبدا، مادر زبان‌های برنامه‌نویسی تابعی است.

همان‌طور که پیشتر نیز اشاره شد، در زبان‌های تابعی شما به «ارزیابی عبارت‌های ریاضیاتی» و در زبان‌های دستوری به «اجرای دستورات» می‌پردازید. تقریبا تمامی زبان‌هایی که می‌شناسید مانند C، C++، Go، Rust، Python، JavaScript، Ruby و Java جز زبان‌های دستوری تلقی می‌شوند، اگرچه ممکن است در طول زمان برخی از قابلیت‌های زبان‌های فانکشنال و محاسبات لامبدا را اضافه کرده باشند. پارادایم برنامه‌نویسی «شی گرایی» نیز زیر مجموعه‌ای از مدل‌های محاسباتی دستوری است.

در مقابل زبان‌هایی مانند Haskell، OCaml، F#، Clojure، Scala، Lisp و Elixir زبان‌های فانشکال و از دسته زبان‌های توصیفی هستند. این زبان‌ها نیز ممکن است در طول زمان برخی از جریان کارهای دستوری (مانند ST Monad ها در Haskell) را اضافه کرده باشند.

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

  • در زبان‌های شی‌گرا شما به زبان می‌گویید «این کار را انجام بده، سپس این کار را انجام بده و در نهایت این کار را انجام بده». در زبان‌های فانشکال شما چیزی که می‌خواهید را توصیف می‌کنید.
  • در زبان‌های شی‌گرا دیتا Mutable و قابل تغییر است، و متد‌های کلاس‌ها Impure هستند. به این معنی که این متد‌ها میتوانند Side-effect داشته باشند. به عنوان مثال شما در حین اجرای تابع Max که قرار است ماکسیمم دو عدد را حساب کند میتوانید به دیتابیس وصل شوید و یا یک موشک را لانچ کنید! در مقابل، در زبان‌های تابعی مقادیر متغیر‌ها و ساختمان‌های داده شما Immutable هستند، و ساید افکت‌ها بسته به زبان برنامه‌نویسی ممکن است وجود نداشته باشد، و یا کنترل شده باشد.
  • زبان‌های شی‌گرا برای پیشمایش از حلقه‌ها، و زبان‌های تابعی از توابع بازگشتی استفاده می‌کنند. از آنجایی که کامپیوتر‌های ما در سطح پردازنده به طور دستوری کار می‌کنند، زبان‌های تابعی باید عملیاتی به نام TCO (Tail-call optimization) را انجام دهند تا هم پرفورمنسی به اندازه حلقه‌ها داشته باشند و هم جلوی پرشدن Stack را بگیرند.
  • در زبان‌های شی‌گرا از دید برنامه‌نویس مقادیر و متغیرها با متد‌های موجود در کلاس‌ها فرق می‌کنند. در حالی که در زبان‌های فانکشنال توابع مانند متغیر‌ها به اصلاح First-class هستند و به یک شکل تعریف می‌شوند. هرجایی که بتوان متغیری تعریف کرد میتوان تابعی تعریف کرد، و هر جا که می‌توان مقدار را پاس داد، میتوان تابعی پاس داد.


یافتن اضلاع یک مثلث در زبان برنامه‌نویسی Haskell
یافتن اضلاع یک مثلث در زبان برنامه‌نویسی Haskell


از دیگر ویژگی‌های زبان‌های فانکشنال اعمال جزئی (Partial Apply)، اثبات بودن برای نظریه تایپ‌ها (Type Theory)، و ترکیب توابع (Composition) است.


اعمال جزئی

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


در تکه کد بالا که به زبان F# نوشته شده است، stringRepeat نام تابع، و پارامتر‌های n و str ورودی‌های تابع هستند. در خط چهارم، بدون اینکه پارامتر دوم به تابع پاس شود، خروجی در مقدار/تابع repeat3Times ذخیره شده است که خود تابعی است که با دریافت یک رشته، یک رشته دیگر را خروجی می‌دهد.

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


اثبات قضایا

در زبان‌های برنامه‌نویسی فانکشنال و Static-type، تایپ‌ها برای برنامه شما یک «قضیه» (Theorem) محسوب می‌شوند، و کد‌هایی که شما که در بدنه توابع می‌نویسید یک اثبات برای این قضایا تلقی می‌گردند. به دلیل نزدیکی ریاضیاتی این نظریه‌ها، تقریباً تمامی زبان‌های برنامه‌نویسی که برای اثبات قضایای ریاضی به کار می‌روند زبان‌های فانکشنال هستند. این امر باعث می‌شود که از بسیاری از خطاها در زمان کامپایل و پیش از اجرای برنامه جلوگیری شود.


ترکیب توابع

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

در تکه کد بالا، تابع getFirstChar با دریافت یک رشته، کاراکتر اول آن را خروجی می‌دهد. همچنین تابع isLetter نیز تابعی است که چک می‌کند آیا کاراکتر ورودی حرف هست یا خیر؟

حال ما می‌توانیم با ترکیب این دو تابع، تابع جدیدی تعریف کنیم که چک کند آیا کاراکتر اول یک رشته حرف است یا نه.


بیشتر بخوانید

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