صناعة المفسّرات › لغة Lox 4 / 35 السابق التالي

اللّغة Lox

ما العمل اللطيف الذي يمكنك فعله لشخصٍ ما أكثر من أن تُعدّ له الفطور؟

أنتوني بوردين

سنقضي بقيّة هذا الكتاب في إنارة كلّ زاويةٍ مظلمة من زوايا لغة Lox، لكنّ بدءك فوراً في صهر الشيفرة الخاصة بالمفسّر دون أن تلمح ولو لمحةً إلى ما سننتهي إليه يبدو قاسياً.

وفي الوقت نفسه، لا أريد أن أجرك خلال صفحاتٍ من الجداول مواصفات قبل أن تصل إلى محرّرك للنصوص. لذلك ستكون هذه مقدّمةً لطيفةً وودّية إلى Lox. وستُهمل فيها كثيرٌ من التفاصيل والحالات الحدّية. فلدينا وقتٌ وفير لتلك لاحقاً.

مرحباً، Lox

وهذا أوّل مذاق لك لـLox:

// Your first Lox program!
print "Hello, world!";

وكما توحي بذلك تعليق السطر الذي يبدأ بـ // والفاصلة المنقوطة في آخره، فإنّ صياغة Lox تنتمي إلى عائلة C. (ولا توجد أقواس حول النصّ لأنّ print جملة مدمجة، وليست دالة في مكتبة.)

والآن، لن أدّعي أنّ صياغة C رائعة. فلو أردنا شيئاً أنيقاً لاستنسخنا Pascal أو Smalltalk على الأرجح. ولو أردنا أن نذهب إلى حدّ التصغير الإسكندنافي الكامل لفعلنا Scheme. ولكلّ منها مزاياها.

ما الذي تملكه الصياغة الشبيهة بـC بدلاً من ذلك هو أمرٌ ستجده في اللّغة أكثر غالباً: المألوفيّة. وأنا أعرف أنّك مرتاحٌ بمثل هذا النمط، لأنّ اللغتين اللتين سنستعملهما لتنفيذ Lox -- Java و C -- ترثان هكذا أيضاً. واستعمال صياغة مشابهة لـLox يعني أنّ أمامك أمراً واحداً أقلّ لتتعلّمه.

لغة عالية المستوى

بينما انتهى هذا الكتاب أكبر مما كنت آمل، فهو ما زال أصغر من أن يسع لغةً ضخمة مثل Java. ولكي نتّسع في هذه الصفحات لتنفيذين كاملين للّغة Lox، لا بدّ أن تكون Lox نفسها مضغوطةً إلى حدٍّ كبير.

وحينما أفكّر في اللّغات التي تكون صغيرة لكن نافعة، تتبادر إلى ذهني لغات النصوص "العالية المستوى" مثل JavaScript و Scheme و Lua. ومن هذه الثلاث، تبدو Lox أقربها إلى JavaScript، في المقام الأوّل لأنّ معظم لغات صيغة C كذلك. وكما سنتعلّم لاحقاً، فإنّ نهج Lox في النطاق يتبع Scheme عن قرب. أمّا نكهة C من Lox التي سنبنيها في [الجزء الثالث][] فمديونة إلى حدٍّ كبير لتطبيق Lua النظيف الكفؤ.

تتشارك Lox مع تلك اللّغات الثلاث جانبَين آخرين:

التنميط الديناميكي

Lox مُنمَّطة ديناميكياً. فيمكن للمتغيّرات أن تخزّن قيماً من أيّ نوع، بل يمكن للمتغيّر الواحد أن يخزّن قيماً من أنواعٍ مختلفة في أوقاتٍ مختلفة. فإن حاولتَ إجراء عمليةٍ على قيمٍ من النوع الخطأ -- كأن تقسم عدداً على نصّ مثلاً -- فسيُكتشف الخطأ ويُبلّغ عنه في وقت التشغيل.

وهناك أسبابٌ كثيرة تُحبّ بها الأنواع الساكنة، لكنّها لا تُرجّح الأسباب العملية لاختيار الأنواع الديناميكيّة في Lox. فنظامُ أنواعٍ ساكنٍ عملٌ ضخم في التعلّم والتنفيذ. وتخطّيها يمنحك لغةً أبسط وكتاباً أقصر. وسنُقلع مفسّرنا ليعمل وينفّذ قطعاً من الشيفرة في أبكر وقت إن أجّلنا فحص الأنواع إلى وقت التشغيل.

إدارة الذاكرة التلقائيّة

توجد اللّغات عالية المستوى كي تُزيل عن كاهلك العمل الشاقّ المُعرَّض للخطأ في المستويات الدنيا، فما أتعسّ من إدارة تخصيص والذاكرة وتحريرها يدوياً؟ لا أحد يستيقظ ليحيّي شمس الصباح قائلاً: "لا أطيق الانتظار لأحدّد الموضع الصحيح لأُنادي فيه free() لكلّ بايت من الذاكرة أُخصّصه اليوم!" "لا أطيق الانتظار لأحدّد الموضع الصحيح لأُنادي فيه free() لكلّ بايت من الذاكرة أُخصّصه اليوم!"

