هادی اعظمی
هادی اعظمی
خواندن ۸ دقیقه·۵ سال پیش

چرا از JavaScript به Rust مهاجرت کردم؟

من درحال نوشتن این مطلب، توضیح مهاجرت واقعا طولانی شده بود.
من درحال نوشتن این مطلب، توضیح مهاجرت واقعا طولانی شده بود.

قبل از اینکه شروع کنم لازمه بگم من «کد نویسی» رو دوست دارم و اگه زبان یا تکنولوژی رو انتخاب کنم جدای از اینکه فرصت دارم یا ندارم که توی مقیاس بزرگ امتحانش کنم، همیشه دوست دارم به عمق ماجرا پی ببرم. همیشه نظرات و تجربیات بقیه رو می‌خونم، مستندات خود زبان و پارادایم‌های قابل استفاه در اون زبان رو دنبال می‌کنم و دید این مقاله طبیعتا آکادمیک هست.

اینا رو گفتم که مشخص بشه حرفهام از دیدگاه کسی نیست که توی شرکت X کارش وابسته به جاوا اسکریپت باشه، من صرفا به برنامه نویسی (این روزها سیستمی) علاقه دارم و این مطلب «شبیه کسی هست که از یه توزیع به توزیع دیگه مهاجرت کرده» درواقع فاکتور انتخاب من هیچوقت بازار کار و Scale نبوده و شکل دلایلم مقایسه‌ای از هردو زبانه، اگه توسعه دهنده جاوا اسکریپت هستین شاید یه چیزایی یاد گرفتین که قبلا نمی‌دونستید.

جاوا اسکریپت چطوری کار می‌کنه؟

کامپایلری که توی دانشگاه نشون میدن
کامپایلری که توی دانشگاه نشون میدن

نمی‌دونم که شما «نظریه زبانها» رو پاس کردین یا نه اما خودمونی ترش همون دوتا فلش Front-End و Back-End هست که کشیدم. به من (و شما که درس رو پاس کردی) همیشه گفتن که آقا کامپایلر باید نوع داده رو چک کنه. و گفتن اگه موقع اجرا دستور به دستور تفسیر کرد بهش میگن «مفسر».

اما این «تنها شکل کامپایلر» نیست! درواقع «به یقین رسیدن» درمورد نوع داده بر اساس «حدس زدن» خیلی می‌تونه پیچیده بشه! برای همین یه چیزی هم هست که ترکیب کامپایلر و مفسره به اسم JIT.

برای اینکه مطلب طولانی نشه کارش اینطوریه:

یک) دستورات حین اجرا مانیتور (‫‪profiler‬‬) میشن.
دو) قطعه کدی که به صورت مکرر تکرار بشه به عنوان نقطه داغ (HOT Code Point) برچسب گذاری میشه.
سه) کامپایلر سعی می‌کنه قطعه کد رو بهینه کنه و معادل Byte Code اون رو جایگزین و اجرا کنه.
چهار) اگه مرحله سه بیشتر از یک بازه‌ای تکرار بشه به جای کامپایل کردن، اون کدداغ؛ دستور به دستور تفسیر می‌شه.

فکرکنید یه حلقه خیلی بد نوشتین (اکثرا وقتی که از تابع Map سوءاستفاده می‌کنید اتفاق میوفته و خیلی بی‌صدا گند می‌زنید به کد) خب این هی تکرار می‌شه راهی نیست که بخوای بهینه کنی و اگه کامپایلر روشی تعریف نکنه برای «بی خیال شدن» دیگه همونجا گیر می‌کنه. (مثلا 10 بار قطعه کد رو اجرا می‌کنه اگه تونست خروجی رو حدس بزنه خب بهینه می‌شه؛ وگرنه چاره‌ای نیست جز تفسیر دستور به دستور. که بسیار پر هزینه هست) - توجه کنید، گفتم دستور به دستور! نه خط به خط؛ چون شما ممکنه دوتا دستور توی یک خط داشته باشی.

شماهایی که یه خورده کارکشته‌تر هستین قطعا به Type Script اشاره می‌کنید. «خب این که روی نوع داده حساسه خیلی بهتره چرا از اون استفاده نمی‌کنی؟»

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

این قطعه کد رو ببینید:

