۴ پارادایم برنامه نویسی در ۴۰ دقیقه + ویدیو

این ویدیو مربوط به یکی از تاک‌های روبی در سال ۲۰۱۲ هست.
علاقه‌ی چندانی به #ruby ندارم و این ویدیو رو اتفاقی دیدم و عنوانش باعث شد نگاهش کنم.

توی این ویدیو، یک مساله رو با چهار زبان برنامه نویسی که هرکدوم توی یک پارادایم متفاوت قرار دارن حل میکنه.
درمورد هر کدوم از زبانها یه توضیح مختصر میده و چندتا کد کوتاه ازشون معرفی میکنه و بعد کدی که برای حل کردن اون مساله زده رو نشون میده و خط به خط بررسی میکنه.
هدفش آشنا کردن ما با طرز فکر (mind set) این زبانها و این روشهای حل مساله هست.
همونطور که توی متن زیر ویدیو نوشته، زبانهای برنامه نویسی صرفا ابزار هستن و هر ابزاری برای یک کاری بهتر عمل میکنه. بعضی تسک‌ها در زبانهای FP بهتر حل میشن. بعضی نیاز به ویژگیهای OOP دارن و بعضیها نیاز به برنامه‌نویسی Logical.

زبانهایی که در این ویدیو نشون میده:

Ruby - Object Oriented Programming
Racket - Functional Programming
Prolog - Logic Programming
Assembly - Imperative Programming

این ویدیو برای سال ۲۰۱۲ هست و شاید اونموقع #clojure زیاد معروف نشده بود وگرنه احتمالا به جای Racket (که این‌هم یکی از گویشهای Lisp هست) از کلوژر استفاده میکرد.
البته Prolog هم یکی از زبانهاییه که برای ساخت هوش مصنوعی (سیستمهای خبره)‌ استفاده میشد. پارادایم Logic programming همیشه برام جالب بوده. گرچه شاید فقط برای کارهای خیلی محدودی بهترین گزینه باشه.

چیزی که خیلی دوست داشتم درمورد این ویدیو، کوتاه‌تر و ساده‌تر بودن کد FP نسبت به OOP بود. با نویز و boilerplate خیلی کمتر و کد کوتاه‌تر مساله رو حل کرد.

4 Likes

از اونجایی که تصمیم به یادگیری LISP گرفتم این ویدیو رو حتما همین الآن میبینم
فقط یه سوال داشتم اون هم این که منظور از boilerplate چیه؟ توی یه کتاب آموزش LISP بهش برخورد کردم و اونجا هم نفهمیدم چیه و توی google translate هم زدم چیز به درد بخوری نیاورد که بفهممش

Boilerplate یعنی چیزهای الکی زیادی که باید بنویسی تا برسی به اصل کار

4 Likes

خوب boilerplate که کامل توضیح داده شد، برای اینکه عمق فاجعه رو بهتر ببینید، این لینک رو نگاه کنید. چندروز پیش دنبال یه راه برای بازکردن عکس در جاوا میگشتم (که توی کلوژر استفاده کنم) و این لینک رو دیدم.
برای باز کردن یک عکس نیاز به یک خط کد هست. برای اون یک خط کد نیاز به ۲۰خط «کد اضافه». (طرفداران OOP ببخشن. علاقه‌ی من نسبت به OOP و ویندوز تقریبا به یک اندازست)
یه مقایسه بین hello world در جاوا و (مثلا) پایتون بندازید کاملا متوجه منظورم میشید.

در کل پارادایم Functional Programming حجم کد کمتری داره نسبت به Object Oriented Programming و خیلی ساده‌تر از پارادایم‌های دیگست (حداقل به نظر من) و خیلی ریاضی‌تره.
منظور از function در حقیقت «توابع» در ریاضیاته. یک تابع که x بهش میدیم و y رو میسازه. و هربار که اون x رو بدیم، همون y رو بهمون میده و کاری با خارج از خودش نداره.
یا مثلا Immutable بودن دیتاها. همونطور که هیچوقت توی ریاضیات 5 تبدیل به 6 نمیشه، پس کد ما هم نباید ++x داشته باشه.

برای یادگیری لیسپ، پیشنهاد میکنم یکی از گویش‌های لیسپ رو یاد بگیرید. مثلا #clojure که هم مدرن‌تره (پس برای حل مشکلات مدرن روی سخت‌افزار مدرن طراحی شده) و هم پلتفورم و لایبرری‌های مناسب‌تری داره. یعنی واقعا به یه دردی میخوره. اصولا میگن «لیسپ یاد گرفتن خوبه. حتی اگه واقعا ازش استفاده نکنی.» ولی درمورد کلوژر اینطور نیست. واقعا مورد استفاده داره و موارد استفادش هم خیلی زیاده.
اگه برای یادگیری لیسپ، نیاز به جملات انگیزشی دارید (:joy:) این لینک رو نگاه کنید.

لیسپ و پرولوگ، در گذشته برای ساخت هوش مصنوعی استفاده‌ی زیادی داشتن. الآن دارم یه کتاب میخونم Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp (لینک گیتهاب برای دانلود رایگان) که سال ۱۹۹۱ منتشر شده. توی این کتاب نوشته «در امریکا از LISP استفاده‌ی زیادی در زمینه‌ی AI میشه و در کشورهای شرق آسیا مثل ژاپن از Prolog خیلی استفاده میشه.» تا جایی که میدونم، اینجا منظور از هوش مصنوعی، Expert systems هست و مسلما ربطی به Artificial Neural Networks (که امروزه موضوعش خیلی داغه) نداره.
خوندن این کتاب رو فعلا بهتون پیشنهاد نمیکنم (گرچه کتاب ارزنده‌ایه) ولی یادگیری کلوژر رو به همه‌ی کسایی که میخوان برنامه‌نویس بهتری بشن پیشنهاد میکنم.


پ.ن: اگه بعد از دیدن ویدیو به Prolog علاقه پیدا کردید، باید بگم که کلوژر یه لایبرری داره به نام core.logic نمیدونم چقدر کامله ولی برای برنامه‌نویسی logical ساخته شده. یعنی مثل پرولوگ، یه سری گزاره‌ی صحیح (true) بهش میدیم و سعی میکنه بین اینا یه pattern پیدا کنه (یه دنیایی بسازه که همه‌ی اینا true باشن) و به سوالهامون پاسخ بده.

2 Likes

سلام
خیلی ممنون از توضیحات کاملتون
علاوه بر ویدیو ای که توی این تاپیک گذاشتن توی نت هم سرچ کردم در مورد تفاوت های OOP و FP که دقیق تر بفهمم تفاوتشون چیه
این مطلب رو خوندم:
https://www.codenewbie.org/blogs/object-oriented-programming-vs-functional-programming
جالب بود فقط این که من هنوز قانع نشدم که FP در کل روش بهتری هست یا اینکه برای بعضی کارها FP بهتره برای بعضی OOP
دلایل زیادی برای این قضیه دارم
مورد اول اینکه توی همین پستی که لینکشو بالا تر گذاشتم یه جا در دفاع از FP گفته:

Something you’ll notice right off is there are fewer lines of code, mainly because we don’t build the class for creating our objects.
و به نظر من اینکه تعداد خط کد کمتری نوشته شده دلیل بر خوبی یا بدی هیچ روش و هیچ زبانی نیست من ترجیح میدم کد بیشتری بزنم اما مساله برام مشخص تر باشه و قطعیت بیشتری داشته باشه

در کل من تو زندگیم یه طرز تفکر دارم و اون هم اینه که دوست دارم توی همه چیز من جمله کار یه چیزی ازم خواسته بشه بدون این که کسی بخواد بدونه چه جوری دارم این کارو انجام میدم و دقیقا خودم هم به همین صورت کار کنم یعنی یه چیزی رو درخواست کنم و دیگه برام مهم نباشه اون وسط چه اتفاقی میوفته
توی هر دو نوع یعنی هم FP و هم OOP ما یه جورایی داریم مسئله رو میشکنیم و کوچیک میکنیم انگار اینجوریه که به جای این که یه دستگاه بزرگ کارمون رو انجام بده چند تا دستگاه کوچیک دارن اون کار رو انجام میدن اما توی OOP اون دستگاه های کوچیک مستقل تر و مبهم تر (برای محیط خارجی) دارن کارشون رو انجام میدن و این ابهام یه نقطه قوته چون ابهام توی کار انجام شده نیست بلکه این ابهام در سطح بالا تر وجود داره و کمک کننده هست چون ذهن شما رو درگیر نمیکنه
احساس میکنم خیلی خیلی بد دارم منظورم رو میرسونم ولی در مجموع کل حرفم اینه که توی OOP میایم یه کلاس میسازیم و بهش میگیم با چی و چه جوری کار کنه و توی سطح بالا تر فقط بهش دستور میدیم دیگه کاری نداریم چه جوری کار میکنه و این ابهامی که توی سطح بالاتر وجود داره به نظر من خیلی خوبه!!!

خوب. کوتاه بودن کد مسلما بهتره ولی نه به این دلیلی که شما بهش توجه کردید. کوتاه بودن کد، یعنی دیباگ راحتتر، یعنی فهمیدن راحتترش توسط انسان.

تفاوت fp و oop هم خیلی بیشتر از این حرفهاست.
توی oop سعی میکنیم هر چیزی رو به عنوان یک شیء ببینیم با یه سری ویژگی و یه سری قابلیت. که این اشیا یه دیتاهایی رو با همدیگه به اشتراک میذارن. به اشتراک گذاری دیتا چیز خیلی بدیه، اینطوری نمیتونیم بفهمیم state الآن آبجکت ما چرا تغییر کرده و چرا چیزیه که الآن هست. پس مجبوریم یه سری کد اضافه بزنیم که getter و setter تعریف کنیم برای کلاسمون که فقط از طریق اون دوتا تابع، از محیط بیرون قابل دسترسی باشه (این خودش یعنی کد نویسی بیشتر) و باز با این حال state آبجکت ما قابل تغییره. باز هم دیتاها ثابت و قابل استناد نیستن و هر لحظه میتونن عوض بشن.

توی fp سیستممون رو به عنوان یک سری «روش حل مساله» تعریف میکنیم. دیتاها هیچ‌جا عوض نمیشن و ثابت هستن پس هر بخشی از سیستم میتونه هر زمان که میخواد یه دیتایی رو بخونه بدون اینکه نگران باشیم که ممکنه یه بخش دیگه اون دیتا رو دستکاری کرده باشه. هر تابعی رو به تنهایی میتونیم تست کنیم چون خروجی اون تابع فقط به ورودیش بستگی داره نه به عوامل بیرونی (پس دیباگ کردن خیلی ساده میشه) میتونیم یه تیکه از کدمون رو کپی کنیم بندازیم توی یه پروژه‌ی دیگه و مطمئن باشیم که درست کار میکنه (چون وابسته به stateهای اطرافش نیست) میتونیم با خیال راحت از تمام قدرت کامپیوتر استفاده کنیم و پردازش موازی داشته باشیم بدون اینکه نگران اشکالاتی که ممکنه توی پردازشهای موازی بیفته باشیم (چون مطمئنیم هیچ دیتایی تغییر نمیکنه و بازنویسی نمیشه)

کلا برنامه نویسی همینه.

اصلا قبول ندارم.
مخصوصا بخش «درگیر نشدن ذهن». یعنی دقیقا قضیه برعکسه! توی fp ما فقط کافیه به همین یک تابع فکر کنیم. درحالی که توی oop باید حواسمون به اطرافش هم باشه.

1 Like

یه سوال خیلی خیلی مهم که یادم رفت بپرسم
دقیقا یعنی چی که دیتا ها عوض نمیشن؟؟؟؟؟
اگه لازم بود عوض بشن چی؟
مثلا شمارنده حلقه؟؟؟
یا اصلا وقتی یه دیتا رو دیگه لازم نداریم و میخوایم باز نویسیش کنیم چی؟؟؟
این قضیه خیلی خیلی برای من عجیبه

کلا برای کسانی که به پارادایم 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

3 Likes