وهناك تقنيّتان رئيسيّتان لإدارة الذاكرة: العدّ بالمراجع (reference counting) وجمع النفايات بالتتبّع (tracing garbage collection) (ويُسمّى عادةً جمع النفايات (garbage collection) أو GC فقط). وعدّادات المراجع أبسط بكثير في التنفيذ -- وأظنّ أنّ هذا هو سبب بدء Perl و PHP و Python جميعاً باستعمالها. لكنّ مع الوقت تصبح قيود عدّ المراجع مزعجةً أكثر من اللازم. وانتهت تلك اللّغات جميعاً في النهاية إلى إضافة جامع نفايات تتبّعي كامل، أو على الأقلّ ما يكفي منه لتنظيف دورات الأشياء.

للجمع التتبّعيّ سمعةٌ مُرعبة. فهو فعلاً مُرهقٌ قليلاً أن تعمل على مستوى الذاكرة الخام. وتنقيح أخطاء جامع نفايات قد يتركك ت يرى في أحلامك مطبوعاتٍ سداسية. لكنّ تذكّر أنّ هذا الكتاب في كون Lox الصغير هذا، الذرّات التي يتكوّن منها كلّ المادة هي بكتابة جامع نفايات خاصّنا. وأظنّ أنّك ستجد الخوارزمية بسيطةً إلى حدٍّ كبير وممتعةً في التنفيذ.

أنواع البيانات

في كون Lox الصغير هذا، الذرّات التي يتكوّن منها كلّ المادّة هي أنواع البيانات المدمجة. ولا توجد سوى حفنة:

  • القيم المنطقيّة. لا يمكنك البرمجة دون منطق، ولا يمكنك المنطق دون قيم منطقيّة. "صحيح" و"خطأ"، Yin و Yang في البرمجيات. وخلافاً لبعض اللّغات القديمة التي تعيد توظيف نوعٍ قائم لتمثيل الصدق والكذب، تمتلك Lox نوعاً منطقيّاً مخصّصاً. ربما لا نعمل في هذه الرحلة بأسلوبٍ متأنٍّ، لكنّنا لسنا بُوّاب.

    وهناك قيمتان منطقيّتان بالطبع، لكلٍّ منهما قيمةٌ حرفية (literal).

    true;  // Not false.
    false; // Not *not* false.
    
  • الأعداد. تمتلك Lox نوعاً واحداً فقط من الأعداد: أعداد الفاصلة العائمة ذاتُ الدقة المزدوجة. وبما أنّ أعداد الفاصلة العائمة تستطيع أيضاً أن تمثّل مدىً واسعاً من الأعداد الصحيحة، فإنّ ذلك يغطّي مساحةً كبيرة، مع إبقاء الأمور بسيطة.

    تمتلك اللّغات المتكاملة صياغاتٍ كثيرة للأعداد -- ست عشري، وعلم الأعداد، وثماني، وكلّ أنواع المرح هذه. وسنكتفي بالقيم الحرفية (literals) الأعداد الأساسية الصحيحة والعشرية.

    1234;  // An integer.
    12.34; // A decimal number.
    
  • النصوص. لقد رأينا قيمةً نصّيةً حرفية في المثال الأوّل بالفعل. وكما في معظم اللّغات، تُحاط النصوص بعلامتَي تنصيص مزدوجَتين.

    "I am a string";
    "";    // The empty string.
    "123"; // This is a string, not a number.
    

    وكما سنرى حين نصل إلى تنفيذها، فهناك قدرٌ كبير من التعقيد مختبئ في ذلك التسلسل البريء من المحارف.

  • Nil. وهناك قيمةٌ مدمجةٌ أخيرة لم تُدعَ قطّ إلى الحفلة، لكنّها تبدو دائماً ما تحضر. وهي تمثّل "لا قيمة". تُسمّى "null" في لُغاتٍ كثيرة أخرى. أمّا في Lox فنكتبها nil. (وحين نصل إلى تنفيذها، سيساعدنا ذلك على التمييز بين nil الخاصة بـLox وبين null في Java أو C.)

    وهناك حججٌ جيّدة لعدم وجود قيمة null في اللّغة، إذ إنّ أخطاء المؤشّرات الفارغة بلاءُ صناعتنا. فلو كنا ننفّذ لغةً مُنمَّطة ساكناً، لكان الأمر يستحقّ محاولة منعها. لكن في اللّغة المُنمَّطة ديناميكياً، يكون التخلّص منها أحياناً أكثر إزعاجاً من وجودها.

التعبيرات

إذا كانت أنواع البيانات المدمجة وقيمها الحرفية ذرّات، فلا بدّ أن تكون التعبيرات (expressions) هي الجزيئات. وسيكون معظمها مألوفاً لك.

العمليات الحسابيّة

تقدّم Lox معاملات الحساب الأساسيّة التي تعرفها وتحبّها من C ومن لغاتٍ أخرى:

add + me;
subtract - me;
multiply * me;
divide / me;

