چرا من عاشق برنامه نویسی فانکشنال هستم؟

ترجمه و تالیف : فاطمه شیرزادفر
تاریخ انتشار : 27 شهریور 99
خواندن در 5 دقیقه
دسته بندی ها : برنامه نویسی

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

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

برنامه نویسی فانکشنال ریاضی را به برنامه نویسی می‌آورد

اولین دلیل من برای برنامه نویسی فانکشنال این است که ریاضی را به برنامه نویسی بازمی‌گرداند. در دانشگاه، من در رشته ریاضی تحصیل کردم؛ احتمالاً هرگز از هندسه، دیفرانسیل یا تئوری گروه‌ها استفاده عملی نخواهم کرد، اما هیچ یک از این دوره‌ها نیز اتلاف وقت نبودند.همه این‌ها قدرت انتزاع (abstraction) را به من آموختند، اینکه چگونه می‌توانیم مفاهیم بزرگی که در زیر مشکلات به ظاهر نامربوط قرار گرفته‌اند را پیدا کنیم و ببینیم

در برنامه نویسی فانکشنال، شما در تمام مدت با انتزاع‌هایی مثل mondها و functorها روبه‌رو می‌شوید. برنامه نویسی فانکشنال ریشه عمیقی در نظریه دسته‌ها(category theory) دارد، شاخه‌ای از ریاضیات که به بررسی اشیاء و روابط آن‌ها می‌پردازد. به عنوان مثال نظریه دسته‌ها به ما می‌گوید که monad فقط یک monoid در دسته endofunctor ها است.معنی این اصطلاحات و این کلمات چیست؟ خب من هیچ ایده‌ای ندارم، اما باید آن‌ها را دریابم!

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

 این کار شما را وادار می‌کند که متفاوت فکر کنید

دلیل دوم من برای یادگیری برنامه نویسی فانکشنال این است که من را مجبور می‌کند متفاوت فکر کنم.

بعد از کنار گذاشتن بیسیک در دهه ۹۰، برای اولین بار کار با جاوا و سی را در دانشگاه یادگرفتم. برنامه‌ها با if و حلقه‌ی for نوشته می‌شدند. داده‌های in-place با فانکشن‌ها یا متد فراخوانی می‌شدند که هیچ چیزی را برنمی‌گرداندند.

درک If-clauses، حلقه‌های for و in-place mutation ها برای ما انسان‌ها راحت است، ‌زیرا به این ترتیب داده‌ها را به صورت شهودی پردازش می‌کنیم.

اگر به شما لیستی از n مهارت که باید یادبگیرید، بدهند، به غیر از اینکه مهارتی را از قبل بدانید؛ به چه شکل نوشته می‌شود؟ الگوریتم آن در زیر آورده شده است :

i=1 این‌گونه قرار دهید

مهارت I را از لیست یادبگیرید و بررسی کنید که آیا این مهارت را می‌دانید یا خیر. اگر آن را نمی‌دانید یادبگیرید.

If i=N ، خارج شو (exit)‌، ‌در غیر این صورت، I را برابر با i = i+1 قرار دهید و به ۱ بروید.

این یک برنامه imperative (دستوری)‌ است که یک دستور پس از دیگری حالت برنامه را تغییر می‌دهد( مهارت‌های شما). به نظر می‌رسد، جهان از اشیاء قابل تغییر ساخته شده است. و این‌گونه است که کامپیوتر هم کار می‌کند، یک statement بعد از دیگری حالت برنامه را تغییر می‌دهد.

حالا تصور کنید که به شما گفته شده باید برای برنامه‌ای بدون حتی یک خط if-loop کد بنویسید؛ همچنین استفاده از اشیاء قابل تغییر نیز ممنوع است. آنچه شما مجاز به انجام آن هستید ایجاد اشیاء جدید و نوشتن فانکشن‌های خالص و referentially transparent است. referentially transparent به این معنی است که یک function call می‌تواند با مقدار برگشتی خودش جابه‌جا شود بدون اینکه در برنامه تغییری ایجاد شود( یا به عبارتی خروجی توابع با پارمترهای یکسان همیشه یکی است و تغییر نمی‌کنه که به این اصل referential transparency میگیم) برای مثال، این فانکشنreferential transparency نیست:

