برنامه نویسی تابعی در سوئیفت – راهنمای مقدماتی


در این مقاله به معرفی مفهوم «برنامه نویسی تابعی» (Functional Programming) به زبان ساده میپردازیم و برخی از مزیتهای آن را بیان میکنیم. در این مقاله صرفاً به معرفی مبانی نظری و بررسی نمونه کدها اکتفا میکنیم. در مقالات بعدی از این سلسله مطالب با تفصیل بیشتری در این خصوص صحبت خواهیم کرد.
تعریف برنامهنویسی تابعی
پیش از هر چیز باید گفت که برنامهنویسی تابعی یک زبان یا ساختار نیست؛ بلکه یک پارادایم برنامهنویسی محسوب میشود، یعنی روشی برای حل مسائل از طریق تجزیه فرایندهای پیچیده به انواع سادهتر است. همان طور که از نام این پارادایم برمیآید، واحد تشکیلدهنده این رویکرد تابع است و هدف از تابع، اجتناب از تغییر یافتن حالت یا «تغییرپذیری مقادیر» (mutating values) خارج از حیطه تعریفشان است. در زبان برنامهنویسی Swift همه اینها به معنی این است که تا کمترین حد ممکن از var استفاده کنیم.
خوشبختانه Swift مزیتهای رویکردهای تابعی را شناسایی کرده و روشهایی برای به خدمت گرفتن آنها پیشبینی کرده است. تمرکز این مقاله بر روی بررسی نمونههایی از این رویکردها و کاوش روشهای سودمند بودن آنها است.
مقایسه رویکردهای دستوری (Imperative) و تابعی
برای این که مزیتها و ویژگیهای برنامهنویسی تابعی را درک کنیم، باید یک مسئله ساده را که به این دو روش متمایز حل شده است مقایسه کنیم. ابتدا رویکرد دستوری را بررسی میکنیم. در رویکرد دستوری که در چارچوب مورد بحث ما متضاد رویکرد تابعی است گزارههایی برای تغییر حالت، درون برنامه مورد استفاده قرار میگیرند:
//Imperative Approach var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] for i in 0..<numbers.count { let timesTen = numbers[i] * 10 numbers[i] = timesTen } print(numbers) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
در ادامه معنی دستوری بودن این رویکرد را بررسی میکنیم. دقت کنید که ما مشغول دستکاری مقادیر درون آرایه تغییرپذیر به نام numbers هستیم و سپس آنها را در کنسول نمایش میدهیم. به سؤالهای زیر پاسخ دهید. به زودی پاسخهای ما را نیز مشاهده خواهید کرد:
- ما در کد خود به چه فرایندی قصد داریم برسیم؟
- چه اتفاقی رخ میدهد اگر نخ دیگری تلاش کند در طی این فرایند به آرایه numbers دسترسی پیدا کند؟
- اگر در ادامه بخواهیم به مقادیر اصلی ذخیره شده در numbers دسترسی یابیم، چه رخ میدهد؟
- چگونه میتوان این کد را به روشی پایدار و مطمئن تست کرد؟
اگر پاسخهای خود به سؤالات فوق را در نظر دارید، اینک به کد زیر که با رویکرد تابعی نوشته شده است نیز توجه کنید:
//Functional Approach let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] extension Array where Element == Int { func timesTen() -> [Int] { var output = [Int]() for num in self { output.append(num * 10) } return output } } let result = numbers.timesTen() print(numbers) //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(result) //[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
ما در این رویکرد نتیجه مشابهی در کنسول دریافت میکنیم؛ اما با روشی کاملاً متفاوت با مسئله مواجه میشویم. دقت کنید که این بار آرایه numbers تغییرناپذیر است و با کلیدواژه let تعریف شده است. ما فرایند ضرب کردن اعداد را به درون متد ذخیره شده در بسطی از آرایه انتقال دادیم. با این که همچنان از حلقه for استفاده میکنیم و متغیری به نام output را بهروزرسانی میکنیم؛ اما حیطه تعریف متغیر محدود به متد است. به طور مشابه آرگومان ورودی ما که در این مورد numbers است به صورت «با مقدار» به متد ارسال میشود و به همین دلیل حیطه تعریف آن مانند «خروجی» (output) است. این متد فراخوانی میشود و میتوانیم numbers و همچنین result را در کنسول نمایش دهیم. اینک یک بار دیگر سه سؤالی که در ابتدای این مقاله اشاره کردیم را بررسی میکنیم.
1. ما در کد خود قصد داریم به چه چیزی دست یابیم؟
در این مثال، ما یک فرایند نسبتاً ساده ضرب کردن در 10 را برای اعداد درون یک آرایه اجرا میکنیم. در قطعه کد «دستوری» (Imperative) باید مانند یک رایانه فکر کنیم و از دستورهای ارائه شده در یک حلقه for برای تعیین خروجی پیروی کنیم. خود کد نشان میدهد که نتیجه چگونه به دست آمده است. در سوی دیگر در رویکرد تابعی چگونگی پیادهسازی در متد پیچیده شده است و از آنجا که متد در یک فایل جداگانه پیادهسازی شده است، ما تنها میتوانیم ()numbers.timesTen را ببینیم. در واقع در این رویکرد کد چیزی که به دست میآید را نشان میدهد، اما در مورد چگونگی به دست آوردن آن چیزی نمیگوید. این روش «برنامهنویسی اعلانی» (Declarative Programming) نامیده میشود و مسلماً موافق هستید که این روش ترجیح بیشتری دارد. در رویکرد دستوری، توسعهدهنده مجبور است طرز کار کد را درک کند تا بفهمد برنامه چه کار میکند. برنامهنویسی تابعی به میزان زیادی گویاتر است و به توسعهدهنده این فرصت را میدهد که درک کند هر متد چگونه کار خود را انجام میدهد.
2. در صورتی که نخ دیگری تلاش کند تا در طی این فرایند به آرایه numbers دسترسی داشته باشد، چه رخ میدهد؟
مثالهای فوق در محیطی جداگانه مطرح شدهاند؛ در حالی که اگر در یک محیط چند نخی پیچیده اجرا شوند، ممکن است دو نخ مجزا تلاش کنند تا همزمان به منابع یکسانی دسترسی داشته باشند و نتیجه این فرایند به طور بدیهی بر اساس ترتیب این دسترسی تعیین خواهد شد. این وضعیت «شرایط رقابت» (Race Condition) نامیده میشود و میتواند منجر به رفتار بسیار غیر قابل پیشبینی و حتی ناپایداریهایی شود که موجب از کار افتادن برنامه میشود.
رویکرد تابعی در مقام قیاس هیچ عارضه جانبی ندارد. به بیان دیگر، خروجی متد هیچ یک از مقادیر ذخیره شده در سیستم ما را تغییر نمیدهد و خروجی متد صرفاً بر اساس ورودی تعیین میشود. در یک چنین حالتی، هر نخ دیگری که به آرایه numbers دسترسی یابد، همواره مقدارهای یکسانی دریافت میکند و رفتار آن پایدار و قابل پیشبینی خواهد بود.
3. اگر بعدتر بخواهیم به مقادیر اصلی ذخیره شده در آرایه numbers دسترسی یابیم چه اتفاقی رخ میدهد؟
در واقع در این بخش نیز به ادامه بحث «عدم عارضه جانبی» مربوط است. بدیهی است که تغییرات رخ داده در حالت ردگیری نمیشوند؛ مگر این که یک الگوی طراحی صرفاً به این منظور پیادهسازی کرده باشید، یعنی نمیتوان به مقادیر ذخیره شده قبلی بازگشت داد. از این رو در رویکرد دستوری حالت اصلی آرایه numbers زمانی که پردازش مورد نظر روی آن اجرا شود از دست میرود. با این حال، راهحل تابعی ما آرایه numbers اصلی را حفظ میکند و سپس یک آرایه جدید با مقادیر مربوط در result به صورت خروجی ارائه میکند. این امر موجب میشود که آرایه اصلی به طرز کارآمدی برای پردازشهای بعدی در اختیار ما قرار داشته باشد.
4. ما کد خود را تا چه حد مطمئن میتوانیم تست کنیم؟
در این مورد نیز از آنجا که رویکرد تابعی همه عوارض جانبی را رفع میکند، پردازشی که تست میشود به طور کامل درون متد ادامه مییابد و بدین ترتیب ورودی هرگز تغییری در حالت خود مشاهده نمیکند و از این رو میتوانید یک تست را به هر تعداد که دوست دارید، درون یک حلقه اجرا کنید و هر بار نیز نتیجه یکسانی را دریافت کنید. تست این مسئله بسیار ساده است. برای مقایسه، اگر راهحل دستوری را در یک حلقه تست کنیم، مقدار ورودی تغییر مییابد و از این رو خروجی متفاوتی در هر بار تکرار خواهیم داشت.
جمعبندی مزیتها
همان طور که در مثال ساده فوق مشاهده کردیم، برنامهنویسی تابعی در مواردی که با دادههای مدل شده سر و کار داریم بسیار مفید است زیرا:
- اعلانی است.
- مشکلات نخ بندی مانند شرایط رقابت را رفع میکند.
- حالت موجود برای استفادههای آتی بدون تغییر میماند.
- تست آن آسان است.
اگر این مطلب برای شما مفید بوده است و علاقهمند به یادگیری بیشتر در این زمینه هستید، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش زبان برنامهنویسی سوئیفت (Swift) برای برنامهنویسی iOS
- مجموعه آموزشهای مهندسی نرم افزار
- برنامه نویسی تابعی (Functional Programming) و مفاهیم مقدماتی آن — به زبان ساده
- آموزش آرایه در برنامهنویسی سوئیفت (Swift)
- آرایه ها در زبان برنامه نویسی سوئیفت (Swift) — به زبان ساده
==