صناعة المفسّرات › الوراثة 14 / 35 السابق التالي

الوراثة

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

تيري براتشيت، قبّعة مليئة بالسماء

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

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

الأصناف الأب والأصناف الفرعية

بما أن المفهوم هو «الوراثة»، فمن الطبيعي أن نتوقّع أن يختاروا استعارة متّسقة ويسمّوها أصناف «أم» و«ابن»، لكن ذلك سيكون سهلاً أكثر من اللازم. في زمن بعيد جداً، صاغ C. A. R. Hoare مصطلح «صنف فرعي» (subclass) للإشارة إلى نوع سجلّ (record) يُدقّق نوعاً آخر. واستعارت Simula ذلك المصطلح للإشارة إلى صنف يرث من صنف آخر. ولا أظنّ أن أحداً قلب البادئة اللاتينية ليحصل على «صنف أب» (superclass) للإشارة إلى الطرف الآخر من العلاقة إلا حين ظهرت Smalltalk. ومن C++ تسمع أيضاً «صنف أساسي» (base) و«صنف مشتقّ» (derived). وسألتزم غالباً بمصطلحي «الصنف الأب» و«الصنف الفرعي».

خطوتنا الأولى نحو دعم الوراثة في Lox هي طريقة لتحديد صنف أب عند إعلان صنف. هناك تنوّع كبير في الصياغة المستخدَمة لذلك. تضع C++ وC# رمز : بعد اسم الصنف الفرعي يليه اسم الصنف الأب. وتستخدم Java كلمة extends بدلاً من النقطتين. وتضع Python الصنف (أو الأصناف) الأب داخل أقواس بعد اسم الصنف. أمّا Simula فتضع اسم الصنف الأب قبل الكلمة المفتاحية class.

في هذه المرحلة المتأخرة من المشروع، أفضّل ألّا أضيف كلمة محجوزة أو رمزاً جديداً إلى محلّلنا المعجمي (scanner). ليس لدينا extends ولا حتى :، لذا سنتبع Ruby ونستخدم علامة أصغر من (<).

class Doughnut {
  // General doughnut stuff...
}

class BostonCream < Doughnut {
  // Boston Cream-specific stuff...
}

لإدماج هذا في القواعد اللغوية، نضيف بنداً اختيارياً جديداً في قاعدة classDecl القائمة.

classDecl      → "class" IDENTIFIER ( "<" IDENTIFIER )?
                 "{" function* "}" ;

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

نريد أن نلتقط هذه الصياغة الجديدة في عقدة AST الخاصة بإعلان الصنف.

^code superclass-ast (1 before, 1 after)

قد تُفاجئ لأننا نخزّن اسم الصنف الأب كـExpr.Variable لا كـToken. فالقواعد اللغوية تقصر بند الصنف الأب على مُعرِّف واحد، لكن في الزمن التشغيل يُقيَّم ذلك المُعرِّف كوصول إلى متغيّر. وتغليف الاسم في Expr.Variable في وقت مبكّر داخل المحلل النحوي يمنحنا كائناً يمكن للمحلّل أن يعلّق عليه معلومات الحَلّ.

الشيفرة الجديدة في المحلل النحوي تتبع القواعد اللغوية مباشرةً.

^code parse-superclass (1 before, 1 after)

بعد أن نحلّل (ربما) إعلان صنف أب، نخزّنه في AST.

^code construct-class-ast (2 before, 1 after)

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

^code resolve-superclass (1 before, 2 after)

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

ولأن حتى المبرمجين ذوي النوايا الحسنة يكتبون أحياناً شيفرة غريبة، هناك حالة حدّية سخيفة علينا الانشغال بها—we're here. تأمّل ما يلي:

class Oops < Oops {}

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

^code inherit-self (2 before, 1 after)

بافتراض أن الشيفرة تُحلّ دون خطأ، تنتقل AST إلى المفسّر.

^code interpret-superclass (1 before, 1 after)

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

var NotAClass = "I am totally not a class";

class Subclass < NotAClass {} // ?!