&quotuse strict&quot var Foo = { get x() { return.this.__x }, set x(v) {this.__x = v} } function Bar (v) { this.x = v } Bar.prototype = Object.create(Foo) Bar.prototype.identify = function () { console.log(this.x) } var p = new Bar(42) p.identify()

این کد عمدا به یک شکل صریح آورده شده، هدفم اینه که بگم حتی تایپ اسکریپت هم اون زیر یه همچین کدی تولید می‌کنه، به خاطر اینکه هرچی جزئیات بیشتر باشه، JIT بهتر می‌فهمه چیکار باید بکنه.

از طرفی تایپ اسکریپت فقط Bundler هست. جلوی خطاهایی که به علت نبود تایپ به وجود میان رو میگیره، اما همچین چیزی هنوز هزینه اجرا داره:

[1, 2, 3, 4, 5].map(x => x * 2).filter(x => x % 2).reduce((a, x) => a + x, 0);

تازه همه‌جا ممکنه نتونی ازش استفاده کنی برای همینه که یه شرکت معقول که دنبال توسعه دهنده NodeJS می‌گرده انتظار داره که ES5 بلد باشی، معنیش اینه که حالشو داشته باشی به صورت صریح کد بنویسی (مثال بالا).

این قطعه کد‌هم ببینید:

var Foo = { *[Symbol.iterator] () { for (let [idx, val]) of this.nums.entries()) { yeild label => `${label}: ${val}(${idx+1} / ${this.nums.length})` } }, bar ([x=1, y=2, ...nums] = []) { this.nums = [x,y,...nums] } } Foo.bar([10,20,30,40,50]) foo (let f of Foo) { console.log(f(&quotFoo&quot)) }

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

بحث ‌اینه که این انتزاعی شدن «هزینه داره» و زبان «باید» این‌ها رو لحاظ کنه چه شما این شکلی کد بنویسی چه ننویسی. در مجموع چیز بدی نیست، ولی چون هزینه داره باید با احتیاط عمل کنی. اصلا، اینهمه موتور V8 بهینه می‌شه یا میرن تایپ اسکریپت درست می‌کنن برای اینه که انتزاع توی زبان باقی بمونه و با یه شکل دیگه هزینه رو کم کنن. (در شرایط تایپ اسکریپت خیلی چیزا zero-cost abstraction شدن).

یادگیری این چیزا، انگیزه زیادی می‌خواد شما نمی‌تونی یک هفته بشینی جلوی Pluralsight و همشو حفظ کنی. «فضای تولید اشتباه» توی این زبان وسیعه برای همین یادگیری عمیق این زبان الزامیه. توی پیاده سازی business logic بیشتر از زبان‌های دیگه مجبوری به «چطور بهینه بنویسم» فکر کنی.

مدیریت حافظه

پیدا کردن نشتی حافظه توی NodeJS همیشه مورد بحثه، علت عمده‌اش اینه که این زبان Garbage Collector داره درواقع شما هرچی کمتر به کامپایلر اطلاعات بدی اون کار بیشتری باید انجام بده...

زنجیره پروتوتایپ
زنجیره پروتوتایپ

چندتا نکته:

یک) همه چیز توی جاوا اسکریپت Object هست (مربع‌ها توی تصویر بالا)
دو) یک Object از پروتوتایپ یک Object دیگه ساخته می‌شه (اینجا Foo پروتوتایپ Global هست)
سه) همه Object‌ها یک پروتوتایپ از Global Object هستن. (بازگشت‌همه به سوی اوست!)

حالا کار Garbage Collector این هست که در زمان اجرای کدشما دنبال Object هایی بگرده که دیگه از Global نمیتونی بهشون برسی. (یعنی اون فلش بزرگ، از Global شروع میکنه میاد پایین) وقتی تشخیص بده دسترسی قطع شده اون رو به عنوان زباله شناسایی می‌کنه و حافظه‌ای که بهش اختصاص داده شده‌ آزاد می‌شه.

هربار که Garbage Collector اجرا بشه، کدشما متوقف می‌شه. برای‌همین اگه زود انجام بشه کدشما کمتر اجرا می‌شه و اگه دیر‌ انجام بشه حافظه زیادی مصرف می‌شه. اینکه بفهمیم «کی آشغالا رو جمع کنیم» همیشه چالش برانگیزه و هیچ زباله جمع‌کنی بی‌عیب نیست.