ولأنّها اثنان، فإنّ هذه المعاملات تُسمّى معاملات ثنائيّة (binary). (وليس لها أيّ علاقة باستعمال "ثنائي" بمعنى الآحاد والأصفار.) ولأنّ المعامل ثابت في وسط المعاملات، فإنّها تُسمّى أيضاً معاملات وَسطيّة (infix) (بخلاف المعاملات السابقة (prefix) التي يأتي المعامل فيها قبل المعاملات، واللاحقة (postfix) التي يأتي بعدها).

أحد معاملات الحساب هو في الواقع معاملٌ محدَّد (infix) وسابق (prefix) في آنٍ واحد. فمعامل - يمكن أن يُستعمل أيضاً لِـنفي عدد.

-negateMe;

وجميع هذه المعاملات تعمل على الأعداد، وتمرير أيّ أنواعٍ أخرى إليها خطأ. والاستثناء هو معامل + -- يمكنك أيضاً تمرير إليه نصّين لِسلسلتِهما.

المقارنة والتساوي

ولنتابع، فلديك بضعة معاملاتٍ أخرى تُعيد دائماً نتيجةً منطقيّة. ويمكنك مقارنة الأعداد (الأعداد فقط) باستخدام معاملات المقارنة القديمة المحترمة.

less < than;
lessThan <= orEqual;
greater > than;
greaterThan >= orEqual;

ويمكنك اختبار قيمتين من أيّ نوعٍ للتساوي أو عدم التساوي.

1 == 2;         // false.
"cat" != "dog"; // true.

حتى مع اختلاف الأنواع.

314 == "pi"; // false.

قيم الأنواع المختلفة لا تكون متكافئة أبداً.

123 == "123"; // false.

أنا معاذ ضد التحويلات الضمنية في الغالب.

المعاملات المنطقيّة

معامل النفي، وهو معاملٌ سابق على الصنف !، يُعيد false إن كان معاملُه صحيحاً، والعكس.

!true;  // false.
!false; // true.

والمعاملان المنطقيّان الآخران هما بالفعل بنائيا تحكّم في هيئة تعبيرات. فالتعبير and يحدّد ما إذا كان قيمتان كلتاهما صحيحتان. فهو يُعيد المعامل الأيسر إن كان خطأً، وإلاّ فالمعامل الأيمن.

true and false; // false.
true and true;  // true.

وتعبير or يحدّد ما إذا كانت أيّ من قيمتين (أو كلتاهما) صحيحة. فهو يُعيد المعامل الأيسر إن كان صحيحاً، وإلاّ فالمعامل الأيمن.

false or false; // false.
true or false;  // true.

وسبب كون and و or أشبه ببنايا التحكّم في التنفيذ هو أنّهما يتحقّقان شرطاً ويقفان مبكّراً (short-circuit). فـand لا تعيد المعامل الأيسر فحسب إذا كان خطأً، بل لا تُقيّم المعامل الأيمن حتى. وبالمقابل (عكساً بالتناسب؟)، إذا كان المعامل الأيسر لتعبير or صحيحاً، تُتخطّى العملية على اليمين.

الأسبقية والتجميع

كلّ هذه المعاملات لها الأسبقية والارتباطية (associativity) نفسهما اللذين تتوقّعهما قادماً من C. (وحين نصل إلى التحليل النحوي، سنصبح أدقّ بكثير في ذلك.) وفي الحالات التي لا تكون فيها الأسبقية هي ما تريد، يمكنك استعمال () لتجميع الأشياء.

var average = (min + max) / 2;

ولأنّها ليست مثيرة للاهتمام تقنياً، فقد حذفتُ بقيّة تشكيلة المعاملات المعتادة من لغتنا الصغيرة. لا معاملات ثنائية (bitwise)، ولا إزاحة (shift)، ولا باقٍ (modulo)، ولا معاملات شرطيّة. أنا لا أُقيّمك، لكنّك ستربح نقاطاً إضافية في قلبي إن زدتَ تطبيقك الخاص بـLox بهذه المعاملات.

إلىها لاحقاً)، فلنرتقِ مستوى.

الجمل

الآن وصلنا إلى الجمل (statements). فبينما مهمّة التعبير الرئيسيّة أن ينتج قيمة، مهمّة الجملة أن تُنتج أثراً. ولأنّ الجمل لا تقوّم إلى قيمة بحكم تعريفها، فإنّها كي تكون نافعة لا بدّ أن تغيّر العالم بطريقةٍ ما على أيّ حال -- عادةً بتعديل بعض الحالة، أو بقراءة مُدخَلات، أو بإنتاج مخرجات.

لقد رأيتَ من قبل نوعَين من الجمل. كان أوّلهما:

print "Hello, world!";

تُقيّم جملة print تعبيراً واحداً وتعرض النتيجة على المستخدم. وقد رأيتَ أيضاً بعض الجمل مثل:

"some expression";

تعبيرٌ يليه فاصلة منقوطة (;) يرقي بالتعبير إلى مرتبة الجملة. ويُسمّى هذا (بخيالٍ وافٍ) جملة تعبيرية (expression statement).

وإذا أردتَ حشر سلسلةٍ من الجمل حيث يُتوقَّع جملةً واحدة، يمكنك لفّها في كتلة (block).

{
  print "One statement.";
  print "Two statements.";
}