بافتراض أن ذلك التحقّق ينجح، نُكمل. فتنفيذ إعلان صنف يحوّل التمثيل النحوي للصنف — عقدة AST الخاصة به — إلى تمثيله الزمني، وهو كائن LoxClass. ونحتاج إلى تمرير الصنف الأب إلى هناك أيضاً. فنمرّر الصنف الأب إلى المُنشئ.

^code interpreter-construct-class (3 before, 1 after)

يخزّنه المُنشئ في حقل.

^code lox-class-constructor (1 after)

ونعلنه هنا:

^code lox-class-superclass-field (1 before, 1 after)

بهذا، يمكننا تعريف أصناف تكون أصنافاً فرعية لأصناف أخرى. والآن، ما الذي يفعله امتلاك صنف أب فعلياً؟

وراثة الدوال

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

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

وهذا يتّسق مع أحد أهداف الوراثة — أن تمنح المستخدمين وسيلة لإعادة استخدام الشيفرة عبر الأصناف. وتنفيذ ذلك في مفسّرنا مذهل في بساطته.

^code find-method-recurse-superclass (3 before, 1 after)

هذا حرفياً كل ما في الأمر. عندما نبحث عن دالة على نسخة، إن لم نجدها على صنف النسخة، فإننا نتعاود صعوداً عبر سلسلة الصنفين الآباء ونبحث هناك. جرّبه:

class Doughnut {
  cook() {
    print "Fry until golden brown.";
  }
}

class BostonCream < Doughnut {}

BostonCream().cook();

ها هي ذي، نصف ميزات الوراثة اكتملت بثلاثة أسطر فقط من شيفرة Java.

استدعاء دوال الصنف الأب

في findMethod() نبحث عن دالة على الصنف الحالي قبل الصعود في سلسلة الصنفين الآباء. إذا وُجدت دالة بالاسم نفسه في الصنف الفرعي وفي الصنف الأب، فإن دالة الصنف الفرعي لها الأولوية أو تَتجاوز (overrides) دالة الصنف الأب. شيء أشبه بكيف تحجب المتغيّرات في النطاقات الداخلية نظائرها الخارجية.

هذا رائع إن أراد الصنف الفرعي أن يستبدل سلوك الصنف الأب بالكامل. لكن عملياً، غالباً ما يريد الصنف الفرعي أن يُدقّق سلوك الصنف الأب. يريد أن يقوم بقدر قليل من العمل الخاص بالصنف الفرعي، لكنه يريد أيضاً تنفيذ سلوك الصنف الأب الأصلي.

لكن، بما أن الصنف الفرعي قد تجاوز الدالة، فلا سبيل للإشارة إلى الأصلية. إن حاولت دالة الصنف الفرعي استدعائها بالاسم، فستصطدم بتجاوزها هي نفسها بشكل متكرر. نحتاج إلى وسيلة لقول «استدعِ هذه الدالة، لكن ابحث عنها مباشرةً على صنف أبي وتجاوز تجاوازي». تستخدم Java كلمة super لهذا، وسنستخدم الصياغة نفسها في Lox. إليك مثالاً:

class Doughnut {
  cook() {
    print "Fry until golden brown.";
  }
}

class BostonCream < Doughnut {
  cook() {
    super.cook();
    print "Pipe full of custard and coat with chocolate.";
  }
}

BostonCream().cook();

إن شغّلتَ هذا، فينبغي أن يطبع:

Fry until golden brown.
Pipe full of custard and coat with chocolate.

لدينا شكل تعبير جديد. فالكلمة المفتاحية super يليها نقطة ومُعرِّف تبحث عن دالة بهذا الاسم. وبخلاف الاستدعاءات على this، يبدأ البحث من الصنف الأب.

الصياغة

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

print super; // Syntax error.

لذلك فإن البند الجديد الذي نضيفه إلى قاعدة primary في قواعدنا اللغوية يشمل الوصول إلى الخاصية أيضاً.

primary        → "true" | "false" | "nil" | "this"
               | NUMBER | STRING | IDENTIFIER | "(" expression ")"
               | "super" "." IDENTIFIER ;

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

var method = super.cook;
method();