برای همینم انتزاع هزینه داره، اینها باید به زنجیر اضافه بشن و چشممون بهشون باشه که ببینیم کی زباله می‌شن.

شماتیک حافظه V8

Resident Set
Resident Set

وقتی به شماتیک بالا نگاه می‌کنید واضحه که فضای کمتری به اجرای کدهای شما اختصاص داده شده (1). از طرفی چون «همه چیز یک شی هست» میزان قابل توجه‌ای از داده‌ها روی لایه 3 ذخیره می‌شه. تمام رشته‌هایی که طول ثابت ندارن، و همچنین closure (که، خیلی توی انتزاعی شدن کد و افزایش خوانایی اون تاثیر داره) هم روی Heap ذخیره می‌شه.

فرق بین 2 و 3 چیه؟ خب ببینید، توی Stack داده‌ها مثل بشقاب روی هم چیده شدن، شما وقتی بخوای بشقابها رو بذاری توی کابینت باید بدونی تعدادشون چندتاست که جای درست قرارش بدی. بعد بشقابهایی که از یه جنس هستن رو دسته بندی می‌کنی دیگه درسته؟ مثلا 6 تا میوه خوری 6 تا سوپ خوری، میدونی کجاست و چندتاس پس سریع بهشون می‌رسی.

اما Heap [معمولا] برای داده هایی هست که حجمشون مشخص نیست (مثلا بخش نظرات این مطلب از نوع رشته هست و نمیدونیم هرکس قراره چقدر بنویسه) در نتیجه کنارهم نیستن و پخش شدن توی حافظه، دنبالشون گشتن هزینه بره. مثل اینکه با دوستات بری رستوران بعد پیشخدمت اول سفارش یکی از دوستات رو بگیره بعد بره یه میز کاملا جدا، بعد دوباره برگرده سفارش اون یکی رفیقت رو بگیره بعد بازم بره ....

قسمت 4 اشاره می‌کنه به اندازه کل این Resident Set که تعریف شده، 5 و 6 هم اشاره دارن به میزان حافظه ای که در اختیار v8 هست و میزانی ازش که استفاده شده (مثلا یکی از فاکتور‌ها اینه که اگر میزان 6 به 5 نزدیک شد یه دور آشغالا رو جمع کن).

قسمت 7 مربوط میشه به وقتی که یه کتابخانه c++ داریم؛ شی هایی که به اشیاء داخل جاوا اسکریپت وصل شدن باید توسط V8 شناسایی و دنبال بشن (که اگر دقت کنید اینم توی Heap ذخیره می‌شه). پس نکته اینجاست که «بریم ماژول c++ بنویسیم» هم لزوما بدون دردسر نیست. مثلا شما ممکنه یک عالمه Object به دنیای JavaScript تزریق کنید که همشون باید دنبال بشن ....

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

حالا Rust چطوری کار می‌کنه؟

من یک ماهه که مهاجرت کردم، و در این سمت ماجرا «مبتدی‌تر» هستم برای همین؛ امیدوارم اگه از این بحث استقبال شد بیشتر درباره Rust بنویسم. منتها توی همین یک ماه چیزای به درد بخور زیادی از کتاب یاد گرفتم.

نکته دیگه هم اینه که، «فضای طرفداران Rust» یه مقداری سمی هست. هر محفلی بری چون زبان تازه داره طرفدار پیدا می‌کنه خیلی‌ها عاشق خوبی‌هاش میشن و چون دیگران کمتر درمورد ایراداتش صحبت می‌کنن پیدا کردنش در دید اول سخته. برای همین اول از بدی هاش می‌گم:

اینکه اینا اول تصمیم گرفتن که unified ABI رو پیاده سازی کنن که، بعدا بدون اعلان یک بیانیه درست حسابی بیخیال قضیه شدن. نبودن ABI بحث رو پیچیده می‌کنه خصوصا توی زمینه safety in portability (این ویدیو رو ببینید) که Rust ادعاهای بزرگی دربارشون داره...