وتؤثّر الكتل أيضاً في النطاق، وهذا ما يقودنا إلى القسم التالي...

المتغيّرات

تُصرِّح عن المتغيّرات باستخدام جمل var. فإن حذفتَ المُهيّئ (initializer)، فإنّ قيمة المتغيّر تكون nil افتراضياً.

var imAVariable = "here is my value";
var iAmNil;

وبعد التصريح عنه، يمكنك بالطبع الوصول إلى المتغيّر وإسناد قيمة إليه باستخدام اسمه.

var breakfast = "bagels";
print breakfast; // "bagels".
breakfast = "beignets";
print breakfast; // "beignets".

لن أتعمّق هنا في قواعد نطاق المتغيّرات، لأنّنا سنقضي وقتاً مدهشاً في الفصول اللاحقة في تخطيط كلّ إنشٍ من تلك القواعد. وفي معظم الحالات، يعمل الأمر كما تتوقّع قادماً من C أو Java.

التحكّم في التنفيذ

من الصعب كتابة برامج نافعة إن لم يستطع تخطّي بعض الشيفرة أو تنفيذ بعضها أكثر من مرّة. وهذا يعني التحكّم في التنفيذ (control flow). وإلى جانب المعاملات المنطقيّة التي غطّيناها، ترفع Lox ثلاث جملٍ مباشرةً من C.

تُنفّذ جملة if إحدى جملتين بناءً على شرطٍ ما.

if (condition) {
  print "yes";
} else {
  print "no";
}

وتُنفّذ حلقة while جسمها مراراً طالما كان تعبير الشرط يُقيَّم إلى صحيح.

var a = 1;
while (a < 10) {
  print a;
  a = a + 1;
}

وأخيراً، لدينا حلقات for.

for (var a = 1; a < 10; a = a + 1) {
  print a;
}

وهذه الحلقة تفعل الشيء نفسه الذي تفعله حلقة while السابقة. ولمعظم اللّغات الحديثة أيضاً نوعٌ ما من حلقة for-in أو foreach للتكرار الصريح على مختلف أنواع التسلسلات. وفي لغةٍ حقيقية، فإنّ ذلك أجمل من حلقة for البدائية بأسلوب C التي حصلنا عليها هنا. تُبقي Lox الأمر أساسياً.

الدوالّ

يبدو تعبير استدعاء دالةٍ كما هو في C تماماً.

makeBreakfast(bacon, eggs, toast);

ويمكنك أيضاً استدعاء دالة دون تمرير أيّ شيء إليها.

makeBreakfast();

وخلافاً لـRuby مثلاً، فإنّ الأقواس إلزامية في هذه الحالة. فإن تركتَها، لن يستدعي الاسمُ الدالة، بل سيشير إليها فحسب.

ولن تكون اللّغة ممتعة إن لم تستطع تعريف دوالّك الخاصّة. وفي Lox، تفعل ذلك بـfun.

fun printSum(a, b) {
  print a + b;
}

والآن هو الوقت المناسب لتوضيح بعض المصطلحات. فبعض الناس يقذف بكلمتي "وسيط" و"وسيلة" (parameter و argument) كأنّهما مترادفتان، وهما كذلك عند كثيرين. وسنقضي وقتاً طويلاً في تفصيل أنعم شعرةٍ فارقة في الدلالة، لذا لنُحدّد مصطلحاتنا. من الآن فصاعداً:

  • الوسيلة (argument) هي قيمةٌ فعليةٌ تمرّرها إلى الدالة عند استدعائها. إذن لاستدعاء الدالة قائمةُ وسائل. وأحياناً تسمع استعمال الوسيط الفعليّ (actual parameter) لها.

  • الوسيط (parameter) هو متغيّرٌ يحمل قيمة الوسيلة داخل جسم الدالة. إذن تصريحُ الدالة له قائمة وسائط. وآخرون يسمّون هذه الوسائط الصورية (formal parameters) أو مجرّد الصوريّات (formals).

ويكون جسم الدالة دائماً كتلةً. وداخلها يمكنك إرجاع قيمة (return) باستخدام جملة return.

fun returnSum(a, b) {
  return a + b;
}

وإذا بلغ التنفيذ نهاية الكتلة دون أن يصطدم بـreturn، فإنّه يُرجِع ضمناً nil.

الإغلاقات

الدوالّ في Lox من الدرجة الأولى (first class)، وهذا يعني ببساطة أنّها قيمٌ حقيقيةٌ تستطيع أن تأخذ مرجعاً إليها، وتخزّنها في متغيّرات، وتتداولها، وما إلى ذلك. وهذا يعمل:

fun addPair(a, b) {
  return a + b;
}

fun identity(a) {
  return a;
}

print identity(addPair)(1, 2); // Prints "3".

ولأنّ تصريحات الدوالّ جمل، يمكنك تصريح دوالّ محلّية داخل دالةٍ أخرى.

fun outerFunction() {
  fun localFunction() {
    print "I'm local!";
  }

  localFunction();
}

وإن جمعتَ بين الدوالّ المحلّية والدوالّ من الدرجة الأولى ونطاق الكتل، فتصطدم بهذا الموقف المهمّ:

fun returnFunction() {
  var outside = "outside";

  fun inner() {
    print outside;
  }

  return inner;
}

var fn = returnFunction();
fn();

هنا، تصل inner() إلى متغيّرٍ محلّي صُرِّح خارج جسمها في الدالة المحيطة. هل هذا جائز؟ وبما أنّ لغةً كثيرة استعارت هذه الميزة من Lisp، فأنت على الأرجح تعرف أنّ الجواب نعم.

ولكي يعمل ذلك، لا بدّ أن "تمسك" inner() بمراجع إلى أيّ متغيّراتٍ محيطة تستعملها كي تبقى موجودة حتى بعد أن تكون الدالة الخارجية قد أعادت. ونسميّ الدوالّ التي تفعل ذلك إغلاقات (closures). وفي هذه الأيام، يُستعمل المصطلح غالباً لكلّ دالةٍ من الدرجة الأولى، وإنّ ذلك تسميةٌ خاطئة إلى حدٍّ ما إن لم تكن الدالة تُغلق أيّ متغيّرات.

وكما يمكنك تخيّل، فإنّ تنفيذ هذا يضيف بعض التعقيد، لأنّنا لم نعد نستطيع أن نفترض أنّ نطاق المتغيّرات يعمل كالمكدّس تماماً حيث تتبخّر المتغيّرات المحلّية لحظة عودة الدالة. وسنمضي وقتاً ممتّعاً في تعلّم كيف نجعل هذا يعمل على نحو صحيح وكفؤ.

الأصناف

ولأنّ Lox مُنمَّطة ديناميكياً، ولأنّ نطاقها معجميّ (بمعنى تقريبي "كتلي")، ولأنّ لها إغلاقات، فهي في منتصف الطريق تقريباً إلى أن تكون لغةً وظيفية. لكنّك كما سترى، هي في منتصف الطريق تقريباً أيضاً إلى أن تكون لغةً موجّهةً نحو الأشياء. ولكلّ النمطين مزايا كثيرة، فظننتُ أنّه يستحقّ أن نغطّي بعضاً من كلٍّ منهما.

ولأنّ الأصناف هُاجمت لأنّها لم ترقَ إلى مستوىضجيجها، دعني أشرح أوّلاً لماذا وضعتُها في Lox وفي هذا الكتاب. فهناك سؤالان فعلاً:

لماذا قد ترغب أيّ لغة في أن تكون موجّهةً نحو الأشياء؟

بعد أننفدت لغةً موجّهةٌ نحو الأشياء مثل Java تذاكرها ولم تعد تقدّم إلا عروضاً في الحلبات، لم يعد محبّتها على الموضة. فلماذا يصنع أحدهم لغةً جديدة بأشياء؟ أليس هذا أشبه بإصدار موسيقى على شريط 8 مسارات؟

من الصحيح أنّ نوبة "الوراثة كلّ الوقت" في التسعينيات أنتجت بعض تسلسلات أصناف وحشية، لكنّ البرمجة الموجّهة نحو الأشياء (OOP) لا تزال ممتعةً إلى حدٍّ كبير. فقد كُتبت مليارات الأسطر من الشيفرة الناجحة بلغات OOP، وشُحنت ملايين التطبيقات إلى مستخدمين سعداء. ومن المرجّح أنّ أغلبية المبرمجين العاملين اليوم يستعملون لغةً موجّهةً نحو الأشياء. ولا يمكن أن يكونوا جميعاً مخطئين إلى هذا الحدّ.

وتحديداً، في اللّغة المُنمَّطة ديناميكياً، تكون الأشياء مفيدةً إلى حدٍّ كبير. فنحن نحتاج إلى طريقة ما لتعريف أنواع بيانات مركّبة (compound) تحزم كتل من الأشياء معاً.

وإذا استطعنا أيضاً تعليق الدوالّ عليها، فسنتفادى الحاجة إلى أن نبدأ كلّ دوالّنا باسم نوع البيانات التي تعمل عليه كي نتجنّب التصادم مع دوالٍ مشابهة لأنواعٍ أخرى. ففي Racket مثلاً، تنتهي إلى تسمية دوالّك بأسماء مثل hash-copy (لنسخ جدول تجزئة) و vector-copy (لنسخ متجه) كي لا يطأ كلٌّ منهما على الأخرى. أمّا الدوالّ (methods) فهي محصورة في الكائن، فيختفي تلك المشكلة.

لماذا Lox موجّهةٌ نحو الأشياء؟

باستطاعتي أن أدّعي أنّ الأشياء أعجوبة، لكنّها تبقى خارج نطاق الكتاب. فمعظم كتب لغات البرمجة، وخاصة التي تحاول تنفيذ لغةٍ كاملة، تُهمل الأشياء. وبالنسبة لي، هذا يعني أنّ الموضوع غير مغطّى جيّداً. ونظراً لانتشار هذا النمط الهائل، فإنّ هذا الإهمال يجعلني حزيناً.

بالنظر إلى كم منّا نقضي اليوم كلّه في استعمال لغات OOP، يبدو أنّ العالم يمكن أن يستفيد من بعض التوثيق عن كيفية صنع واحدة. وكما سترى، تبيّن أنّ هذا مثيرٌ للاهتمام إلى حدٍّ كبير. ليس صعباً كما قد تخشى، وليس بسيطاً كما قد تظنّ أيضاً.