def square(x):
    print(f"Computing the square of {x}") 
    return x*x

شما نمی‌توانید مربع (X) را با X*X جایگزین کنید و انتظار داشته باشید که این برنامه بدون تغییر باقی بماند. فانکشن خالص، فانکشنی است که اصل referential transparency، برای همه‌ی آرگومان‌ها referential transparency باشد.

ناگفته نماند که چنین محدودیت‌هایی شما را وادار می‌کند که در مورد کدنویسی متفاوت فکر کنید؛ و برای من این مورد بسیار خوب است.

اخیر در پایتون و جاوااسکریپت بیشتر کدنویسی کرده‌ام، درحالی که من عاشق انعطاف پذیری و سادگی هر دو زبان هستم، و همیشه چیز جدیدی برای یادگیری درباره هردوی آن‌ها وجود دارد اما بازهم فکر نمی‌کنم آن‌ها فرصت‌های زیادی برای یادگیری مفاهیم جدید ارائه دهند. آخرین باری که چیزی درباره پایتون یادگرفتم وقتی بود که ما یک ابزار برای کامند لاین ساختیم که از asyncio استفاده کردیم یا هنگامی که مجبور شدم generic ها را در typing module درک کنم. و بیشتر اوقات، کد شامل همان if-clauses و حلقه‌های for است مخصوصاً در بعضی از فریمورک‌های جدید.

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

البته شاید کافرما یا حتی اعضای تیم به این مسأله که شما چگونه چیزی را حل می‌کنید اهمیت ندهند و آن را درک نکند و این یکی از دلایلی است که من برنامه نویسی فانکشنال را یک سرگرمی برای خودم می‌دانم. برنامه‌های کاملاً فانکشنال باید همراه با تیمی باشد که همگی دانش کافی برای حل مشکلات به این روش داشته باشند؛ در چنین تیمی هزینه یادگیری مفاهیم جدید نیز پایین‌تر خواهد بود زیرا این مفاهیم جدید ممکن است پایه کد همه را بهبود ببخشند.

از مثال‌های بالا ممکن است این به نظر برسد که برنامه نویسی imperative (دستوری) غیر فانکشنال یا "non-functional" است. برای اینکه ببینید اینگونه نیست، در اینجا بخشی از کد Scala از کتاب Functional Programming in Scala (کتاب قرمز رنگ) آورده شده است:

val factorialREPL: IO[Unit] = sequence_(
    IO { println(helpstring) },
    doWhile { IO { readline } } { line =>
        when (line != "q") {
            for {
                n <- factorial(line.toInt)
                _ <- IO { println("factorial: " + n) }
            }
        } yield ()
    }
)

این یک برنامه فانکشنال است که به روش imperative (دستوری)‌نوشته شده است. چرا یک حلقه for وجود دارد؟ این syntactic sugar اسکالا برای برای ترکیب فانکشن‌هایی مثل map،filter و flatMap است. ( syntactic sugar جنبه‌ای از سینتکس یک زبان برنامه نویسی است که برنامه‌ها را در خواندن، نوشتن یا درک آسان‌تر می‌کند)

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

در ابتدا این مفاهیم بیشتر برای من تئوریک به نظر می‌رسید و فکر می‌کردم زیاد با FP روبه رو نمی‌شوم. با این حال به محض اینکه اولین کار خودم را شروع کردم، برنامه‌نویسان باتجربه به من می‌گفتند از نوشتن کد با implicit side effects (عوارض جانبی ضمنی) و mutable state (حالت قابل تغییر) تاجایی که ممکن است خودداری کنید. من در آن زمان نفهمیدم که این ایده‌ها چه ربطی به FP دارند، اما اکنون می‌توانم ببینم که چه تعداد ایده‌هایی در FP ساخته شده‌اند.

به عنوان نمونه، چگونه FP می‌تواند به نوشتن کد تمیزتر کمک کند؟ خب شما فانکشنی مثل این دارید:

