مرحبًا بكم في شهرنا الموضوعي الثاني: Mechanical March. نركّز هذا الشهر على لغات الأنظمة، تلك اللغات التي تُترجم إلى كود الآلة.
هذا جزء مقال وجزء نص مكتوب من فيديو Mechanical March. سأقدّم لكم مقدمة سريعة عن الشهر، ثم نلقي نظرة موجزة على لغات الأنظمة، وتطوّرها عبر التاريخ، وإيجابيات الترجمة إلى كود الآلة وسلبياتها، ثم نظرة سريعة على كل لغة من اللغات المميزة. سينضمّ إليّ مجددًا Erik، وسيتولى معظم الحديث في النصف الثاني. لكنني سأبدأ معكم ببعض المعلومات العملية.
أولًا، اللغات المميزة لهذا الشهر. إنها C وC++ وD وGo وNim وRust وV وZig. وللحصول على شارة Mechanical March عليك إكمال خمسة تمارين في إحدى تلك اللغات. مسار Go لدينا يضمّ أحد أفضل المناهج الدراسية على Exercism، لذا أوصيك بشدة بتجربته. نحن أيضًا من المعجبين الكبار بـ Nim هنا في Exercism، فهي لغة بسيطة نسبيًا للبدء بها وسهلة الكتابة جدًا، لذا أوصيك بالتأكيد بتجربتها أيضًا.
لدينا كذلك خمسة تمارين مميزة يمكنك تجربتها:
- linked-list أو simple-linked-list (حسب اللغة): تخصيص الذاكرة وتحريرها، والمؤشرات
- secret-handshake: عمليات البتات
- pangram: حلقات
for، والسلاسل النصية والمحارف - sieve: المصفوفات، وحلقات
for - binary-search: المصفوفات، والحلقات
هناك شارة جديدة أعلنت عنها في فيديو التحديث الذي نشرته قبل بضعة أيام، وهي مخصّصة لإكمال التمارين المميزة الخمسة في اللغات الموضوعية. فللحصول على تلك الشارة ستحتاج إلى إكمال كل تلك التمارين في إحدى لغات الأنظمة في وقت ما خلال السنة.
ولدينا أيضًا الكثير من الأمور الممتعة التي تتجهّز: مقابلات مع شخصين من فريق Go Core، ونأمل أيضًا مع Rust وبعض اللغات الأخرى. وسنقدّم الكثير من البث المباشر طوال الشهر. وستصل قريبًا أيضًا بعض الهدايا الترويجية الخاصة بـ Mechanical March!
فلنغصّ الآن في الجانب التقني قليلًا.
فيمَ تُستخدم هذه اللغات؟
نحن نستخدمها في أنحاء Exercism كافة. واجهة سطر الأوامر لدينا مكتوبة بـ Go، وأداتنا الداخلية لإدارة المسارات المسماة configlet مكتوبة بـ Nim، والمكتبة الأساسية التي تحسب أسطر الكود في حلولك مكتوبة بـ Rust. يا Erik، لماذا اخترنا تلك اللغات لتلك الأدوات؟
واجهة سطر الأوامر:
- أعتقد أننا استخدمنا Go لأنها اللغة التي كانت Katrina أكثر دراية بها.
- Go رائعة لهذه الأدوات الطرفية الصغيرة نسبيًا.
- كود Go مباشر نسبيًا، ما يسهّل المساهمة.
- من السهل نشر ملفات Go التنفيذية لأنها لا تحتاج إلى بيئة تشغيل.
- Go تُجيد الترجمة المتقاطعة.
Nim
- لدى Nim معظم المزايا نفسها التي لدى Go.
- لم يكن لدينا كثير من الأشخاص الذين يعرفون Go ويستطيعون صيانة واجهة سطر الأوامر، فانتقلنا إلى Nim.
في أي أماكن أخرى تتوقّعون أن تجدوا هذه اللغات مستخدمة؟
- في أي مكان تكون فيه الأداء مهمًا (التعريفات، والألعاب، وأنظمة التشغيل، وأنظمة البناء والمترجمات)
- في أي مكان تكون فيه الموارد محدودة (مثل البرمجيات المضمّنة)
- في أي شيء يحتاج إلى قابلية نقل عالية، أي أن يعمل على منصات كثيرة مختلفة
ما هو كود الآلة؟
كما قلت سابقًا، تتميّز هذه اللغات في Mechanical March بأنها تُترجم إلى كود الآلة. هل يمكنك أن تشرح قليلًا ما هو كود الآلة، وما هو كود البايت في المقابل؟
- كود الآلة هو كود يمكن تشغيله مباشرة على الآلة.
- أما كود البايت فيحتاج إلى كود آخر يفسّره أو يترجمه إلى كود الآلة. لذلك يحتاج كود البايت إلى خطوة وسيطة قبل أن يمكن تنفيذه.
ما إيجابيات كود الآلة وسلبياته مقارنة بكود البايت؟
الإيجابيات:
- بدء تشغيل أسرع (بلا خطوة ترجمة JIT)
- أثر أصغر في الذاكرة (لا تُحمَّل أي بيئة تشغيل، ولا كود بايت في الذاكرة، وهذا رائع للأنظمة المضمّنة)
- لا تحتاج الآلة الهدف إلى تثبيت بيئة تشغيل (وهذا مهم لإبقاء حاويات docker صغيرة)
السلبيات:
- غير قابل للنقل. كود البايت قابل للنقل، أما الكود المُترجم إلى آلة فهو خاص بمنصة بعينها.
- لا يمكن إجراء تحسينات متقدمة مثل التحسين الموجَّه بالقياس (تحديد أفضل طريقة لترجمة الكود (أو كود البايت) بعد تشغيله فترة من الزمن)
ملاحظة: الأساليب الهجينة ممكنة، حيث تُترجم اللغة إلى كود البايت ثم تستخدم أداة أخرى لترجمة ذلك الكود إلى كود الآلة.
تطوّر برمجة الأنظمة
حسنًا. لنلقِ نظرة موجزة على تطوّر بعض اللغات هذا الشهر. لنبدأ أولًا من C وكيف تطوّرت منها C++. حدّثنا قليلًا عن هاتين اللغتين.
C لغة منخفضة المستوى جدًا. يبدو شعورك معها كأنك فوق كود الآلة بقليل فقط. وهذا يجعلها قوية جدًا وقابلة للتحسين بدرجة عالية، لكنه يجعلها أيضًا عُرضة لـ bugs إلى حد ما (مثل استثناءات المؤشر الفارغ وتجاوزات المخزن المؤقت). إدارة الذاكرة كلها يدوية، وبالتالي فهي مسؤولية المبرمج، وقد تؤدي إلى bugs و/أو تسريبات في الذاكرة. C++ مثل C لكن مع دعم البرمجة كائنية التوجه. ما زالت منخفضة المستوى إلى حد كبير وتتطلب منك إدارة الذاكرة يدويًا. وكلتا اللغتين C وC++ تتيحان لك كتابة التجميع المضمّن (ASM)!
وماذا عن لغات الأنظمة الأحدث؟ كيف تطوّرت؟
تدعم جميع لغات برمجة الأنظمة الحديثة إدارة الذاكرة تلقائيًا، إما عبر عدّ المراجع، أو جامع المهملات، أو آلية أخرى.
تدعم لغات برمجة الأنظمة القديمة كلها المؤشرات الفارغة (وسمّاها Tony Hoare "خطأ المليار دولار"). وهي سيئة السمعة لأنها تؤدي إلى أخطاء وقت التشغيل وثغرات أمنية. تتخلص العديد من اللغات الحديثة من القيمة الفارغة، أو على الأقل تتطلب جهدًا لاستخدامها.
وتغيير آخر هو الانتقال من كون القيم القابلة للتغيير هي الافتراضية إلى كون القيم غير القابلة للتغيير افتراضية. فمثلًا، كل من 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
- واحدة من أقدم اللغات، وربما الأكثر استخدامًا في العالم
- تُكتب بها أطنان من البرمجيات، مثل Unix وLinux
- شديدة التأثير (فكّر في: وجود لغات شبيهة بـ C)
- إدارة يدوية للذاكرة
- أداء عالٍ جدًا (قريبة من "العتاد")
- تعمل في أي مكان
- مثالية للأنظمة المضمّنة
- لغة صغيرة نسبيًا
C++
- طوّرها Bjarne Stroustrup
- خَلَفٌ لـ C، لكن مع توجّه كائني مضاف (C مع أصناف)
- ساعدت في تعميم البرمجة كائنية التوجه
- مزايا لغوية أعلى مستوى من C
- تدعم البرمجة العامة عبر القوالب
- تُضيف دعم الوحدات عبر فضاءات الأسماء
- كثير من الألعاب (ومحركاتها) مكتوبة بـ C++، وأجزاء كبيرة من Windows
- إدارة يدوية للذاكرة
- ما زالت تتطور وتُضاف إليها مزايا جديدة بانتظام (مواصفة ضخمة)
D
- طوّرها Walter Bright، ثم انضم إليه لاحقًا Andrei Alexandrescu
- اعتُبرت في الأصل إعادة هندسة لـ C++ (تعلّمًا من "أخطائها")، وهي تستلهم من لغات أخرى كثيرة
- متعددة الأنماط، تدعم البرمجة الأمرية والكائنية والوظيفية
- تشغيل بيني سهل مع C/C++
- صيغة موحّدة لاستدعاء الدوال
- تقييم الدوال وقت الترجمة (مثل توليد آلة حالة للتعبيرات النمطية وقت الترجمة)
- تدعم البرمجة الوظيفية والدوال "النقية"
- مزايا أمان كثيرة
- أمان الذاكرة عبر
@safe - العقود (شروط مسبقة ولاحقة، واللامتغيرات)
- دوال نقية
- تركيز على اختبارات الوحدة، مع وجود الاختبارات بجوار الكود المصدري الذي تختبره (احتاج ذلك إلى استثناء في موقع Exercism :))
- Unicode
Rust
- طوّرها Graydon Hoare، أحد موظفي Mozilla Research، ثم تبنّتها Mozilla رسميًا، وهي الآن جزء من Rust Foundation
- متعددة الأنماط، تدعم البرمجة كائنية التوجه (لكن برأي واضح، فلا وراثة مثلًا)، والأمرية والوظيفية (أنواع Option/Result، ومطابقة الأنماط)
- كُتبت بها أدوات جديدة كثيرة (مثل SWC، وأيضًا Gleam، وهي اللغة الثانية المدعومة في نواة Linux، ونعتمد عليها في عدّاد أسطر الكود)
- تركيز على الموثوقية والأداء
- مبنية على LLVM
- اللغة الأكثر تفضيلًا في استبيان StackOverflow على مدى 7 سنوات الماضية
- سريعة، ويعود ذلك جزئيًا إلى نواة ومكتبة قياسية بالحد الأدنى
- آمنة، من ناحية أمان الذاكرة وأمان الخيوط عبر الملكية ومدد الحياة، وهي غير قابلة للتغيير افتراضيًا
- نظام أنواع قوي، يلتقط العديد من bugs وقت الترجمة (خصوصًا ما يتصل بالذاكرة). ويُخرج المترجم رسائل خطأ مفيدة جدًا
- كل ما تحتاجه مضمّن: المترجم، وأداة البناء، والمنسّق، ومدير الحزم، وتكاملات IDE
- توثيق رائع (حتى إن هناك مستندًا ضخمًا عن كيفية عمل المترجم)
- قابلة للنقل: تُترجم إلى ملف تنفيذي واحد ساكن ولا تحتاج إلى تثبيت بيئة تشغيل
- تشغيل بيني سهل مع كود C
- تجريدات بلا كلفة
- تزامن بلا خوف
- ماكرو
Nim
- طوّرها Andreas Rumpf (وكان اسمها في الأصل Nimrod)
- صياغتها مستوحاة من Python
- متعددة الأنماط
- تُستخدم في Exercism داخل configlet
- أداء رائع: مكررات بلا كلفة إضافية، وتفضيل تخصيص الأنواع القائمة على القيم على المكدس
- نظام أنواع حديث ومعبّر: استنتاج الأنواع، والصفوف، والأنواع العامة، والأنواع الجمعية، و
async/await - جامع مهملات، لكن مع دعم إدارة ذاكرة حتمية (خيارات متعددة لإدارة الذاكرة)
- تنفيذ الكود وقت الترجمة
- صيغة موحّدة للاستدعاء
- ماكرو: سهولة توسيع اللغة
- نظام الآثار: ترميز الآثار الجانبية في نظام الأنواع
Go
- طوّرها Robert Griesemer وRob Pike وKen Thompson في Google
- أمرية/إجرائية في الأغلب، وتدعم أسلوبًا شبيهًا بالبرمجة كائنية التوجه (لكن بلا وراثة)
- تُستخدم في مشاريع كبيرة مثل Docker وKubernetes. وهي رائعة أيضًا لـ backends وواجهات سطر الأوامر (مثل esbuild)
- تهدف إلى أن تكون بسيطة بما يكفي لتحملها في رأسك (صياغة قليلة)
- أمان الذاكرة عبر جامع المهملات
- سريعة: ترجمة سريعة، واختبار سريع، وأداء سريع وقت التشغيل. ودعم مدمج لكتابة اختبارات الأداء!
- ذات رأي واضح: جهد كبير للتأثير في أسلوب كود Go: صياغة قليلة، وتنسيق الكود عبر
go fmt، وأدوات لفحص الكود بحثًا عن الاستخدام الاصطلاحي، وإخراج أخطاء لأشياء تُعد تحذيرات في لغات أخرى (مثل المتغيرات غير المستخدمة)، وتوثيق يسرد اصطلاحات Go الواجب استخدامها - نظام أنواع خفيف يجعل Go مرنة جدًا (يدعم استنتاج الأنواع)
- قابلة للنقل: تُترجم إلى ملف تنفيذي واحد ساكن ولا تحتاج إلى تثبيت بيئة تشغيل. وسهولة الترجمة المتقاطعة. والتزامن عبر goroutines (خفيفة الوزن) والتواصل عبر القنوات
- تنميط بنيوي عبر الواجهات (يشبه التنميط بالبط، لكن بفحص ساكن)
- معالجة الأخطاء: تشجّع اللغة على فحص الأخطاء والتعامل معها
VLang
- طوّرها Alexander Medvednikov وDelyan Angelov
- مستوحاة من Go:
- الاستراتيجية نفسها: "طريقة واحدة فقط لفعل الأشياء"
- الاستراتيجية نفسها: "صياغة بالحد الأدنى"
- الكوروتينات
- تختلف عن Go في:
- لا nil/null، بل تستخدم نوع النتيجة
- غير قابلة للتغيير افتراضيًا
- أنواع جمعية (وظيفية)
- إدراج القيم في السلاسل النصية
- بيئة تشغيل وملفات تنفيذية أصغر
- مطابقة الأنماط
- العمل على جعل جامع المهملات اختياريًا (autofree)
- تشغيل بيني مع C بلا كلفة
- توليد التوثيق من الكود
- مترجم سريع يستخدم ذاكرة قليلة
Zig
- طوّرها Andrew Kelley
- صياغة قليلة نسبيًا (ملف قواعد PEG من 500 سطر)
- تنفيذ الكود والانعكاس وقت الترجمة
- تهدف إلى أن تكون "واضحة"، بلا تدفق تحكم خفي ولا تخصيصات خفية ولا ماكرو أو برمجة فوقية
- تخصيص يدوي للذاكرة
- تدعم مخصصات مختلفة
- دوال المكتبة القياسية التي تخصص ذاكرة لها معامل مخصص
- إطار الاختبار يمكنه كشف تسريبات الذاكرة
- الأمان:
- الأخطاء قيم ويجب التعامل معها
- لا null، بل تستخدم النوع الاختياري
- يمكن كتابة الاختبارات في الملف نفسه الذي فيه الكود المصدري (مثل D)
- تستخدم LLVM كخلفية
- يمكنها ترجمة كود C/C++
- تشغيل بيني سهل مع C
- ترجمة متقاطعة سهلة
الخاتمة
سنترك الأمر هنا، إذ أتصور أن أذهان الجميع ممتلئة تمامًا في هذه المرحلة.
آمل أن تكون هذه مقدمة مفيدة وممتعة للغات المميزة هذا الشهر. أتمنى أن تستمتعوا كثيرًا باستكشاف هذه اللغات. نودّ أنا وErik أن نسمع أيها ستختارون وكيف وجدتموها، فالرجاء المشاركة في التعليقات أو في المنتدى!
شكرًا لمشاهدتكم!