الأصناف أم النماذج الأوّلية

حين نتحدّث عن الأشياء، فهناك فعلياً منهجان: [الأصناف][] و[النماذج الأوّلية][] (prototypes). جاء الأصناف أولاً، وهي أشيع بفضل C++ و Java و C# وأقرانها. أمّا النماذج الأوّلية فكانت فرعاً شبه منسيٍّ إلى أن استحوذت JavaScript على العالم عرَضاً.

وفي اللّغات القائمة على الأصناف، هناك مفهومان أساسيان: النسخ (instances) والأصناف. فالنسخ تخزّن حالة كلّ كائن ولديها مرجع إلى صنف ذلك النسخة. والأصناف تحوي الدوالّ وسلسلة الوراثة. ولاستدعاء دالةٍ على نسخة، يوجد دائماً مستوىٌ من التوجيه غير المباشر. فأنت تبحث عن صنف النسخة ثمّ تجد الدالة هناك:

كيف تُبحث الحقول والدوال في الأصناف والنسخ

أمّا اللّغات القائمة على النماذج الأوّلية تدمج هذين المفهومين. فلا توجد سوى الأشياء -- لا أصناف -- وقد يحتوي كلّ كائنٍ منفرد على حالة ودوالّ. ويمكن للأشياء أن ترث من بعضها مباشرةً (أو "تُفوِّض إلى" بلغة النماذج الأوّلية):

كيف تُبحث الحقول والدوال في نظام قائم على النموذج الأوّلي

وهذا يعني أنّ اللّغات القائمة على النماذج الأوّلية أكثر أساسيةً من الأصناف في بعض الجوانب. فهي ممتعةٌ حقّاً في التنفيذ لأنّها بسيطة إلى هذا الحدّ. كما أنّها تستطيع التعبير عن كثير من الأنماط غير المعتادة التي تصرفك عنها الأصناف.

لكنّني أطلعتُ على الكثير من الشيفرة المكتوبة بلُغات النماذج الأوّلية -- بما فيها بعض ما من صنعي. فهل تعرف ماذا يفعل الناس عادةً بكلّ تلك القدرة والمرونة في النماذج الأوّلية؟ ...إنّهم يستعملونها لإعادة اختراع الأصناف.

لا أعرف لماذا يحدث هذا، لكنّ الناس يبدو أنّهم يفضّلون بشكل طبيعي أسلوباً قائماً على الأصناف (كلاسيكي؟ أرستقراطي؟). والنماذج الأوّلية أبسط في اللّغة فعلاً، لكنّها لا تنجز ذلك على الأرجح إلا بـدفع التعقيد إلى المستخدم. لذا، في حالة Lox، سنوفّر على مستخدمينا هذا العناء وندمج الأصناف فيها مباشرةً.

الأصناف في Lox

كفى تبريراً، ولنرَ ما لدينا فعلاً. تضمّ الأصناف في معظم اللّغات كوكبةً من الميزات. أما في Lox فقد اخترتُ ما أظنّه أسطع نجومها. وتُصرِّح عن الصنف ودوالّه هكذا:

class Breakfast {
  cook() {
    print "Eggs a-fryin'!";
  }

  serve(who) {
    print "Enjoy your breakfast, " + who + ".";
  }
}

يحتوي جسم الصنف على دوالّه. وهي تبدو كتصريحات دوالّ لكن بلا الكلمة المفتاحية fun method. وحين يُنفَّذ تصريحُ الصنف، تُنشئ Lox كائنَ صنف وتخزّنه في متغيّر باسم الصنف. وكما الدوالّ تماماً، الأصناف من الدرجة الأولى في Lox.

// Store it in variables.
var someVariable = Breakfast;

// Pass it to functions.
someFunction(Breakfast);

ثمّ نحتاج إلى طريقة لإنشاء النسخ. يمكننا إضافة كلمةٍ مفتاحية شبيهة بـnew، لكنّنا لإبقاء الأمور بسيطة جعلنا الصنف نفسه في Lox دالةَ مصنع للنسخ. استدعِ الصنف كدالة، فيُنتج نسخةً جديدة من نفسه.

var breakfast = Breakfast();
print breakfast; // "Breakfast instance".

الإنشاء والتهيئة

الأصناف التي لا تتضمّن سوى سلوكٍ ما ليست مفيدةً إلى حدٍّ كبير. فالفكرة وراء البرمجة الموجّهة نحو الأشياء هي حشر السلوك والحالة معاً. ولفعل ذلك تحتاج إلى حقول. وتسمح لك Lox، مثل غيرها من اللّغات المُنمَّطة ديناميكياً، بإضافة الخصائص على الأشياء بحرّية.

breakfast.meat = "sausage";
breakfast.bread = "sourdough";

والإسناد إلى حقلٍ ينشئه إن لم يكن موجوداً.

وإذا أردتَ الوصول إلى حقلٍ أو دالةٍ على الكائن الحالي من داخل دالة، فاستعمل this القديمة المألوفة.