بحث بعدی توی نحوه تعریف زبان هست، ما می تونیم هم well defined specification داشته باشیم (که یعنی یک تعریف در زبان تغییر نمی‌کنه/مگر به ندرت) و هم pseudo-specification که یعنی اون زیر ماجرا «می‌تونه» تغییر کنه وقتی نسخه زبان کمترین بروزرسانی رو دریافت کنه یا حتی کامپایلر به کامپایلر اسخراج مفاهیم متفاوت می‌شن.

که با توجه به نبود ABI این مشکل شدت پیدا می‌کنه چون منجر به تولید باینری‌های متفاوت می‌شه که مستقیما باهمدیگه سازگار نیستن! (اینم بگم فقط پشتیبانی از ABI کافی نیست، پایداری هم ملاکه مثلا توی c++ چون پیاده سازی‌های مختلفی داریم بعضی هاشون ABI پایدار ندارن! بعضی‌ها دارن)

اگه به Standard Library زبان نگاه کنیم جاهای زیادی به Unsafe Blockها پناه برده که بهشون اجازه میده وقتی نیازه از حصار type system ای که خودشون تعریف کردن بیرون برن به امید اینکه این قطعه کد‌های نا‌امن کپسوله شدن و مشکل جدی پیش نمیاد در هر صورت.

یه جورایی یعنی من‌ سازنده زبان می‌دونم چیکار می‌کنم پس اشکالی نداره... ولی خب چون تایپ سیستم این زبان جدیده، «تایید» ادعا‌هاشون هم نیازمند تحقیق و توسعه ابزارهای درخور تحلیل این زبان هست.

خلاصه بگم، اهدافشون قابل تحسین هست ولی درست نیست که ذوق زده بشیم و به چشم «گلوله نقره‌ای توسعه نرم افزار» بهش نگاه کنیم. و دلیل نمیشه بیخیال یادگیری عمیق بشیم.

انتزاع بدون هزینه

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents.lines().filter(|line| line.contains(query)).collect() }

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

موارد دیگه Trait و Generic هستن که توی انتزاع بسیار نقش دارن، با یه مثال بدون کد توضیح میدم که نه شما خسته بشید نه محتوا طولانی بشه (چون چشم شما ممکنه عادت نداشته باشه به Rust و نامفهومه).

دربان ساختمانی رو تصور کنید، گفتن یه عده زن و مرد بین 18 تا 30 سال قراره وارد ساختمان بشن برای شرکت در یک رویداد. خب این دربان باید کارت ملی همه رو نگاه کنه که بفهمه هر کس چند سالشه ... بعد اینکه «زن و مرد» اینجا معنی نداره! ما فقط سن رو نیاز داریم درسته؟

حالا به همون دربان میگن، هرکسی که «این کارت رو داشت» بهش اجازه بده بره توی رویداد شرکت کنه.

انتزاع یعنی این! شما نیاز نیست حتما با تمام جزئیات یه چیزی رو تعریف کنی؛ توی Rust هم کافیه بگی، پارامتر ورودی متد من «هر چیزی هست که، فلان Trait رو داره». درواقع مثل Interface.

اینها همه بدون هزینه هستن علتش رو اینجا بخونید. (شاید درباره اینم بعدا مفصل نوشتم)

خطایابی آسون

توی Rust خیلی چیزا به صورت پیشفرض Panic میدن - یعنی خطایی که برنامه دیگه قادر به ادامه نیست و خارج می‌شه. برای همین وقتی به شما گفتن کد فلانی رو بخون کافیه در اولین قدم دنبال الگوهایی باشید که خطا تولید می‌کنن.

مثلا اگه یه جایی خروجی اون از نوع Option باشه و بعد از Unwap استفاده کنن یعنی این کد اگر خطا بده، برنامه کاملا متوقف میشه! - پس دنبال کردن این سرنخ‌ها بهتون ایده می‌ده که چطوری خطای احتمالی رو بو بکشید.

یه نمونه دیگه استفاده از Unsafe Block هست. وقتی کد رو تحلیل می‌کنید مطمئن بشید که چنین بلاک‌هایی به خوبی تعریف شدن.

بعلاوه اینها، قابلیت نوشتن Unit Test و Integration Test توی اکوسیستم زبان تنیده شده و بندرت نیاز دارید به یادگیری ابزارهای شخص ثالث.

اینجا Null نداریم

بله، NULL یا همون اشتباه میلیون دلاری! توی این زبان وجود نداره. «هیچی» داریم، که به صورت یه پرانتز باز و بسته نوشته می‌شه و بهش میگن Unit و «یه چیزی» هم داریم، همین Option هست. یا مقدار بهت می‌ده یا نمیده که برای همین «ممکنه» خطا تولید بشه. (نترسید تو Rust کلی روش هست برای اینکه جلوش رو گرفت).

کامپایلر مثل معلم خصوصی

let x = 5; let mut y = x; println!(&quot{} - {}&quot, x,y);

من گفتم Y مقدارش Mutable هست (یعنی می‌خوام بعدا عوضش کنم) ولی نیازی نیست، چون فقط قراره چاپ بشه و از Scope خارج میشه.

بنازم به شعورت ای کامپایلر
بنازم به شعورت ای کامپایلر

خب شما بودی عاشقش نمی‌شدی؟ زیرشم خط کشیده !! (مجبور شدم اسکرین شات بگیرم چون ویرایشگر ویرگول پاراگراف رو بهم زد). بدون شک این بهترین قسمت زبان هست.

منابع یادگیری تمیز و بروز

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

برای من خیلی با ارزش بود که دیدم کتاب Rust انقدر پر محتوا و بروز هست.

کلام پایانی

حدودا از دو سال پیش خیلی به JIT و بحث Garbage Collection و این حرفا علاقه مند شدم و سعی کردم با JavaScript یه سری پروژه شخصی درست کنم. مثلا یکی از ایده‌ها شبیه raspap-webgui بود با NodeJS و داکر؛ هدفم این بود که هر قسمت از برنامه (مثل DHCP و Access Point و «قند شکن») توی یه کانتینر جدا باشن و ماژولار مثلا Ovpn کار نکرد سریع با کانتینر Tor جایگزین کنم...

که به نحوی zero-config باشه و آزادی عمل داشته باشم برای نصبش توی توزیع‌های گنو/لینوکس - و شاید ویندوز‌! بعد با NodeJS یه رابط کاربری و یه خط فرمان درست کنم که یه سری واحدهای اندازه گیری رو از سیستم بخونه، کانتینر‌های داکر رو بالا بیاره و غیره ... (پروژه تورباکس یه نسخه خیلی خلاصه شده از همین ایده هست).

از اون موقع تا الان خیلی چیزا درمورد اون ایده تغییر کرده خیلی راه‌ها هست که می‌تونم برم، می‌دونم که «بهینه ترین» فکر ممکن نبوده (مثلا جای داکر، از guix استفاده کنم چون داکر اینجا فقط نقش یه مدیر بسته داره و بعدش خودم فضانام شبکه به پروسه‌ها اختصاص بدم -- شبیه تورباکس).

و هدفم از نوشتن این مطلب اینه که، فرقی نداره کدوم زبان رو برای شروع انتخاب کردین؛ اینکه از لحظه شروع تا الان چه چیزی یاد گرفتین خیلی با ارزش تره تا اینکه اون انتخاب بد بوده یا خوب.

اینطوری فکر نکنید که Garbage Collection کاملا بده و باید ازش دوری کنید، نه! منتها در اکثر موارد «کم کاری» رخ میده. مثلا زبان ML یکی از معقول ترین پیاده سازی هاست.

من خیلی دوست داشتم بنویسم و بنویسم و بنویسم ولی حوصله مخاطب هم مهمه. اگه ازش استقبال بشه درمورد Rust بیشتر می‌نویسم. مثلا یه مطلب جدا کاملا درباره قرض گرفتن و مالکیت داده نیازه توی این مقایسه‌ها نمی‌گنجید.

منابع کلی:

The Rust Programming Language
Kyle Simpson - Keep Betting On JavaScript
A crash course in just-in-time (JIT) compilers
Anders Hejlsberg on Modern Compiler Construction
Memory Leaks Demystified
ERC Project &amp;amp;quot;RustBelt&amp;amp;quot;

جاوا اسکریپتjavascriptrustمهاجرت
توسعه دهنده نرم افزار
شاید از این پست‌ها خوشتان بیاید