إذن تعبير super نفسه يحتوي فقط على رمز الكلمة المفتاحية super واسم الدالة المُراد البحث عنها. وعقدة شجرة التركيب المقابلة هي إذن:

^code super-expr (1 before, 1 after)

متبعةً للقواعد اللغوية، تذهب شيفرة التحليل الجديدة داخل دالة primary() القائمة لديّ.

^code parse-super (2 before, 2 after)

الكلمة المفتاحية super في البداية تخبرنا أننا صادفنا تعبير super. وبعد ذلك نستهلك النقطة المتوقّعة واسم الدالة.

الدلالات

Earlier، قلتُ إن تعبير super يبدأ البحث عن الدالة من «الصنف الأب»، لكن أيّ صنف أب؟ الإجابة الساذجة هي الصنف الأب لـthis، أي الكائن الذي استُدعيت عليه الدالة المحيطة. من باب المصادفة، يُنتج ذلك السلوك الصحيح في حالات كثيرة، لكنه ليس صحيحاً في الواقع. تأمّل ما يلي:

class A {
  method() {
    print "A method";
  }
}

class B < A {
  method() {
    print "B method";
  }

  test() {
    super.method();
  }
}

class C < B {}

C().test();

ترجم هذا البرنامج إلى Java أو C# أو C++ وسيطبع "A method"، وهو ما نريد أن تفعله Lox أيضاً. عند تشغيل هذا البرنامج، يكون this داخل جسم test() نسخة من C. والصنف الأب لـ C هو B، لكن ذلك ليس المكان الذي ينبغي أن يبدأ منه البحث. لو بدأ من هناك لاستُدعي method() الخاصة بـ B.

بدلاً من ذلك، ينبغي أن يبدأ البحث على الصنف أب للصنف الذي يحتوي على تعبير super. وفي حالتنا هنا، بما أن test() معرَّفة داخل B، فإن تعبير super داخلها ينبغي أن يبدأ البحث على صنف B’ الأب -- A.

سلسلة الاستدعاءات التي تتدفّق عبر الأصناف.

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

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

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

هناك فرق مهم وهو أننا ربطنا this عند الوصول إلى الدالة. فالدالة نفسها يمكن استدعاؤها على نسخ مختلفة، وكل واحدة تحتاج إلى this الخاصة بها. أما مع تعبيرات super، فإن الصنف الأب خاصية ثابتة لإعلان الصنف نفسه. فكلما قيّمتَ تعبير super ما، يكون الصنف الأب نفسه دائماً.

وهذا يعني أننا نستطيع إنشاء بيئة الصنف الأب مرة واحدة، حين يُنفَّذ تعريف الصنف. وقبل أن نعرّف الدوال مباشرةً، ننشئ بيئة جديدة نربط بها صنف الصنف الأب بالاسم super.

بيئة الصنف الأب.

حين ننشئ التمثيل الزمني LoxFunction لكل دالة، تكون تلك هي البيئة التي سيلتقطها في إغلاقها. لاحقاً، حين تُستدعى دالة ويُربط this، تصبح بيئة الصنف الأب هي الأب لبيئة تلك الدالة، هكذا:

سلسلة البيئات بما في ذلك بيئة الصنف الأب.

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

^code begin-super-scope (2 before, 2 after)

إذا كان إعلان الصنف يملك صنفاً أبا، فإننا ننشئ نطاقاً جديداً يحيط بكل دواله. وفي ذلك النطاق، نعرّف الاسم "super". وبمجرد أن ننتهي من حلّ دوال الصنف، نتخلّص من ذلك النطاق.

^code end-super-scope (2 before, 1 after)

إنها تحسين بسيط، لكننا ننشئ بيئة الصنف الأب فقط إذا كان الصنف يملك فعلاً صنفاً أباً. فلا فائدة من إنشائها حين لا يوجد صنف أب، إذ لن يكون هناك صنف أب لتخزينه فيها.

مع وجود "super" معرَّفاً في سلسلة نطاقات، نتمكّن من حلّ تعبير super نفسه.

^code resolve-super-expr

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

هذه الشيفرة مُنشودة في المفسّر أيضاً. فحين نقيّم تعريف صنف فرعي، ننشئ بيئة جديدة.

^code begin-superclass-environment (6 before, 2 after)

داخل تلك البيئة، نخزّن مرجعاً إلى الصنف الأب — كائن LoxClass الفعلي الخاص بالصنف الأب الذي أصبح لدينا الآن بما أننا في الزمن التشغيلي. ثم ننشئ كائنات LoxFunction لكل دالة. وستلتقط تلك البيئة الحالية — البيئة التي ربطنا فيها "super" للتوّ — كإغلاق لها، فتمسك بالصنف الأب كما نحتاج. وبمجرد أن ينتهي ذلك، نُخرج البيئة من المكدّس.

^code end-superclass-environment (2 before, 2 after)

نحن مستعدّون لتفسير تعبيرات super نفسها. هناك بعض القطع المتحركة، لذا سنبني هذه الدالة على مراحل.

^code interpreter-visit-super

أولاً، العمل الذي كنّا نفضي إليه. نبحث عن الصنف أب للصنف المحيط بالبحث عن "super" في البيئة الصحيحة.

حين نصل إلى دالة، نحتاج أيضاً إلى ربط this بالكائن الذي تُنال منه الدالة. في تعبير مثل doughnut.cook، الكائن هو أيًّا ما نحصل عليه من تقييم doughnut. أما في تعبير super مثل super.cook، فالكائن الحالي هو ضمناً نفسه الكائن الحالي الذي نستخدمه. بمعنى آخر، this. وبرغم أننا نبحث عن الدالة على الصنف الأب، فالحقل النسخة ما زال this.

للأسف، داخل تعبير super لا نملك عقدة مناسبة يستطيع المحلّل أن يعلّق عليها عدد القفزات إلى this. ولحسن الحظ، نحن نتحكّم في تخطيط سلاسل البيئات. فالبيئة التي يُربط فيها "this" هي دائماً في الداخل مباشرةً من البيئة التي نخزّن فيها "super".

^code super-find-this (2 before, 1 after)

إن طرح المسافة بواحد يجد "this" في تلك البيئة الداخلية. وأعترف أن هذا ليس أجمل شيفرة، لكنها تعمل.

الآن نحن مستعدّون للبحث عن الدالة وربطها، بدءاً من الصنف الأب.

^code super-find-method (2 before, 1 after)

هذا أشبه تماماً بشيفرة البحث عن دالة في تعبير get، باستثناء أننا نستدعي findMethod() على الصنف الأب بدلاً من صنف الكائن الحالي.

هذا كل شيء تقريباً. إلا أنه، بالطبع، من الممكن أن نفشل في العثور على الدالة. لذلك نتحقّق من ذلك أيضاً.

^code super-no-method (2 before, 2 after)

ها قد يحصل عليها! خذ مثال BostonCream السابق وجرّبه. وبافتراض أننا وأنت فعلنا كل شيء على ما يرام، فينبغي أن يقليها أولاً ثم يحشوها بالكريمة.

الاستخدامات غير الصالحة لـ super

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

class Eclair {
  cook() {
    super.cook();
    print "Pipe full of crème pâtissière.";
  }
}

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

بل هناك حتى استخدامات مكسورة أبسط لـ super:

super.notEvenInAClass();

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

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

أولاً، نضيف حالة جديدة إلى التعداد الذي نستخدمه لتتبّع نوع الصنف المحيط بالشيفرة الحالية التي يجري زيارتها.

^code class-type-subclass (1 before, 1 after)

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

^code set-current-subclass (1 before, 1 after)

ثم حين نحلّ تعبير super، نتحقّق من أننا حالياً داخل نطاق يسمح بذلك.

^code invalid-super (1 before, 1 after)

إن لم يكن كذلك — يا لطيم! — فقد أخطأ المستخدم.

الخاتمة

نجحنا! تلك القطعة الأخيرة من معالجة الأخطاء هي آخر جزء من الشيفرة اللازم لإكمال تنفيذنا للغة Lox بلغة Java. وهذا إنجاز حقيقي ينبغي أن تفخر به. في Chapters الاثني عشر الماضية وألف سطر أو نحوها من الشيفرة، تعلّمنا ونفّذنا...

