برای یک مسابقه برنامهنویسی یک سوال ساده طرح کردم که به راحتی قابل حل باشه ولی یک شرط مهم برا حل گذاشتم، برای حل تنها مجاز به استفاده از زبان Haskell هستید. اما چرا هسکل مهمه و چرا همچین تصمیمی گرفتم؟ در ادامه متوجه میشوید.
این روزها هرکس بخواهد برنامهنویسی را شروع کند، بین زبانهای پایتون و سی و یا جاوااسکریپت یا نهایتا جاوا و سیپلاسپلاس انتخاب میکند و برنامهنویسی رویهای (procedural) و در ادامه شیگرا (Object Oriented) را یاد میگیرد و احتمالا در ادامه هم در همین بخش ادامه میدهد و فریمورکهای جدید و ...
اما برنامهنویسی فانکشنال (functional programming یا FP) مورد غفلت واقع شده اما برنامهنویسی فانکشنال چی هست؟ به طور خلاصه یک پارادایم برنامهنویسی در مقابل برنامهنویسی رویهای و شیگراست.
با توجه به قدمت برنامهنویسی رویهای، زبانهای برنامهنویسی رویهای زبانهای پرقدمتی مثل FORTRAN و BASIC و C و COBOL هستند. این دسته از زبانها متکی به صدا زدن procedure (روتین یا فانشکن) هستند که هر روتین مجموعهای از دستورات است. در واقع کدها در قالب routine ها نوشته میشوند.
از بزرگان برنامهنویسی شیگرا میشود به جاوا و سیپلاسپلاس، سیشارپ و جاوااسکریپت و .. اشاره کرد که مفاهیم برنامهنویسی شیگرا مثل ابجکت، کلاس، پنهانسازی(encapsulation)، وراثت(inheritance) و چندریختی(polymorphism) را پشتیبانی میکنند. در این پارادایم، کدها در غالب متدهایی برای اشیا نوشته میشوند و قابل فراخوانی روی هر شی (یا در مواردی روی کلاس) هستند.
اما زبانهای فانکنشال مثل Haskell و #F و Erlang و elixir و Clojure روش فکری متفاوتی دارند! در این زبانها کدها در قالب فانکشنها نوشته میشوند (مثل برنامهنویسی رویهای؟) اما فانکشنها شهروندان درجهاول در زبان هستند یعنی کار کردن با آنها به راحتی کار کردن با متغیرهاست و امکان pass شدن و ترکیب شدن و .. را دارند.
توجه: مثالهایی که زده شد دقیق نیستند چون اکثر زبانها چند پارادایمی هستند و نمیتوان گفت سی فقط رویهای است چون با پوینتر به فانکشن قابلیتهای فانکشنال را هم تا حدی میسر کرده است و یا مثلا جاوا در نسخه 8 امکانات برنامهنویسی فانکشنال را تا حدی میسر کرده است.
ریشه و پایهی زبانهای فانکشنال، lambda calculus است که ایدهی نوشتن function و حل مساله به روش بازگشتی را پایه گذاشت.
اما مفاهیمی که در زبانهای فانکشنال با آنها سرو کار داریم چه چیزهایی هستند؟
۱- فانکشنهای مرتبه بالا(higher order function) فانکشنی که یک یا چند فانکنش به عنوان ورودی بگیرد و فانکشن به عنوان خروجی بدهد، مثل انتگرال!
۲- فانکشنهای خالص(pure function): تابع pure تابعیست که خروجیاش دقیقا از روی ورودیهایش ساخته میشود (مثل همهی توابع ریاضی)، یعنی این تابع با بیرون از تابع و بقیهی دنیا (به جز از طریق ورودیها) هیچ ارتباطی ندارد و هیچ اثری هم روی بیرون از تابع (بقیه دنیا) نمیگذارد به جز محاسبهی خروجی! به طور عملیتر هیچ متفیر گلوبالی وجود ندارد، کار با IO و تعامل با کاربر و هرگونه side-effectی وجود ندارد.
۳- توابع بازگشتی (recursive functions): توابعی که برای حل یک مساله، از حل زیر مسالههایی با ابعاد کوچکتر کمک میگیرند (مشابه استدلال استقرایی که f(n) را فرض میکنیم و برای f(n+1) اثبات میکنیم) برای حل مساله با اندازه k از همان تابع برای ورودی با اندازه k-1 (یا به هر صورتی کوچکتر از k) کمک میگیرند و با استفاده از آن، مساله را برای ورودی با اندازه k حل میکنند. البته یک شرط پایه (مثل پایه استقرا) نیز وجود دارد که برای اندازه k=1 است که جواب بدیهی دارد.
کمی بیشتر در مورد برنامهنویسی تابعگرا میتوانید اینجا بخوانید.
اما برگردیم به هسکل:
هسکل یک زبان فانکشنال است (جزو مطرحترین زبانهای فانکشنال است) ولی نه یک زبان فانکشنال معمولی، یک زبان purely functional است.
هسکل statically typed است یعنی تایپ ها باید در زمان کامپایل معلوم باشند، این کار برای به حداقل رساندن خطا و اشتباه برنامهنویس است.
هسکل یک زبان تنبل (lazy) است. pure function ها به زبان امکان تنبلی (laziness) رو میدهند، تنبلی یعنی اینکه تا زمانی که واقعا به چیزی نیاز نداریم، برایش زمان و انرژی نگذاریم!
توی زبان برنامهنویسی به این معنیست که تا زمانی که به یک داده واقعا نیاز نیست، محاسبه نشود، این امر چطوری امکانپذیره؟ مقدار ها از توابع ساخته میشوند و توابع زبان هم همگی pure هستند پس تفاوتی ندارد که همان موقع که فراخوانی میشوند اجرا شوند و خروجی را آماده کنند یا هرموقع به خروجیشان واقعا نیاز بود!
این laziness امکانات عجیبی رو برای زبان (و بیشتر از زبان، برای برنامهنویس) فراهم میکند مثلا میتوانیم یک لیست شامل همهی اعداد طبیعی داشته باشیم. دقیقا مشخص است که این لیست شامل چه عددهاییست ولی مقدار تک تکشون دقیقا محاسبه نشده و هربار به هر مقداری نیاز داشته باشیم محاسبه میشود!
اما همهی این تفاوتها و تغییر در پارادایم به چه علتی؟
در زبانهای فانشکنال به جای how to do به what to do پرداخته میشود یعنی برنامهای که نوشته میشود به جزییات اتفاقات سطح پایین کاری ندارد و صرفا تصریح میکند از برنامه چه انتظاری داریم! فرض کنید یک لیست از اعداد زوج میخواهیم، در زبانهای رویهای باید یک حلقه بزنیم و دقیقا بدانیم تا کجا به اعداد زوج احتیاج داریم و برای هر عدد، در صورتی که زوج بود باید به آرایه یا list اضافهاش میکردیم اما در زبان فانکشنال کافیست بگوییم یک لیست اعداد طبیعی رو فیلتر کن، چه فیلتری؟ فیلتری که زوجها رو برگرداند.
یا فرض کنید میخواهیم بررسی کنیم که یک عدد، اول هست یا نه، کافیه یه لیست از اعداد ۲ تا n-1 بسازیم و به هسکل بگوییم که بر هیچ کدام بخشپذیر نباشد، با یک خط کد امکان پذیر است!
این تفاوتها (و احتمالا یه سری تفاوت که من اینجا نیاوردم و خودم بلد نبودم) برنامههای فانکشنال را گویاتر میکند، تغییر در کد را راحتتر امکانپذیر میسازد و کدها را کوتاه تر میکند.
یک خصوصیت خوب دیگر این زبانها قدرتشان در همزمانی (concurrency) است که به خاطر ذات immutable بودن دادهها و مستقل بودن هر تابع از دیگر قسمتهای برنامه میسر شده است.
البته انتقاداتی هم به سرعت و پرفرمنس این زبانها وارده چون دقیقا مشخص نمیکنیم زبان چطوری عمل کند، ممکن است سرعت اجرای برنامه پایین بیاید که به نظر من با درک صحیح از اتفاقات پشت صحنه میتوان سربار اجرایی را به حداقل رساند.
برای شروع به کار با زبان هسکل ، می توانید از repl آنلاین آن در سایت رسمی خود هسکل استفاده کنید، البته کامپایلرهای انلاین هم در دسترس هستند.
کامپایلر خوب و شناختهشدهی هسکل، ghc است که در اکثر سیستمعامل ها قابل نصب و استفاده است.
همچنین در این لینک میتوانید توضیحات دیگری را بخوانید.
آموزشهای شروع به کار خوب هسکل هم موارد زیر است:
دو کتاب بسیار خوب هم پیدا کردم، یکی به زبان انگلیسی و قابلیت خواندن آنلاین و یکی به زبان فارسی که میتوانید فصلهای اولش را در این لینک بخوانید. (بسیار توصیه میشود)