به دومین ماه موضوعیمان خوش آمدید: Mechanical March. در این ماه روی زبانهای سیستمی تمرکز میکنیم، همانهایی که به کد ماشین کامپایل میشوند.
این مطلب هم پست است و هم رونوشتی از ویدیوی Mechanical March. اول یک معرفی کوتاه از ماه ارائه میکنم و بعد کمی به زبانهای سیستمی نگاه میکنیم: سیر تحولشان در طول تاریخ، مزایا و معایب کامپایل شدن به کد ماشین، و نگاهی سریع به هر یک از زبانهای ویژه. دوباره Erik همراه من است و بیشترِ صحبتهای نیمهی دوم را او انجام میدهد. اما اجازه بدهید من با کمی اطلاعات عملی شروع کنم.
خب، اول از همه زبانهای ویژهی این ماه. این زبانها عبارتاند از C، C++، D، Go، Nim، Rust، V و Zig. برای گرفتن نشان Mechanical March باید پنج تمرین را در یکی از این زبانها کامل کنید. مسیر Go ما یکی از بهترین برنامههای درسی Exercism را دارد، پس واقعاً توصیه میکنم آن را امتحان کنید. ما در Exercism خیلی طرفدار Nim هم هستیم، چون شروع با آن نسبتاً ساده است و نوشتنش هم بسیار آسان، پس قطعاً توصیه میکنم آن را هم امتحان کنید.
پنج تمرین ویژه هم برای امتحان کردن داریم:
- Linked-list یا simple-linked-list (بسته به زبان): تخصیص و آزادسازی حافظه، اشارهگرها
- secret-handshake: عملیات بیتی
- pangram: حلقههای For، رشتهها و کاراکترها
- sieve: آرایه، حلقههای For
- binary-search: آرایه، حلقهها
نشان تازهای هم هست که چند روز پیش در ویدیوی بهروزرسانی معرفی کردم؛ این نشان برای کامل کردن پنج تمرین ویژه در زبانهای موضوعی است. پس برای گرفتن آن نشان باید همهی آن تمرینها را جایی در طول سال، در یکی از زبانهای سیستمی، کامل کنید.
کلی برنامهی جذاب هم در راه داریم: مصاحبه با چند نفر از تیم اصلی Go و به امید خدا با تیم Rust و چند زبان دیگر هم. در طول ماه پخش زندهی زیادی هم خواهیم داشت. و بهزودی هم هدایای Mechanical March میرسد!
خب، کمی بیشتر به بخش فنی بپردازیم.
این زبانها برای چه کاری استفاده میشوند؟
خب، ما در سراسر Exercism از آنها استفاده میکنیم. CLI ما با Go نوشته شده است، ابزار داخلی مدیریت مسیرمان به اسم configlet با Nim نوشته شده، و کتابخانهی اصلی که خطوط کد راهحلهای شما را میشمارد با Rust نوشته شده است. Erik، چرا آن زبانها را برای آن بخشهای ابزارسازی انتخاب کردیم؟
CLI:
- فکر میکنم Go را انتخاب کردیم چون Katrina بیشترین آشنایی را با آن داشت.
- Go برای این ابزارهای خط فرمان نسبتاً کوچک عالی است.
- کد Go نسبتاً سرراست است و همین مشارکت را آسانتر میکند.
- باینریهای Go بهراحتی قابل استقرار هستند، چون به محیط اجرا نیاز ندارند.
- Go کامپایل متقابل را خوب انجام میدهد.
Nim
- Nim بیشترِ همان مزیتهای Go را دارد.
- افراد زیادی که Go بدانند و بتوانند CLI را نگهداری کنند نداشتیم، پس به Nim روی آوردیم.
فکر میکنید این زبانها در چه جاهای دیگری استفاده میشوند؟
- هر جایی که کارایی مهم است (درایورها، بازیها، سیستمعاملها، سیستمهای ساخت/کامپایلرها)
- هر جایی که منابع محدود است (مثلاً نرمافزار جاسازیشده)
- هر چیزی که باید بهشدت قابل حمل باشد، یعنی روی پلتفرمهای مختلف اجرا شود
کد ماشین چیست؟
همانطور که قبلاً گفتم، زبانهای Mechanical March با کامپایل شدن به کد ماشین متمایز میشوند. میشود کمی توضیح دهید کد ماشین چیست و در مقابل، بایتکد چیست؟
- کد ماشین کدی است که میتواند مستقیماً روی ماشین اجرا شود.
- در مقابل، بایتکد به کد دیگری نیاز دارد که آن را تفسیر یا به کد ماشین کامپایل کند. بنابراین بایتکد پیش از اجرا به یک مرحلهی میانی نیاز دارد.
مزایا و معایب کد ماشین در مقایسه با بایتکد چیست؟
مزایا:
- راهاندازی سریعتر (بدون مرحلهی کامپایل JIT)
- رد پای حافظهی کمتر (محیط اجرایی بارگذاری نمیشود، بایتکدی در حافظه نیست، عالی برای سیستمهای جاسازیشده)
- ماشین هدف نیازی به نصب محیط اجرا ندارد (برای کوچک نگه داشتن کانتینرهای Docker مهم است)
معایب:
- قابل حمل نیست. بایتکد قابل حمل است، اما کد ماشین کامپایلشده مخصوص یک پلتفرم خاص است.
- نمیتوان بهینهسازیهای پیشرفته مانند بهینهسازی مبتنی بر پروفایل را انجام داد (یعنی تعیین بهترین روش کامپایل کد (بایتکد) پس از مدتی اجرای آن).
نکته: رویکردهای ترکیبی هم ممکن است؛ زبان به بایتکد کامپایل میشود و بعد ابزار دیگری آن بایتکد را به کد ماشین کامپایل میکند.
سیر تحول برنامهنویسی سیستمی
خب. بیایید کمی به سیر تحول برخی از زبانهای این ماه نگاه کنیم. اول، از C شروع میکنیم و ببینیم C++ چطور از آن تکامل یافت. کمی دربارهی این زبانها برایمان بگویید.
C زبانی بسیار سطحپایین است. حس میکنید فقط کمی بالاتر از کد ماشین هستید. همین آن را بسیار قدرتمند و بهشدت قابل بهینهسازی میکند، اما تا حدی هم مستعد باگ است (مثلاً خطاهای اشارهگر تهی و سرریز بافر). مدیریت حافظه کاملاً دستی است و بنابراین بر عهدهی برنامهنویس است، که میتواند به باگ و/یا نشت حافظه منجر شود. C++ مثل C است، اما با پشتیبانی از برنامهنویسی شیءگرا. هنوز نسبتاً سطحپایین است و از شما میخواهد مدیریت حافظه را دستی انجام دهید. هر دو زبان C و C++ اجازه میدهند اسمبلی درجا (ASM) بنویسید!
زبانهای سیستمی جدیدتر چطور؟ چگونه تکامل یافتهاند؟
همهی زبانهای مدرن برنامهنویسی سیستمی از مدیریت خودکار حافظه پشتیبانی میکنند، چه با شمارش ارجاع، چه با جمعآوریکنندهی زباله و چه با سازوکاری دیگر.
زبانهای سیستمی اولیه همگی اشارهگر تهی را پشتیبانی میکنند (Tony Hoare آن را «اشتباه میلیارد دلاری» خود نامید). اینها به خاطر منجر شدن به خطاهای زمان اجرا و آسیبپذیریها بدناماند. بسیاری از زبانهای مدرن null را کنار گذاشتهاند، یا دستکم استفاده از آن را پرزحمت کردهاند.
تغییر دیگر، حرکت از پیشفرض بودن مقادیر تغییرپذیر به پیشفرض بودن مقادیر تغییرناپذیر است. مثلاً Rust و Vlang هر دو مقادیر تغییرناپذیر را بهعنوان پیشفرض دارند و برای تغییرپذیری باید خودتان انتخاب کنید.
همهی زبانهای جدیدتر از همکاری متقابل با C (یا C++) پشتیبانی میکنند، چون کد بسیار زیادی با آن زبانها نوشته شده است.
نکتهی جالب دیگر این است که برخی از زبانهای جدیدتر مستقیماً به کد ماشین کامپایل نمیشوند، بلکه از ابزارهای دیگری برای این کار استفاده میکنند. مثلاً Rust و Zig از LLVM استفاده میکنند، در حالی که Nim اجازه میدهد از طیف گستردهای از کامپایلرها استفاده کنید. به این کار «ترنسپایل» میگویند.
ماکروها و فرابرنامهنویسی چه؟
دربارهی ماکروها/فرابرنامهنویسی شکاف جالبی وجود دارد. ماکروها در C/C++ قدرتمندند، اما شهرتی نسبتاً مشکوک دارند که کار کردن با آنها سخت است. Rust، Nim و D همگی فرابرنامهنویسی قدرتمند ارائه میدهند، اما به شکلی بسیار دلپذیرتر. در مقابل، VLang و Zig هر دو صریحاً نبودِ ماکرو را یکی از ویژگیهای زبان خود ذکر میکنند و Go رویکرد متفاوتی با go generate دارد.
زبانهای سیستمی شهرت سطحپایین بودن دارند. آیا این شهرت هنوز منصفانه است؟
زبانهای جدیدتر همگی در سطوح انتزاعی بالاتری از C/C++ کار میکنند. مثلاً Rust، D و Nim امکان نوشتن کد به شیوهای بسیار تابعی را هم میدهند. Nim و D حتی مفهوم توابع «خالص» را هم دارند.
معرفی زبانهای این ماه
خب، خوب است که به هر یک از زبانها بهنوبت نگاه کنیم. همهی زبانها شباهتهایی دارند: همگی بهصورت قوی و ایستا نوعدهی شدهاند. اما بیایید ببینیم چه تفاوتهایی دارند. با C شروع کنیم؟
C
- توسط Dennis Ritchie توسعه یافته است
- یکی از قدیمیترین و احتمالاً پرکاربردترین زبانهای جهان
- نرمافزارهای بیشماری با C نوشته شدهاند، مثل Unix و Linux
- بسیار تأثیرگذار (کافی است به وجود زبانهای شبیه C فکر کنید)
- مدیریت حافظهی دستی
- کارایی بسیار بالا (نزدیک به «سختافزار»)
- همهجا اجرا میشود
- عالی برای سیستمهای جاسازیشده
- زبانی نسبتاً کوچک
C++
- توسط Bjarne Stroustrup توسعه یافته است
- جانشین C، اما با شیءگرایی افزوده («C با کلاسها»)
- به همهگیر شدن برنامهنویسی شیءگرا کمک کرد
- ویژگیهای سطحبالاتر از C دارد
- از برنامهنویسی جنریک از طریق قالبها پشتیبانی میکند
- پشتیبانی از ماژول از طریق فضای نام
- بسیاری از (موتورهای) بازی با C++ نوشته شدهاند، و بخشهای بزرگی از Windows
- مدیریت حافظهی دستی
- هنوز در حال تحول است و مرتباً ویژگیهای تازه به آن افزوده میشود (مشخصات بزرگ)
D
- توسط Walter Bright توسعه یافت و بعدها Andrei Alexandrescu هم به آن پیوست
- در ابتدا بازمهندسیشدهی C++ به شمار میرفت (با درس گرفتن از «اشتباهات» آن) و از زبانهای بسیار دیگری الهام گرفته است
- چندپارادایمی، با پشتیبانی از برنامهنویسی دستوری/شیءگرا/تابعی
- همکاری متقابل آسان با C/C++
- روش یکسان فراخوانی توابع
- ارزیابی توابع در زمان کامپایل (مثلاً تولید ماشین حالت Regex در زمان کامپایل)
- از برنامهنویسی تابعی و توابع «خالص» پشتیبانی میکند
- ویژگیهای ایمنی متعدد
- ایمنی حافظه از طریق @safe
- قراردادها (پیششرط/پسشرط، ناورداها)
- توابع خالص
- تمرکز بر آزمونهای واحد، با آزمونهایی در کنار کد منبعی که آزمون میکنند (در وبسایت Exercism به یک استثنا نیاز داشت :))
- یونیکد
Rust
- توسط Graydon Hoare، کارمند Mozilla Research، توسعه یافت؛ سپس Mozilla رسماً آن را پذیرفت و اکنون بخشی از بنیاد Rust است
- چندپارادایمی، با پشتیبانی از شیءگرایی (اما نظرمحور، مثلاً بدون وراثت)، برنامهنویسی دستوری و تابعی (انواع Option/Result، تطبیق الگو)
- ابزارهای جدید بسیاری با Rust نوشته شدهاند (مثلاً SWC، و همچنین Gleam، و دومین زبان پشتیبانیشده در هستهی Linux؛ ما برای شمارندهی خطوط کد به آن وابستهایم)
- تمرکز بر قابلیت اطمینان و کارایی
- ساختهشده روی LLVM
- محبوبترین زبان در نظرسنجی StackOverflow در ۷ سال گذشته
- سریع، تا حدی به خاطر هسته و کتابخانهی استاندارد کمحجم
- ایمن، هم از نظر حافظه و هم از نظر رشتههای اجرایی، از طریق مالکیت و طول عمر؛ بهصورت پیشفرض تغییرناپذیر
- سیستم نوعی قدرتمند که بسیاری از باگها را در زمان کامپایل میگیرد (بهویژه باگهای مربوط به حافظه). کامپایلر خطاهای واقعاً مفیدی تولید میکند
- همهچیز همراه: کامپایلر، ابزار ساخت، قالببند، مدیر بسته، یکپارچهسازی با IDE
- مستندات دلپذیر (حتی سند بزرگی دربارهی اینکه کامپایلر چطور کار میکند وجود دارد)
- قابل حمل: به یک باینری واحد و ایستا کامپایل میشود و نیازی به نصب محیط اجرا ندارد
- همکاری متقابل آسان با کد C
- انتزاعهای بدون هزینه
- همروندی بیهراس
- ماکروها
Nim
- توسط Andreas Rumpf توسعه یافت (نام اولیهاش Nimrod بود)
- شکل نوشتنش از Python الهام گرفته است
- چندپارادایمی
- در Exercism در configlet استفاده میشود
- کارایی عالی: تکرارگرهای بدون سربار، ترجیح تخصیص پشتهای برای انواع مقدارمحور
- سیستم نوعی مدرن و رسا: استنتاج نوع، تاپلها، جنریکها، انواع جمعی، async/await
- جمعآوری زباله، اما با پشتیبانی از مدیریت حافظهی قطعی (چند گزینه برای مدیریت حافظه)
- اجرای کد در زمان کامپایل
- فراخوانی یکسان
- ماکروها: گسترش زبان بهراحتی
- سیستم اثرها: کدگذاری عوارض جانبی در سیستم نوعی
Go
- توسط Robert Griesemer، Rob Pike و Ken Thompson در Google توسعه یافت
- بیشتر دستوری/رویهای، با پشتیبانی از رویکردی شبیه شیءگرایی (اما بدون وراثت)
- در پروژههای بزرگی مثل Docker و Kubernetes استفاده میشود. برای بکاند و CLI هم عالی است (مثلاً esbuild)
- هدفش این است که آنقدر ساده باشد که در ذهن جا شود (نگارش کم)
- ایمنی حافظه از طریق جمعآوریکنندهی زباله
- سریع: کامپایل سریع، آزمون سریع و اجرای سریع. پشتیبانی داخلی برای نوشتن بنچمارک!
- نظرمحور: تلاش زیادی برای تأثیرگذاری بر سبک کد Go صورت گرفته: نگارش کم، قالببندی کد با go fmt، ابزارهایی برای بررسی کاربرد اصطلاحی کد، خطا دادن روی چیزهایی که در زبانهای دیگر هشدارند (مثلاً متغیرهای استفادهنشده)، مستنداتی که اصطلاحات Go برای استفاده را فهرست میکند
- سیستم نوعی سبک که Go را بسیار منعطف میکند (از استنتاج نوع پشتیبانی میکند)
- قابل حمل: به یک باینری واحد و ایستا کامپایل میشود و نیازی به نصب محیط اجرا ندارد. کامپایل متقابل آسان. همروندی از طریق گوروتینها (سبک) و ارتباط از طریق کانالها
- نوعدهی ساختاری از طریق رابطها (شبیه به duck typing، اما بهصورت ایستا بررسی میشود)
- مدیریت خطا: زبان تشویق میکند که خطاها را بررسی و مدیریت کنید
VLang
- توسط Alexander Medvednikov و Delyan Angelov توسعه یافت
- از Go الهام گرفته است:
- همان راهبرد «تنها یک راه برای انجام کارها»
- همان راهبرد «نگارش حداقلی»
- کوروتینها
- متفاوت از Go:
- بدون nil/null، اما از نوع نتیجه استفاده میکند
- بهصورت پیشفرض تغییرناپذیر
- انواع جمعی (تابعی)
- درج رشتهای
- محیط اجرا/باینریهای کوچکتر
- تطبیق الگو
- در تلاش برای اختیاری کردن GC (autofree)
- همکاری متقابل بدون هزینه با C
- تولید مستندات از کد
- کامپایلر سریع که حافظهی کمی مصرف میکند
Zig
- توسط Andrew Kelley توسعه یافت
- نگارش نسبتاً کم (فایل گرامر PEG با ۵۰۰ خط)
- اجرای کد و بازتاب در زمان کامپایل
- هدفش «واضح» بودن است؛ بدون جریان کنترل پنهان یا تخصیص پنهان یا ماکرو/فرابرنامهنویسی
- تخصیص حافظهی دستی
- از تخصیصدهندههای مختلف پشتیبانی میکند
- توابع کتابخانهی استاندارد که تخصیص میدهند، پارامتر تخصیصدهنده دارند
- چارچوب آزمون میتواند نشت حافظه را تشخیص دهد
- ایمنی:
- خطاها مقدارند و باید مدیریت شوند
- بدون null، از نوع اختیاری استفاده میکند
- آزمونها را میتوان در همان فایل منبع نوشت (مثل D)
- از LLVM بهعنوان بکاند استفاده میکند
- میتواند کد C/C++ را کامپایل کند
- همکاری متقابل آسان با C
- کامپایل متقابل آسان
نتیجهگیری
همینجا این را رها میکنیم، چون فکر میکنم مغز همه در این مرحله حسابی پر شده است.
امیدوارم این معرفی برای زبانهای ویژهی این ماه مفید و سرگرمکننده بوده باشد. امیدوارم از کاوش در این زبانها کلی لذت ببرید. من و Erik هر دو دوست داریم بشنویم کدام را انتخاب میکنید و چه تجربهای دارید، پس حتماً در بخش نظرات یا در انجمن پست بگذارید!
ممنون که تماشا کردید!