const containsFinnishLapphund: (jpegBase64: String) => boolean = ...

این کد بررسی می‌کند که آیا یک تصویر حاوی Finnish Lapphund (نوعی سگ) است یا نه ( در ادامه نویسنده درباره مسابقه‌ای که برای این نوع سگ برگزار می‌شود صحبت می‌کند که می‌توانید با کلیک روی لینک آن را ببینید). Signature (امضا)‌ می‌گوید که این فانکشن یک رشته رمزگذاری شده base64 را می‌گیرد و یک boolean برمی‌گرداند. بر اساس Signature، انتظار دارم که این فانکشن دارای implicit side effects (عوارض جانبی ضمنی) نباشد. بنابراین، با اطمینان می‌توانم این فانکشن را برای ۱۰۰ تصویر به طور موازی و بدون نگرانی صدا بزنم، برای مثال می‌توانم در مورد شرایط مسابقه، وقفه‌ها یامحدودیت سرعت ضربه زدن، از API های خارجی استفاده کنم.

در زبان‌های non-functional،‌ نوشتن کد وظیفه توسعه‌دهنده است که تعجبی هم ندارد اما در Haskell، یک type signature مثل این:

containsFinnishLapphund :: String -> Bool

عوارض جانبی قابل مشاهده مثل ذخیره تصویر در یک محل را غیرممکن می‌کند. اگر این فانکشن اصرار بر ساخت یک network call یا ورود به کنسول داشته باشد، به یک type signature دارد.

containsFinnishLapphund :: String -> IO Bool

IO typeclass در اینجا این را explicit (غیر ضمنی) می‌کند که فانکشن کاری را در دنیای خارجی انجام می‌دهد.

نمونه دیگر از ایده‌های FP که امروزه سبک‌‌های خوبی در برنامه نویسی درنظر گرفته می‌شوند، سبک declarative (اعلانی) است. به عنوان مثال، اکثر برنامه‌نویسان امرزوه موافقت می‌کنند که عناصر را از یک آرایه حذف کنند و بقیه را مربع بگذارند، مثل این:

const double = (arr) => 
    arr.filter(v => v % 2 === 0).map(v => v*v);

 که به این ترجیح داده می‌شود:

const double = (arr) => {
    const newArr = []; 
    for (const i = 0; i++; i < arr.length) {
        if (arr[i] % 2 === 0) {
            newArr.push(arr[i] * arr[i]);
        }
    }
    return newArr;
}

در زبان‌های فانکشنال، روش اول(سابق) راه پیش‌فرض برای حل مسأله خواهد بود. بازهم این به این معنا نیست که سبک declarative (اعلانی) بهتر از سبک imperative (دستوری) است. اما نشان می‌دهد که سبک اعلانی جوانب مثبت خود را دارد. در FP ، سبک اعلانی می‌تواند با فانکشن ترکیبی و سبک بدون نقطه، push شود:

square :: Int -> Int
square num = num * num

isEven :: Int -> Bool
isEven n = n `mod` 2 == 0

double :: [Int] -> [Int]
double = map square . filter isEven

 برای من کدی مثل این ظریف و زیباست. درحالی که طول می‌کشد تا به فانکشن ترکیبی و سبک بدون نقطه عادت کنید؛ اما به نظرم ارزش تلاش را دارد.

 و در آخر

این بخشی از تجربیات من بود. من عاشق برنامه نویسی فانکشنال هستم زیرا به من دلیل می‌دهد تا دوباره ریاضی بخوانم و این است که مرا مجبور می‌کند متفاوت فکر کنم تا روش‌های خوبی برای برنامه نویسی به وجود آید. ار وقتی که برای مطالعه گذاشتید ممنونم. لطفاً اگر نظری دارید در بخش نظرات با ما به اشتراک بگذارید.

منبع

گردآوری و تالیف فاطمه شیرزادفر
آفلاین
user-avatar

تجربه کلمه‌ای هست که همه برای توصیف اشتباهاتشون ازش استفاده میکنن، و من همیشه دنبال اشتباهات جدیدم! برنامه‌نویس هستم و لینوکس‌ دوست

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید