آموزش کلوژر ۲ - یه مقدار تئوری

clojure
learning_clojure
clojurescript
tutorial

#1

قبل از اینکه شروع کنیم بد نیست بدونید که، اگر نیاز به REPL انلاین دارید و می خواین کد های این قسمت رو تست کنید می تونین از این REPL استفاده کنین. فقط نکته ای که باید بهش توجه کنید این هست که این REPL در واقع clojurescript هست و ممکن هست بعضی از نتایج با REPL کلوژر متفاوت باشه.

قبل از اینکه بریم سراغ اصل داستان نیاز هست که یه سری اصطلاحات پر کاربرد تو دنیای کلوژر رو بدونید که در طی یادگیری راحت باشید.

List

ساید خیلی شنیده باشید که میگن lisp و clojure زبان های پردازش لیست هستند. اما منظور از لیست چی هست ؟ لیست یکی از ساختار های اصلی داده در lisp و clojure برای کالکشن ها (Collection) هست که از نظر روش استفاده و ظاهر شباهت داره به آرایه ها در زبان های دیگه. برای مثال:

(1 2 3 4)

در کد بالا یک لیست از اعداد ۱ تا ۴ رو می بینید. برای تعریف لیست باید از پرانتز استفاده کنید و عناصر لیست رو با فاصله از هم جدا کنید. لیست ها در کنار ارزیابی (evaluation) مهمترین مفاهیم lisp و clojure هستند.

نکته مهمی که باید به اون توجه داشت اینه که اگر لیست بالا رو در REPL وارد کنید با خطایی مواجه می شید. دلیل این خطا این هست که clojure به صورت پیشفرض ( در ادامه خواهد خواند ) برای ارزیابی لیست عنصر اول رو به عنوان فانکشن در نظر می گیره و سعی می کنه که اون رو پیدا کنه اما چون همچین فانکشنی ( 1 ) وجود نداره اون خطا رو ایجاد می کنه. اما اگر می خواین یه لیست درست کنید بدون این که کدتون ارزیابی شه باید کد رو quote کنین. به این شکل: (4 3 2 1)'.

Evaluation

در همه زبان های برنامه نویسی، عبارات برنامه نویسی در زمان اجرا ارزیابی می شن و با ارزش یا عبارت حاصل از ارزیابی جابه جا می شن. دقیقا مثل عملیات ریاضی. مثلا 4 * (2 + 3) با ارزیابی (2 + 3) شروع می شه که نتیجه ارزیابی می شه 5 و عبارت اولیه به شکل در میاد 4 * 5 که ارزیابی نهایی می شه 20. البته ارزیابی یه عدد می شه خود اون عدد. در زبان آمیانه می گیم نتیجه اجرای عبارت.

در کلوژر هم هر عبارتی یه روش ارزیابی می شه. مثلا اعداد، رشته ها (strings)، وکتورها (Vector) و تقریبا همه چیز به خود اون چیز ارزیابی می شن. اما در این میون ارزیابی لیست ها با همه چی فرق داره. کلوژر برای ارزیابی یه لیست به این شکل عمل می کنه که اولین عنصر (element) لیست رو به عنوان نام فانکشن و بقیه عناصر رو به عنوان پارامترهای اون فانکشن در نظر می گیره. بعد سعی می کنه اون فانکشن رو با پاس دادن پارامترهاش صدا بزنه. ارزیابی لیست میشه ارزش بازگشتی از اون فانکشن. به همین سادگی. برای مثال:

(myfunc 1 2 3 4)

کلوژر برای ارزیابی یا اجرای لیست بالا اول دنبال یه قانکشن به اسم myfunc می گرده و وقتی پبداش کرد با پاس دادن چهار تا پارمتر بعدی 1، 2، 3 و 4 اون رو صدا می زنه به کل لیست رو با نتیجه صدا زدن اون فانکشن که در واقع ارزش بازگشتی از تابع هست جایگذین میکنه. به مثال پیچیده تر:

(println (str "3+2 = " (+ 3 2)))

در مثال بالا کلوژر از داخلی ترین لیست شروع به ارزیابی کردن می کنه. و با ارزیابی هر لیست اون رو با مقدار به دست اومده از ارزیابیش جایگزین می کنه. روند به این شکل هست:

۱. داخلی ترین لیست (2 3 +) هست. برای ارزیابی این لیست فانکشنی به اسم + رو پیدا می کنه، 2 و 3 رو بهش پاس میده و نتیجه برگشتی که 5 باشه رو با کل لیست جابه جا می کنه که. کل لیست در این مرحله به این شکل در میاد:

(println (str "3+2 = " 5))

۲. حالا نوبت به لیست بعدی میرسه که به این شکل هست (str "3+2 = " 5). فانکشن str رو پیدا می کنه ( این فانکشن با چسباندن همه پارامترهاش به هم یه رشته می سازه و اون رو برمی گردونه) و "= 2+3" و 5 رو بهش پاس می ده و رشته بازگشتی رو با خود لیست جا به چا می کنه. لیست این مثال تا اینجا به این شکل هست:

(println "3+2 = 5")

۳. حالا فقط یه لیست مونده. هموجور که حدس زیدن با صدا زدن فانکش println و پاس دادن رشته ای که در عنصر دوم لیست هست، اون رشته رو در خروجی چاپ می کنه.

با دونستن در مورد لیست ها و روش ارزیابی اونها شما می تونین ۹۹٪ کد های کلوژر رو به راحتی بخونین.
خیلی ها می گن lisp و clojure سینتکس ندارن و این به این معناست که دستور زبانی برای این زبان ها وجود نداره ( البته بنده مخالف این گفته هستم). اما دلیل اصلیش این هست که شما در هنگام نوشتن کد یه نرم افزار با کلوژر به جای نوشتن دستور زبان در هال ساختن یه سری دیتا استراکچر هستید. در واقع کد همون دیتا هست. یه سری لیست تو در تو.

همین باعث شده که دستور زبان lisp، clojure و همه زبان های شبیه لیسپ در طی این همه سال پایدار بمونه. برعکس زبان هایی مثل جاواسکریپت که دستور زبانش هر سال تغییر مکنه lisp و clojure همیشه ثابت هست.

Immutable

شاید زیاد شنیده می گن دیتا استراکچر های فلان زبان immutable هستند. منظور از این عبارت این هست که دیتا استراکچر های اون زبان غیر قابل تغییر هست. یه دیتا رو نمی شه تغییر داد. اگر این موضوع رو خوب درک نمی کنین نگران نباشین. آروم آروم باهاش آشنا خواهید شد.

REPL

REPL مخفف عبارت Read, Eval, Print, Loop به معنی بخوان، اجرا کن، چاپ کن، و لوپ انجام بده ( برگرد به اول) هست. کلوژر با خواندن دیتا به شکل متن و تبدیل اون به دیتا استراکچر هایی مثل لیست و اجرای ( ارزیابی) اون ها نتیجه رو در خروجی چاپ می کنه و دوباره منتظر ورودی میشه.

به زبان ساده تر REPL یه محیط محاوره ای (‌شبیه به ترمینال و شل)‌ برای کد های کلوژر هست. آلتبه خیلی بیشتر از اینهاست اما فعلا این توضیح تا اینجا کافیست.

شما می تونین با فرمان زیر به یه REPL دسترسی داشته باشید. lein repl

Binding

در کلوژر مفهومی با عنوان متغیرر ( variable ) و جود ندارد. همون جور که در بالا توضیح داده شد دیتا استراکچر های کلوژر immutable هستند و تغییر ناپذیر، پس متغییر کاملا بی معنی هست.

با اختصاص دادن یک اسم رو به یه مقدار یه بایندیگ ساخته می شه. در واقع یک باندیگ یه اسم برای یه داده هست. برای مثال:

(def myname "sameer")

در کد بالا سمبل myname به مقدار “sameer” اختصاص داده مشه (به صورت سراسری).

Symbol

ردرک سمبل ها برای افرادی که از زبان هایی به کلوژر میان که در اونها همچین مفهومی وجود نداره، سخت هست.
سمبل ها فقط شناسنده هستند و بس. یه سمبل یه اسم هست که به چیزی اشاره می کنه. برای مثال اسمی که به یه بایندیگ یا یه فانکشن اشاره می کنه.


دوست دارم نظرتون رو در مورد این قسمت بدونم دوستان.


رهنمای شروع برنامه نویسی با کلوژر ( Clojure )
آموزش کلوژر ۴ - توابع بی‌نام
آموزش کلوژر ۶ - دیتا
#2

بسیار عالییییییییییییییییی و تشکر ، اگر ممکن باشه مفاهیم فانکشنالم در کنار اینا بیان کنید تشکر


#3

جالب بود ممنون،
در مورد run time evaluation میشه گفت مفهومی با عنوان Compile time function execution یا compile time function evaluation هم وجود داره البته :slightly_smiling_face:

هرچند بهتره بحث رو منحرف نکنم :unamused:


#4

اتفاقا نکته خوبی. می خواستم در این مورد و ماکرو ها هم بینویسم اما گفتم شاید زود باشه برای این شماره و باعث ایجاد سردرگمی بشه


#5

اینکه شروع کردی به آموزش کلوژر خیلی عالیه فقط یه نکته واسه همه آموزش ها! جوری آموزش بدیم که برای کسایی که تازه میخوان وارد دنیای کدنویسی بشن قابل درک و فهم باشه و در نهایت بتونن از آموزش و زحماتت کمال استفاده رو ببرن


#6

حالا من در فراغ متغییر چه کنم!!! :face_with_thermometer: :rofl:
واقعا خوب بود و با اون دوستمون که گفت مفاهیم فانکشنال ام در کنارش بیان کن موافقم اگه باشه عالی میشه


#7

عجله نکن. آروم آروم جا می افته.


#8

من یه چیزی رو درک نمیکنم. اگه متغیر نداریم پس این چیه؟

user=> (def name "pouya")
user=> (println name)
pouya
user=> (def name "sameer")
user=> (println name)
sameer‍‍‍‍‍

این مگه نمیشه «متغیری با نام name که دارای مقدار “pouya” است. بعدش مقدار رو مساوی “sameer” قرار میدیم.»
پس چرا میگیم متغیر نداره؟‌چرا میگیم این زبون Immutable هست؟
کلا Immutable رو درک نمیکنم.

و سوال دیگه. symbol چیه؟ من الآن فقط میدونم درکش سخته :sweat_smile:


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


#9

ببین def برای تعریف به binding هست. در واقع شما می یای درفعه اول سمبل name رو به عنوان یه اسم برای رشته اسم خدت انتخاب می کنی دوفعه دوم می یای همون سمبل name رو به عنوان اسم رشته “sameer” انتخاب می کنی. در واقع یه binding جدید تعریف می کنی. binding ها همیشه عمومی هستند حتی اگه توی یه فانکشن تعریفشون کنی. شاید در نگاه اول شبیه متغیر باشن. اما کاملا فرق دارن.

در مورد اینکه دیتا immutable هست. در فرایندی که انجام دادی فقط اسم یه داده رو گذاشتی رو داده دیگه. تغییرش ندادی. مثلا این کد رو ببین:

(let [x {:name "sameer"}
      y (assoc x :age 30)])

تو مثال بالا من یه بایندینگ لوکال به اسم x درست کردم که به یه hashmap اشاره داره. بعد یه کلید جدید توش ست کردم. اما این کار مقدار x رو عوض نمی کنه بلکه یه hashmap جدید از رو x می سازه و اسم y رو روش می ذاره. شما مقداری رو که x بهش اشاره می کنه رو نمی تونین تغییر بدین اما می تونین اسم x رو روی یه مقدار دیگه بذارین.

symbol رو می تونی به عنوان یه اسم در نظر بگیری. مثلا نقی یه اسم هست. که می تونه به یه ادم اشاره کنه. اما خود اون ادم نیست. فقط یه اسم هست.

به نکته خوبی اشاره کردی. اپدیت می کنم حتما. ممنونم.


آموزش کلوژر ۶ - دیتا
#10

همچنان از درک این مساله عاجزم :slight_smile:
میشه گفت «ما اینجا var نداریم و به جاش pointer داریم»؟ یعنی با اون کاری که من کردم، استرینگ “pouya” یه جایی توی رم سیستم گم و گور شد و یه استرینگ دیگه یه جای دیگه ساخته شد و name به اون پوینت کرد؟


گرفتم!


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


#11

بله دقیقا این جوریه.

برای شروع مطمئنم که درکش سخته مخصوصا وقتی از زبان های رایج اومده باشین. اما بعد از چند وقت متغیر ها کلا عجیب بنظر میان :smile:

اما یکی از مهمترین دلیل های ( جدا از دلایل فلسفیش ) اینه که شما وقتی دیتا immutable داشته باشی عملا نمی تونی تغییرش بدی پس در برنامه های که به صورت موازی اجرا می شن دیگه این خطر وجود نداره که تو ترد همزمان یه مقدار متغییری رو بخوان عوض کنن. چون کلا نمی تونن عوض کنن. و این می شه اصل و پایه thread safe بودن. یه دلیل دیگه هم اینه که وقتی از دید کامپایلر شما state ی نداشته باشی که بخوای تغییراتش رو ترک کنی خیلی از مشکلاتی که در سایر زبان ها هست رو نخواهی داشت و کد ساده تری باید بنویسی و طبیعتا می تونی کد ساده تر و سریع تری داشته باشی.


#12

سوال. رم پر نمیشه؟ (من از دنیای میکروکنترلرهای ۸بیتی با ۸کیلوبایت رم میام طبیعیه نگران این چیزا باشم :slight_smile:)


همین الآن از immutable خوشم اومد.
تا قبل از این فقط توی یک مورد استفاده از #define (توی زبان C و…) رو منطقی میدونستم اونم نگهداری عدد پی بود :joy:


#13

نه دیگه یکی از خوبی هایی که گفتم همینه. چون دیگه رفرنسی بهش نیست در جا GC می یاد ازاد می کنه .


#14

افتخار ک نیس … فقط تنبلیشون میومد با lockها موقع مالتی تردینگ کار کنن. اوردن زبون رو کلا immutable کردن :joy: و ی جور طرز فکره … امتیاز نیست . جاوا هم کانست و فاینال داره برای ایمیوتیبلیتی …


#15

افتخار که نیست. اما این کار رو برای multi threading و خلاص شدن از lock ها انجام دادن. بدون لاک هر ترد می تونه کار خودش رو انجام بده بدونه اینکه منتظریه لاک باشه که ریلیز شه. برای مثلا پردازش موازی به مراتب در کلوژر اسون تر و ساده تر از زبان هایی هست که دیتا immutable ندارن. البته immutable بودن دیتا شرط لازم هست اما کافی نیست.

در مورد final در جاوا هم باید بگم که کلا با immutability فرق داره و نباید اشتباه گرفته بشن. برای اطلاعات بیشتر این لینک مفید هست:

اما یه نکته دیگه هم که باید در مورد immutability تو clojure بهش اشاره کنم اینه که با اینکه دیتا تغییر ناپذیر هست اما این مساله باعث نمی شه که دیتا همین جوری تو جافظه کپی بشه. کد زیر رو در نظر بگیرید:

(def a {:name "sameer" :nick "lxsameer"})
(def b (assoc a :age 30))

در کد بالا assoc یه hashmap جدید از روی a می سازه و اون کلید age رو اضافه می کنه و بر می گردونه که ما اسم b رو روش می ذاریم. شاید در نگاه اول بگید که خوب ما الان از یه دیتا ثابت ۲ تا کپی تو حافظه داریم ( از دید برنامه نویس اینجوری هست و باید همینجوری هم فرض کنید) اما در واقع clojure یه کپی از این دیتا رو توی حافظه داره و اون کلید اضافی رو توست یه رفرنس یه دیتا اصلی نگه می داره. در واقع پشت صحنه a و b دیتا به اشتراک می ذارن و این باعث می شه از مموری بهتر استفاده بشه. این مورد تنها بخاطر immutable بودن دیتا عملی هست چون شما مطمئن هستی که دیتا رو نمی شه تغییر داد. اما در زبان هایی که immutable نیست دیتا کامپایلر یا مفسر مجبور به ساخت کپی هست. برای اینکه تغییر a رو b تاثیر نداشته باشه. بعضی زبان ها هم که خوب این تغییر رو قبول دارن و با تغییر a، b هم تغییر خواهد کرد که همین موارد باعث از بین رفتن thread safety و رخ دادن بعضی خطاهای نا خواسته می شه.

برای اطلاعات بیشتر خوندن این مقاله خالی از لطف نیست:

https://hypirion.com/musings/understanding-persistent-vector-pt-1


#16

Multi threading تنها مزیت و علت immutablity نیست بلکه این کار از کلی خطا در برنامه نویسی جلو گیری میکنه
در جبر همچین عبارتی بی معناست و دچار تناقض میشه

x = x + 1

این طرز فکر چیز عجیب غریب نیست و همگام با تمام قوانین جبر هستش


#17

بله اینو در جریان بودم … اما فاینال تا وقتی از طرف یوزر تغییر نکنه … توسط سایر ابجکت ها و متغییرا و کامپایلر/مفسر تغییر داده نمیشه و ثبات رفرنس برای ترد سیفتی کفایت میکنه . .

As already pointed out it's absolutely thread-safe, and final is important here due to its memory visibility effects.

Presence of final guarantees that other threads would see values in the map after constructor finished without any external synchronization. 

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


#18

خوب فرق نمی کنه کاربر هست که سیستم رو طراحی می کنه. درست هست که رفرنس تغییر نمی کنه اما این دلیل نمی شه که مقداری که اون فاینال داره عوض نشه تو اون لینکی که دادم اگر توجه کنین مثال هم داره. فاینال فقط رفرنسی هست که تغییر نمی کنه ولی مقدارش رو می شه عوض کرد. و خود رفرنس thread safe هست اما مقدارش thread safe نیست و هر ترد می تونه جدا تغییرش بده. ببین مهم اینه کم شما مقدارت تغییر نکنه نه رفرنس. ثبات ترد به هیچ وجه برای thread safety کفایت نمی کنه. چون شما اون مقداری رو که بهش اشاره می کنه رو می تونی تغییر بدی.

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


#19

ترد ها با خود دیتا کاری ندارن که بخان تغییرش بدن … تا جایی ک میدونم ریس کاندیشن و تغییر متغییر هم ناشی از عدم ثبات رفرنس هست … مگر اینکه کامپایلر به حدی داغون باشه!! ک این حرکت رو بزنه…(معمولا تو زبان هایی ک سعی میکنن بدون دخالت یوزر… مالتی تردینگ انجام بدن این اتفاق میفته . مثل چیپل)

این قبول … ولی لزومی نمیبینم مثلا استک از نو پیاده کنم … وقتی هستن … (با اینکه پیاده کردم و دربارش میدونم) …

من ک همون اول گفتم : )) برای خلاصی از لاک زدن و ریلیز خیلی خوبن اینا …


#20

ترد کد شما رو روش ران می شه. وقتی کد شما دیتا ای داشته باشه که بشه تغییرش داد از اونجایی که ترد ها یه سگمنت از مموری را شیر دارن و ازش استفاده می کنن . تغییر دیتا می تونه رو بقیه ترد ها هم تاثیر بذاره. ترد به خودیه خود فقط یه استراکچر تو کرنل هست و کد شما رو ران می کنه.

نه به این شکل نیست حتی اگه ثبات رفرنس داشته باشید در مقابل race ایمن نیستید. تغییر پذیر بودن دیتا همیشه باعث این مشکل هست که کلا وجود لاک ها برای همین مساله هست.

متوجه منطورت از استک نشدم.

شاید بهتر باشه با مثال صحبت کنم این کد رو در نظر بگیرین:

package race;

class Havij {
    public int x = 0;
}

class StupidThread extends Thread  {
    final Havij h;
    int x, i;

    StupidThread(final Havij h, int x, int i) {
        this.h = h;
        this.i = i;
        this.x = x;
    }

    public void run() {
        String tname = Thread.currentThread().getName();
        this.h.x = this.x;

        try {
            Thread.sleep(this.i);
        }
        catch(InterruptedException e) {

        }
        System.out.println(String.format("%s -> Expect h.x to be 4 but it is: %d", tname, this.h.x));
        // expect h.x to be 4;
        this.h.x = this.h.x + 1;

        try {
            Thread.sleep(this.i);
        }
        catch(InterruptedException e) {

        }
        System.out.println(String.format("%s -> Expect h.x to be 5 but it is: %d", tname, this.h.x));

    }

}

class HavijKade {
    final public Havij h = new Havij();

    public void start() {
        StupidThread t1 = new StupidThread(this.h, 4, 1000);
        StupidThread t2 = new StupidThread(this.h, 4, 1500);
        t1.start();
        t2.start();
    }
}

public class Race {
    public static void main(String[] args) {
        HavijKade havijkade = new HavijKade();
        havijkade.start();
    }

}

الان ‍HavijKade.havij بصورت فاینال تعریف شده و به هیچ عموان رفرنسش عوض نخواهد شد. اما مقداری که به اون اشاره می شه رو میشه عوض کرد که باعث ایجاد مشکل می شه. خروجی به این شکل خواهد بود:

Thread-0 -> Expect h.x to be 4 but it is: 4
Thread-1 -> Expect h.x to be 4 but it is: 5
Thread-0 -> Expect h.x to be 5 but it is: 6
Thread-1 -> Expect h.x to be 5 but it is: 6

هموجور که مشخص هست با تغییر مقدار اون متغیر فاینال همه چی بهم ریخت و با اینکه ترد ها انتظار داشتن مقدار h.x که گرفتن 4 باشه اما اینجور نیست چون اون یکی ترد قبلا تغییرش داده. بهمین سادگی تو به برنامه کاربردی همه چی میره هوا. اینجور مشکلات دیباگ کردنشون هم که کار حضرت فیله. اما تو یه زبانی مثل Clojure که دیتا immutable هست هیچ وقت همچین اتفاقی نمی افته.