توابع Immutable و ارتباط آن با val در کاتلین


Kotlin Immutability
Kotlin Immutability

مباحث این مقاله :

  • مفهوم تغییرناپذیری
  • اثبات تغییرناپذیر نبودن val
  • پیاده سازی val تغییرناپذیر
  • انواع توابع تغییرناپذیر
https://gist.github.com/husen-hn/a8f717844c3076b7b666e8f8ed084d94

یک شیء یا ویژگی mutable بعد ساخته شدن قابل تغییر است، و در مقابل immutable شیء یا ویژگی است که حالت یا ویژگی آن پس از ایجاد تغییر نمی یابد، اگر نیاز به تغییر باشد باید یک نسخه جدید تغییر یافته از آن ساخته بشود. و در حالت کلی ما حالت یا state شی را تغییر نمی دهیم.

در کاتلین بحث تعریف متغیر با var با ویژگی mutable و با val با ویژگی Immutable است، که val جای بحث بیشتری دارد.

کاتلین به این صورت بیان می کند که val همون Immutable است، فقط اینکه همیشه Immutable بماند را گارانتی نمیکند.

در ادامه مفصل تر توضیح میدم:

درست نیست که ما val را Immutable بدانیم. طبق داکیومنت کاتلین val به عنوان "فقط خواندنی" شرح داده شده و این با Immutable بودن تفاوت دارد، چطور؟!

https://gist.github.com/husen-hn/267d9decdff056d403fee5cff957c82d

یه کلاس میسازیم که تعداد اشکال رو برای ما محاسبه میکند.

https://gist.github.com/husen-hn/f87020104a6c7502aa7f085741d90bc2

از کلاس یه آبجکت میسازیم و سعی میکنیم که مقدار space.all که با val تعریف شده رو تغییر بدیم، در جواب اول که با مقدار های اولیه جواب ۴۷ و بعد از تغییر مقدار space.squares جواب به ۸۴ تغییر پیدا کرده بنابراین نمیشه به val برچسب Immutable زد.

https://twitter.com/samdvr/status/1232011833143259136?s=20
آیا read-only با immutability متفاوت است؟
میشود Immutable را با read-only برابر دانستن. به صورت دیگری، یعنی ما immutability را یک نوع deep read-only یا فقط خواندنی عمیق میدانیم. (میتوانیم متغیر فقط خواندنی (val) در کاتلین را همان immutability بدون گارانتی یا به نوعی فقط خواندنی سطحی بدانیم)

پیاده سازی val تغییرناپذیر

خب تا اینجا فهمیدیم که val فقط خواندنی است و قابل تغییر هم است؛ در ادامه سعی میکنیم این قابل تغییر بودن را هم سلب کنیم و یک متغیر deep read-only با val بسازیم.

https://gist.github.com/husen-hn/576dbe241191c6d313842c744b597aac

در کد مربوطه امکان نمونه سازی از کلاس MyValue وجود ندارد، ولی بجاش از MyValue.Immutable یا MyValue.Mutable نمونه سازی می کنیم. زیربنای پراپرتی value را با val تعریف کردیم و در MyValue.Mutable آن را override کردیم به var.

در کاتلین چنین امکانی وجود دارد که با var یک پراپرتی val را override کرد ولی امکان برعکس این وجود ندارد، چون val یک getter دارد و شما میتوانید هنگام مشتق کردن با override، یک setter به آن اضافه کنید ولی شما نمیتوانید یک setter را در پراپرتی var در پایه را حذف کنید و با val آن را override کنید.

با استفاده از این الگوی کاملا ساده، اجرای واقعی تغییرناپذیری را در compile-time به دست می‌آوریم:

https://gist.github.com/husen-hn/73af94e31a395331953f4be95000e7af

همچنین لازم به ذکر است که یک ترفند بسیار زیبا وجود دارد که می توانیم از آن برای ساختن یک خاصیت var تغییرناپذیر استفاده کنیم:

https://gist.github.com/husen-hn/e6382ba6ca3b795645ef8581ee56ce7f

هرچند value با var تعریف شده است، ما میتوانیم private setter بسازیم و value جوری رفتار کند که انگار با val تعریف شده:

https://gist.github.com/husen-hn/06640639d733813b20af3c6ab7da6c5c


علاوه بر این ها ما const val را هم داریم که برای ما immutability را گارانتی میکند، ولی انعطاف پذیری خیلی کمی دارد و فقط بعضی data type های اولیه رو پشتیبانی میکند، که نمیتواند در مسیر کد زنی سرویس دهی خوبی داشته باشد.

خب! تا اینجا سعی کردیم با روش OOP تغییرناپذیری را تجربه کنیم، اساسا تغییر ناپذیری بر پایه Functional Programming است که یکی از پارادایم های تحت پشتیبانی کاتلین محسوب میشود که در ادامه به آن می پردازیم. (کاتلین برخلاف زبان هایی مثل کلوژور، هسکل و... pure نیست، یعنی بصورت خالص تابع گرا نیست دلیل آنهم مشهود است کاتلین زبانی اساسا شی گرایی است و تابع گرایی قدرتمندی را هم باخود دارد و با این اوصاف نمیتواند خالص باشد. تابع گرایی دقیقا جایی است که immutability از آنجا نشأت میگیرد، به همین خاطر کاتلین immutability قدرتمند مثل زبان های pure را دارا نیست)

تغییرناپذیری در کاتلین

بحث immutability را با انواع آن شروع میکنیم :

  • تغییرناپذیری مرجع (Referential immutability)
  • مقادیر تغییرناپذیر (Immutable values)
https://media.giphy.com/media/SueY1pCpzCwne/giphy.gif

تغییرناپذیری مرجع (Referential immutability)

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

