برنامه نویسی فانکشنال : کنترل وضعیت

برای درک بهتر نوشته‌های این بخش، باید ابتدا بخش‌های زیر را مطالعه کرده باشید:

تشریح مفهوم «وضعیت» (State)

در بخش قبلی عنوان کردیم که هدف اصلی برنامه نویسی، تغییر شکل داده‌ی ورودی به داده‌ی خروجی است. کدهای ما برای محقق ساختن این هدف، فرآیندی را از زمان اجرا تا زمان خاتمه‌ی برنامه طی خواهند. در حین انجام این روند، از موجودیت‌های مختلفی استفاده می‌شود: متغیر‌ها، ثابت‌ها، دستورات کنترلی و حلقه‌ها، ساختارهای داده‌ای ترکیبی مثل آرایه‌ها و جداول هش، منابع جانبی مانند ورودی/خروجی و…

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

حالا برای لحظه‌ای دنیای کامپیوتر را فراموش کنید و بیایید «وضعیت» را با یک مثال در دنیای واقعی توضیح دهیم:

ساعت ۱۰ صبح است و شما یک لباس سیاه رنگ به تن دارید! در اینجا آن لباس سیاه، وضعیت ظاهری شما را در آن زمان از روز بیان می‌کند. حالا قصد داریم وضعیت ظاهری شما را تغییر دهیم: آن لباس سیاه را از تن درآورید و یک لباس آبی بپوشید. فرض را هم بر این می گذاریم که درآوردن لباس سیاه و پوشیدن لباس آبی یک دقیقه زمان برده است. بعد از انجام این کار، شما یک وضعیت ظاهری جدید خواهید داشت: در ساعت ۱۰:۰۱ صبح، آن لباس آبی بیانگر وضعیت ظاهری شماست.

نتیجه گیری می‌کنیم که:

  • تغییر در وضعیت فعلی، باعث رخ دادن یک وضعیت جدید شده است.
  • زمان همیشه رو به جلو حرکت می کند. وضعیت فعلی شما، همیشه بعد از وضعیت پیشین شما رخ داده است!
  • شما نمی‌توانید وضعیت پیشین خود را تغییر دهید، چون آن وضعیت مربوط به زمانی در گذشته است و در عالم واقعیت شما نمی‌توانید گذشته را تغییر دهید.
  • وضعیت ظاهری شما در گذر زمان قابل ثبت و پیگیری است. از همین رو می‌توانید وضعیت خود در زمان های مختلف را با یکدیگر مقایسه کنید. فرضا لباس شما در حال حاضر آبی است، اما می دانید که وضعیت قبلی که مربوط به یک دقیقه پیش است چگونه بوده: لباس سیاه.

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

L = [a, b, c, d, e]

حالا قصد داریم تغییری در وضعیت این آرایه به وجود بیاوریم (محتوای آن را تغییر دهیم). فرضا می‌خواهیم خانه‌ی دوم که حاوی مقدار c است را با مقدار w پر کنیم. با توجه به نتایج بدست آمده از بررسی ما از عالم واقعیت، می دانیم که وضعیت قبلی آرایه را نمی توانیم تغییر دهیم و فقط می‌توانیم یک وضعیت جدید را ایجاد کنیم. پس باید یک آرایه جدید، با محتوای جدید ایجاد کنیم، نه اینکه محتوای آرایه را به صورت درجا تغییر دهیم. اما متاسفانه این کاری است که اکثر زبان‌های برنامه نویسی آن را انجام می دهند. دلیل اصلی آن هم این است که در گذشته، حافظه‌ها بسیار کوچک‌تر از آنی بودند که بتوانند تمام وضعیت‌های یک موجودیت در گذر زمان را در خود نگه داری کنند.

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

معایب تغییر وضعیت موجودیت‌ها به طور در جا، هنگامی چهره‌ی واقعی خود را نشان می‌دهد که بخواهیم موجودیت‌ها را در کارهای همروند و موازی شرکت دهیم! در چنین محیطی پروسه‌ها و روندهای مختلف ممکن است بخواهند با هم به یک موجودیت دسترسی پیدا کنند. اما دسترسی آن‌ها مانند گذشته به صورت ترتیبی نیست! در دنیای موازی، در همین لحظه که پروسه a مشغول کار روی یک داده است، ممکن است پروسه b هم مشغول کار روی همان داده باشد.

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

در یک محیط همروند و موازی، وضعیت موجودیت ها باید همانند دنیای واقعی غیر قابل تغییر باشد. و اگر نیاز به تغییر وضعیت بود، به جای اینکه تغییرات به طور در جا اعمال شود، باید یک وضعیت جدید حاصل شود و وضعیت پیشین دست نخورده باقی بماند. در محیط موازی مفهوم زمان نقش پر رنگی دارد و باید بتوان مقادیر را در گذر زمان بررسی کرد. اما در زبان‌های معمول، چون چنین قابلیت‌ای وجود ندارد، انجام کارهای همروند و موازی بسیار بسیار بسیار بسیار بسیار سخت و فوق العاده پر خطا است!

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

جواب: به کمک داده‌های تغییرناپذیر (Immutable Data)

داده‌های تغییرناپذیر (Immutable Data)

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

  • ۱. داده‌های تغییرناپذیر از نظر منطقی رفتاری مشابه عالم واقعی دارند. وضعیت موجودیت‌ها در زبان‌های فانکشنال تغییر ناپذیر است. در بخش بعدی توضیح می‌دهیم که همین یک مورد، تا چه حدی در ساده کردن کدنویسی تاثیر گذار است.

  • ۲. با توجه به توضیحاتی که در بالاتر آمد، داده‌های تغییرناپذیر برای کارهای همروند و موازی بسیار مناسب‌ اند. پروسه‌ها در داده‌های هم تداخل ایجاد نمی کنند، و داده‌ها نیاز به سینک شدن ندارند. قفل‌ها یا همان Lock های پردردسر را هم که کلا فراموش کنید…

  • ۳. اگر یادتان باشد در بخش قبلی گفتیم برای پردازش موازی، علاوه بر اینکه باید الگوریتم‌ها را به قسمت‌های کوچک‌تر تقسیم کنیم، نیاز داریم که داده‌ی مورد نیاز آن الگوریتم‌ها را هم از حالت متمرکز خارج کنیم تا قابلیت پخش شدن داشته باشند. خوبی ساختارهای داده‌ای تغییرناپذیر این است که به طور ذاتی غیر متمرکز هستند و حالت پخش شده دارند! تقریبا تمام ساختارهای داده ای که در زبان‌های فانکشنال می‌بینید، در پشت صحنه به کمک لیست‌های پیوندی و درخت‌ها پیاده‌سازی شده اند و همانطور که می دانید این دو، داده‌ها را به صورت پخش شده و غیر متمرکز نگه داری می‌کنند. ساختارهای داده‌ای در زبان های فانکشنال به طور ذاتی برای محیط‌های همروند و موازی بهینه هستند!

در بخش بعدی، برای بیان سادگی مدل فانکشنال، آن را با مدل شی‌گرا مقایسه خواهیم کرد.