کلا برای کسانی که به پارادایم oop آشنایی دارن، قضیهی immutable data structure عجیب به نظر میرسه.
یادگیری pure function برای کسایی که برنامه نویسی بلد نیستن راحتتر از کساییه که oop کار میکنن (و برعکس).
قضیه اینه که اصلا حلقه نداریم. (داریم ولی نه به اون شکلی که شما فکر میکنید)
اگه من یه حلقه داشته باشم که i++ کنه و کدش روی هستهی اول پردازنده اجرا بشه، بعد توی هستهی دیگه هم یه حلقه داشته باشم که i++ کنه، نتیجهی هیچکدوم از اون پردازشها قابل پیشبینی نیست (و احتمالا هر دفعه یه جوابی میده)
توی functional programming به جای حلقه از recursion استفاده میکنیم. (بهش tail call هم میگن) یعنی تابعی که خودش رو صدا میزنه و هر بار مقدار جدیدی که ساخته رو به خودش میفرسته. اینطوری هیچ دیتایی خارج از تابع تغییر نمیکنه و میتونیم این تابع رو چندبار توی چند thread مختلف اجرا کنیم بدون اینکه مزاحم همدیگه بشن.
خواستم یه نمونه کد کلوژر نشون بدم ولی دیدم چیزهای زیادی برای توضیح دادن هست. (مثلا if یا نحوهی تعریف توابع)
وقتی دیتا رو دیگه لازم نداشته باشیم، garbage collect میشه و از رم میره بیرون.
اگه بخوایم یه دیتایی از روی دیتای قبلی بسازیم، دیتای جدیدمون جایگزین قبلیه نمیشه. بهش یه اسم دیگه میدیم و در کنار دیتای قبلی قابل استفادست. اینطوری منابع دیگهای که با اون دیتا کار دارن، همچنان دیتایی رو میبینن که میخوان ببینن.
مثلا این کد رو ببینید:
user=> (def my-text "hello")
#'user/my-text
user=> (def my-new-text (str my-text " world!"))
#'user/my-new-text
user=> (println my-text)
hello
nil
user=> (println my-new-text)
hello world!
nil
البته این مثال خیلی سادهایه و برای توضیح بهترش باید از لیست یا وکتور استفاده میکردم به جای string ولی یه کم پیچیده میشد برای کسی که کلوژر بلد نیست.
یه قضیهی دیگه هم هست. mutable بودن دیتاها (یعنی اینکه یه دیتایی سر جای خودش override بشه) زمان زیادی از cpu و ram میبره. مثلا همین قضیهی i++ توی حلقهها.
شاید فکر کنید که کپی گرفتن از دیتا (مثل مثالی که بالاتر زدم) هم رم زیادی مصرف میکنه، ولی (حداقل) کلوژر اینکار رو با کمترین مصرف رم انجام میده. یعنی تمام دیتا رو کپی نمیگیره. فقط یه pointer به دیتای قدیمی (که مطمئنیم هیچوقت تغییر نمیکنه) میزنه و میگه «این دیتا، همون قبلیه که یک ملیون رکورد توش بود فقط تهش عدد ۴۵۶ اضافه شده».
یه چیز باحال دیگه که ربطی به سوال شما نداشت ولی به نظرم لازمه بگم.
از اونجایی که دیتا همیشه ثابته و خروجی توابع فقط به ورودیشون بستگی داره، میشه یه سری از function call ها رو cache کنیم! مثلا یه تابع داریم که hash میسازه. ساختن هش زمان و cpuی زیادی میگیره. اگه میدونیم که قراره یک هش یکسان رو چند بار تولید کنه، میتونیم ورودی و خروجی اون تابع رو memoize کنیم، که دفعهی بعد اگه با همون آرگومانها صدا زده شد، جواب رو مستقیم بهمون بده و دیگه پردازش نکنه. (البته روشهای بهتری هم برای اینکار هست)
توی development هم استفادهی زیادی میکنیم از ویژگی pure بودن توابع.
مثلا من یه سورسکد دارم که ۲۰تا تابع توش هست و میخوام ببینم این یک تابع خاص، خروجیش چی میشه. لازم نیست تمام کد رو اجرا کنم و مثلا خروجی اون تابع رو با یه print مشخص کنم (یا کد اضافه کنم برای لاگ گرفتن) فقط کافیه توی ادیتوری که دارم همون یک تابع رو evaluate کنم و جوابش رو ببینم. ادیتورهای زیادی این ویژگی رو دارن. از جمله emacs و vim و lighttable و atom و…
باحالیش اینجاست که حتی لازم نیست کل تابع رو evaluate کنم. میتونم هرکدوم از قسمتهاش رو تک تک اجرا کنم و خروجیشون رو ببینم. (مثلا وسط کار ۲تا عدد رو با هم جمع میکنه، من میتونم خروجی جمع اون دوتا عدد رو ببینم بدون اینکه کل تابع اجرا بشه)
از بحث functional که بگذریم، کلوژر (همهی لیسپها) یه ویژگی دارن که توی زبونهای دیگه وجود نداره. (بعضی زبونها سعی کردن شبیه اینکار رو انجام بدن ولی به خاطر معماریشون امکانش نیست)
اونهم قضیهی Macro هاست. با استفاده از مکرو، میتونیم «کدی بنویسیم که کد مینویسه» یا درستتر بگم «کدی بنویسیم که در زمان کامپایل اجرا میشه و میتونه کل ساختار زبان کلوژر رو تغییر بده»
همونطور که شما گفتید، ما مسائل رو خورد میکنیم و سعی مکنیم با زبان و ابزاری که داریم اون رو حل کنیم.
تفاوت این قضیه بین lispها و سایر زبونها به این شکله که در زبونهای دیگه، سعی میکنیم مشکلمون رو توی زبان برنامه نویسی بگنجونیم ولی توی لیسپها زبان برنامه نویسیمون رو گسترش میدیم تا مشکلمون توش جا بشه.
Lisp is a programmable programming language. — John Foderaro, CACM, September 1991
Will write code that writes code that writes code that writes code for money. — on comp.lang.lisp