class Breakfast {
  serve(who) {
    print "Enjoy your " + this.meat + " and " +
        this.bread + ", " + who + ".";
  }

  // ...
}

ومن حشر البيانات داخل كائنٍ ما هو ضمان أنّ الكائن في حالةٍ صالحة وقت إنشائه. ولفعل ذلك يمكنك تعريف مُهيّئ. فإذا كان لصنفك دالةٌ اسمها init()، فستُدعى تلقائياً حين يُبنى الكائن. وأيّ وسائط تُمرَّر إلى الصنف تُوجَّه إلى مُهيّئه.

class Breakfast {
  init(meat, bread) {
    this.meat = meat;
    this.bread = bread;
  }

  // ...
}

var baconAndToast = Breakfast("bacon", "toast");
baconAndToast.serve("Dear Reader");
// "Enjoy your bacon and toast, Dear Reader."

الوراثة

تتيح لك كلّ لغةٍ موجّهةٍ نحو الأشياء ليس فقط تعريف الدوالّ بل إعادة استعمالها عبر أصنافٍ أو أشياءٍ متعدّدة. ولهذا، تدعم Lox الوراثة المفردة (single inheritance). فحين تُصرِّح عن صنف، يمكنك تحديد صنفٍ يرث منه باستخدام معامل أصغر-من (<).

class Brunch < Breakfast {
  drink() {
    print "How about a Bloody Mary?";
  }
}

هنا، Brunch هي الصنف المشتق (derived class) أو الصنف الفرعي (subclass)، و Breakfast هي الصنف الأساس (base class) أو الصنف الأعلى (superclass).

وكلّ دالةٍ معرَّفة في الصنف الأعلى متاحةٌ أيضاً لصنوفه الفرعية.

var benedict = Brunch("ham", "English muffin");
benedict.serve("Noble Reader");

حتى دالة init() ذاتها يتمّ توريثها. وعملاً، يريد الصنف الفرعي عادةً أن يعرّف دالة init() خاصّة به أيضاً. لكنّ دالة الأصل تحتاج أيضاً إلى أن تُستدعى كي يتمكّن الصنف الأعلى من الإبقاء على حالته. ونحتاج إلى طريقة لاستدعاء دالةٍ على نسختنا خاصّة من دون أن نمرّ على دوالّنا الخاصّة.

وكما في Java، تستعمل super لذلك.

class Brunch < Breakfast {
  init(meat, bread, drink) {
    super.init(meat, bread);
    this.drink = drink;
  }
}

هذا كلّ ما في الأمر تقريباً بالنسبة للتجهيز نحو الأشياء. وقد حاولتُ إبقاء مجموعة الميزات دنيا. لكنّ بنية الكتاب أرتغلت تنازلاً واحداً. فـLox ليست لغةً موجّهةً نحو الأشياء خالصة. ففي لغة OOP حقيقية كلّ كائنٍ نسخةٌ من صنف، حتى القيمُ الأوّلية مثل الأعداد والقيم المنطقيّة.

ولأنّنا لا ننفّذ الأصناف إلّا بعد أن نبدأ في التعامل مع الأنواع المدمجة بوقتٍ طويل، فإنّ ذلك كان سيكون صعباً. لذا فإنّ قيم الأنواع الأوّلية ليست كائناتٍ حقيقية بالمعنى الذي تعني به النسخ من الأصناف. فلا لها دوالّ ولا خصائص. ولو كنتُ أحاول أن أجعل Lox لغةً حقيقية لمستخدمي حقيقيين لكنّي سأُصلح ذلك.

المكتبة المعياريّة

شبهُنا من الانتهاء. هذه هي اللّغة كلّها، فلم يبقَ إلّا المكتبة "الأساسيّة" أو "المعياريّة" -- وهي مجموعة الوظائف المنفَّذة مباشرةً داخل المفسّر التي يُبنى فوقها كلّ سلوكٍ يعرّفه المستخدم.

هذا أحزنُ أجزاء Lox. فمكتبتها المعياريّة تتجاوز حدود التصغير وتقترب من النّيهية الصريحة. فالشيفرة النموذجية في الكتاب لا تحتاج إلّا أن نُثبت أنّ الشيفرة تعمل وتفعل ما يُفترض أن تفعله. ولذلك لدينا فعلاً جملة print المدمجة.

ولاحقاً، حين نبدأ في التحسين، سنكتب بعض المعايير (benchmarks) ونرى كم يستغرق تنفيذ الشيفرة. وهذا يعني أنّنا نحتاج إلى تتبّع الزمن، لذا سنعرّف دالةً مدمجةً واحدة، وهي clock()، التي تُعيد عدد الثواني منذ بدء البرنامج.

و... هذا كلّ شيء. أعرف، صحيح؟ إنّه مُحرِج.

وإذا أردتَ تحويل Lox إلى لغةٍ نافعةٍ حقّاً، فأوّل ما ينبغي أن تفعله هو توسيع هذا. فمعالجة النصوص، والدوالّ المثلثية، ومدخلات /مخرجات الملفات، والشبكات، بل حتى قراءة مُدخَلات المستخدم ستكون مفيدة. لكنّنا لا نحتاج أيّاً من ذلك لهذا الكتاب، وإضافته لن تعلّمك شيئاً مثيراً، لذا تركتُه.