https://gist.github.com/husen-hn/9946fa42c96185eb9df17b0316de3ae4

خروجی:

MutableObj MutableObj(value='')
MutableObj MutableObj(value='Changed')
[a, b, c, d, e]
[a, b, c, d, e, f]

خب ما اینجا دو پراپرتی val داشتیم یکی list و یکی با mutableObj. ما mutableObj را با آبجکت MutableObj() مقدار دهی کردیم و چون این پراپرتی با val است mutableObj همیشه به آبجکت MutableObj() اشاره خواهد کرد. ولی اگر در کامنت (2) متوجه شدید ما ویژگی mutableObj را تغییر دادیم، از آنجایی که پراپرتی کلاس MutableObj تغییرپذیر (var) است.

همین کار رو با list هم انجام دادیم، ما میتونیم بعد از مقدار دهی اولیه لیست، آیتم های دیگه ای را اضافه کنیم، هردو list و mutableObj مثال خوبی از تغییرناپذیری مرجع یا immutable reference هستند. پراپرتی ها مقداردهی اولیه میشوند و دیگر نمیشود که چیز دیگری اختصاص داد، اما مقادیر اساسی آنها قابل تغییر است. دلیل این، نوع داده ای است که برای اختصاص به آن پراپرتی ها استفاده می کنیم. هم کلاس MutableObj و هم ساختمان داده MutableList قابل تغییر هستند، بنابراین ما نمی توانیم تغییرات مقدار را برای نمونه های (instance) آنها محدود کنیم.


مقادیر تغییر ناپذیر (Immutable values)

طرف دیگر مقادیر تغییر ناپذیر یعنی در مقادیر آن هیچ تغییری صورت نگیرد؛ و نگهداری بسیار پیچیده ای دارد. در کاتلین const val تغییر ناپذیری مقادیر را برای ما نشان میدهد که از لحاظ انعطاف پذیری خیلی مشکل دارد و فقط type های اصلی را پشتیبانی میکنه (قبلا درموردشون بحث کردیم) که همین محدودیت میتواند در سناریو های واقعی باعث دردسر بشود.


مجموعه های تغییرناپذیر (Immutable collections)


کاتلین درحد ممکن در هر جایی immutability رو ترجیح داده، کاتلین برای این امر گزینه هایی رو برای استفاده در زمان و جای خودش در اختیار توسعه دهنده قرار داده است. این قدرت این زبان را نشان میدهد، برخلاف بسیاری از زبان هایی که mutability را ترجیح داده اند (جاوا، C# و ...) و یا آنهایی که فقط immutability رو ترجیح دادند مثل هسکل، کلوژور،... . کاتلین هردو ویژگی را بصورت جداگانه دارد و این توسعه دهنده است که انتخاب میکند immutable باشد یا mutable. (چون کاتلین pure fp نیست قابلیت های بسیاری رو از دست داده ولی در عوض به بهترین شکل ممکن mutability و immutability رو یکجا جمع کرده است).

کاتلین دو اینترفیس برای آبجکت های کالکشن دارد. Collection و MutableCollection؛ همه کلاس های کالکشن (مثل List, Set, یا Map) یکی از آن دو را پیاده سازی میکند. همانطور که از نامشان پیداست، این دو اینترفیس به ترتیب برای سرویس دهی به کالکشن های تغییرناپذیر و قابل تغییر طراحی شده اند.

برای مثال:

https://gist.github.com/husen-hn/1923fcd531953458fb84fe10a2f79b1d

خروجی:

Immutable List [1, 2, 3, 4, 5, 6, 7]
Mutable List [1, 2, 3, 4, 5, 6, 7]
Mutable List after add [1, 2, 3, 4, 5, 6, 7, 8]
Mutable List after add [1, 2, 3, 4, 5, 6, 7]

اینجا با کمک متد listOf، لیست immutable ساختیم (کامنت 1). متد listOf یک لیست immutable میسازد. این متد یک generic type نیز دارد که اگر المنت های آرایه خالی نباشند نیازی به آن نیست. متد listOf یک نسخه mutable نیز دارد. mutableListOf() هیچ تفاوتی ندارد بجز در اینکه MutableList برمیگرداند.

ما میتونیم یک لیست immutable را به لیست mutable به کمک تابع افزونه (extension function) toMutableList() تبدیل کنیم (کامنت 2). و در کامنت 3 یک المنت جدید اضافه میکنیم. به هرحال خروجی را اگر چک کنیم لیست تغییر ناپذیر اصلی بدون هیچ تغییری باقی مانده است، با این حال المنت به جای آن به لیست جدید ایجاد شده که MutableList است، اضافه می شود.




در این مقاله باهم یاد گرفتیم که تغییر ناپذیری در کاتلین به چه صورتی است. این ویژگی زیبای ارث رسیده از دنیای functional programming رو باید عمیق تر یاد بگیریم و امکانات بسیار قدرتمندی که کاتلین بر اساس fp توسعه داده است حداکثر استفاده را بکنیم، برای این امر دو کتاب Functional Kotlin و Functional Programming in Kotlin در زمان نوشتن این مقاله در بازار موجود است.کتاب دومی خیلی بهتر از اولی است ولی کامل نیست هنوز، کتاب اولی بیشتر به معرفی میپردازه و من تجربه خوبی با آن نداشتم. همچنین کتاب Programming Kotlin فصل ۳ که درمورد برنامه نویسی فانکشنال است خیلی خوب از صفر و پایه ای شروع میکنه ولی حقیقت در اینه که برای یادگیری عمیق و درک خوب و درست از برنامه نویسی فانکشنال خوبه که با یک زبان فانکشنال خالص سروکله بزنیم. مثل: لیسپ، کلوژور، هسکل، الکسیر و... .