أنجزنا كل ذلك من الصفر، بلا اعتماديات خارجية ولا أدوات سحرية. أنت وأنا، ومحرّرا النصّ لدينا، وبضعة أصناف تجميع في مكتبة Java القياسية، ووقت تشغيل JVM.

بهذا تنتهي الجزء الثاني، لكن الكتاب لم ينتهِ. خُذ استراحة. ربما اكتب بضعة برامج Lox ممتعة وشغّلها في مفسّرك. (قد ترغب في إضافة المزيد من الدوال الأصلية (native functions) لأشياء مثل قراءة مدخلات المستخدم.) حين تستعيد نشاطك وتصبح مستعدّاً، سننطلق في [مغامرتنا التالية][].

التحديات

  1. تدعم Lox الوراثة المفردة فقط -- قد يملك الصنف صنفاً أبا واحداً، وذلك هو الطريق الوحيد لإعادة استخدام الدوال عبر الأصناف. وقد استكشفت لغات أخرى طرقاً متنوّعة لإعادة استخدام القدرات ومشاركتها بين الأصناف بمرونة أكبر: الخلط (mixins)، والصفات (traits)، والوراثة المتعدّدة، والوراثة الافتراضية، ودوال التوسيع، إلخ.

    إن أضفتَ ميزة من هذا النوع إلى Lox، أيّها ستختار ولماذا؟ إن شعرت بالشجاعة (وينبغي أن تكون كذلك في هذه المرحلة)، فامضِ وأضفها.

  2. في Lox، كما في معظم لغات البرمجة الموجّهة للكائنات الأخرى، نبدأ عند البحث عن دالة من أسفل تسلسل الأصناف ونصعد تدريجياً -- فدالة الصنف الفرعي مقدَّمة على دالة الصنف الأب. وللوصول إلى دالة الصنف الأب من داخل دالة تتجاوزها، تستخدم super.

    وتتّخذ لغة BETA تتّخذ نهجاً معاكساً. فحين تستدعي دالة، تبدأ من أعلى تسلسل الأصناف وتعمل نازلاً. فدالة الصنف الأب تغلب على دالة الصنف الفرعي. وللوصول إلى دالة الصنف الفرعي، يمكن لدالة الصنف الأب أن تستدعي inner، وهو شيء يشبه عكس super. فهو يتسلسل إلى الدالة التالية في التسلسل النازل.

    تتحكّم دالة الصنف الأب في متى وأين يُسمح للصنف الفرعي بأن يُدقّق سلوكه. فإذا لم تستدعِ دالة الصنف الأب inner إطلاقاً، فلا يملك الصنف الفرعي أي وسيلة لتجاوز سلوك الصنف الأب أو تعديله.

    أخرج سلوك التجاوز وsuper الحالي في Lox واستبدله بدلالات BETA. باختصار:

    • عند استدعاء دالة على صنف، فضّل الدالة الأعلى في سلسلة وراثة الصنف.

    • داخل جسم دالة، يبحث استدعاء inner عن دالة بالاسم نفسه في أقرب صنف فرعي على امتداد سلسلة الوراثة بين الصنف الذي يحتوي inner وصنف this. إن لم تكن هناك دالة مطابقة، فلا يفعل استدعاء inner شيئاً.

    مثلاً:

    class Doughnut {
      cook() {
        print "Fry until golden brown.";
        inner();
        print "Place in a nice box.";
      }
    }
    
    class BostonCream < Doughnut {
      cook() {
        print "Pipe full of custard and coat with chocolate.";
      }
    }
    
    BostonCream().cook();
    

    ينبغي أن يطبع هذا:

    Fry until golden brown.
    Pipe full of custard and coat with chocolate.
    Place in a nice box.
    
  3. في الفصل الذي عرّفتُ فيه Lox، تحدّيتك أن يأتي ببعض الميزات التي ترى أن اللغة تفتقدها. والآن بما أنك تعرف كيف تبني مفسّراً، نفّذ إحدى تلك الميزات.