لا تقلق، فسنملك في اللّغة نفسها الكثير من الأشياء المثيرة لنشغلنا بها.

التحديات

  1. اكتب بعض برامج Lox النموذجية وشغّلها (يمكنك استعمال تطبيقات Lox في مستودعي). حاول أن ت come up بسلوكٍ في الحالات الحدّية لم أحدّده هنا. هل تفعل ما تتوقّع؟ ولماذا أو لماذا لا؟

  2. تترك هذه المقدّمة غير الرسمية الكثير دون تحديد. اكتب عدّة أسئلة مفتوحة لديك حول صياغة اللّغة ودلالتها. ما رأيك في الإجابات؟

  3. Lox لغةٌ صغيرة جداً. ما الميزات التي تظنّ أنها تنقصها وستجعل استعمالها في برامجٍ حقيقية مملاً؟ (بطبيعة الحال، من الكتبة، من المكتبة المعياريّة.)

ملاحظة تصميم: التعبيرات والجمل

تمتلك Lox تعبيراتٍ وجملاً معاً. فبعض اللّغات تُهمل الأخيرة. وبدلاً من ذلك، تعامل التصريحات وبنى التحكّم في التنفيذ كتعبيراتٍ أيضاً. وهذه اللّغات التي "كلّ شيء فيها تعبير" تميل إلى أن تكون ذات نسبٍ وظيفي، وتشمل معظم Lisps و SML و Haskell و Ruby و CoffeeScript.

ولكي تفعل ذلك، فإنّك تحتاج في كلّ بنيةٍ "شبيهة بالجملة" في اللّغة أن تقرّر ما القيمة التي تُقيَّم إليها. وبعض ذلك سهل:

  • يُقيَّم تعبير if إلى نتيجة الفرع الذي اختير. وبالمثل، يُقيَّم switch أو أيّ فرعٍ متعدّد المسارات إلى الحالة المختارة.

  • يُقيَّم تصريحُ متغيّر إلى قيمة المتغيّر.

  • تُقيَّم الكتلة إلى نتيجة آخر تعبير في التسلسل.

وبعضها أغرب قليلاً. فما الذي ينبغي أن تُقيَّم إليه حلقة؟ فحلقة while في CoffeeScript تُقيَّم إلى مصفوفة تحتوي كلّ عنصرٍ كان الجسم يُقيَّم إليه. وقد يكون ذلك مفيداً، أو هدراً للذاكرة إن لم تكن تحتاج إلى المصفوفة.

وعليك أيضاً أن تقرّر كيف تتألف هذه التعبيرات الشبيهة بالجمل مع التعبيرات الأخرى -- يجب أن تدخلها في جدول أسبقية القواعد. فمثلاً تسمح Ruby بما يلي:

puts 1 + if true then 2 else 3 end + 4

هل هذا ما تتوقّعه؟ هل هو ما يتوقّعه مستخدموك؟ وكيف يؤثر هذا في كيفية تصميمك لصياغة "جملك"؟ لاحظ أنّ لدى Ruby end صريحةً تخبرك متى يكتمل تعبير if. فمن دونها، يُحتمل أن يُحلَّل + 4 كجزءٍ من بند else.

وتحويل كلّ جملةٍ إلى تعبيرٍ يفرض عليك الإجابة عن بضعة أسئلة متشعّبة مثل هذه. وفي المقابل، فأنت تلغي بعض التكرار. فـC لديها كتلٌ لتسلسل الجمل، ومعامل الفاصلة لتسلسل التعبيرات. ولديها جملة if ومعامل ?: الشرطيّ أيضاً. فلو كان كلّ شيء في C تعبيراً، لكان بإمكانك توحيد كلٍّ من ذلك.

واللّغات التي تلغي الجمل تميل أيضاً إلى أن تحوي إرجاعات ضمنيّة (implicit returns) -- إذ تُعيد الدالة تلقائياً أيّ قيمةٍ يُقيَّم إ إليها جسمها دون حاجة إلى صيغة return صريحة. وهذا مفيدٌ حقّاً للدوالّ والدوالّ الصغيرة. وفي الواقع، أضافت لغاتٌ كثيرة -- بل لغاتٌ لها جمل -- صياغاتٍ مثل => لتعريف دوالّ يكون جسمها نتيجة تقويم تعبيرٍ واحد.

لكنّ جعل كل الدوالّ تعمل على تلك الطريقة قد يكون غريباً قليلاً. فإن لم تكن حذراً، ستسرّب دالتُك قيمةَ إرجاع حتى إن كنتَ لا تنوي سوى إنتاج أثرٍ جانبي. لكن عملياً، لا يجد مستخدمو هذه اللّغات أنّ ذلك مشكلة.

أمّا في Lox، فقد منحتها جُملاً لأسبابٍ واقعية بحتة. واخترتُ صيغةً شبيهة بـC من أجل المألوفيّة، ومحاولة أخذ صياغة جمل C القائمة وتفسيرها كتعبيرات تصبح غريبةً بسرعة.