بنية وتفسير برامج الحاسوب › التجريد فوق اللغوي 5 / 7 السابق التالي

التجريد فوق اللغوي

… إنّ السحر في الكلمات: أبرا كدابرا، وافتح يا سمسم، وغيرها — لكنّ الكلمات السحريّة في حكايةٍ لا تكون سحريّةً في الحكاية التالية. والسحر الحقيقيّ هو أن تفهم أيّ الكلمات تنفع، ومتى، ولأيّ شيء؛ والخدعة هي أن تتعلّم الخدعة. … وهذه الكلمات مصنوعةٌ من حروف أبجديتنا: عشراتٌ قليلةٌ من التعرّجات التي نستطيع رسمها بالقلم. هذا هو المفتاح! والكنز أيضًا، إن استطعنا الحصول عليه بأيدينا! إنّه كأنّ — كأنّ مفتاح الكنز هو الكنز! — جون بارث، كيميرا

لقد رأينا في دراستنا لتصميم البرامج أنّ المبرمجين الخبيرين يضبطون تعقيد تصاميمهم بالتقنيّات العامّة ذاتها التي يستخدمها مصمّمو جميع النظم المعقّدة. فهم يجمعون العناصر الأوّليّة لتكوين كائناتٍ مركّبة، ويجرّدون الكائنات المركّبة لتكوين وحدات بناءٍ على مستوى أعلى، ويحفظون وحدانيّة تصاميمهم بتبنّي نظراتٍ واسعة النطاق مناسبةٍ لبنية النظام. ولتوضيح هذه التقنيّات، استخدمنا Lisp لغةً لوصف العمليّات ولبناء كائنات البيانات الحسابيّة والعمليّات لنمذجة الظواهر المعقّدة في العالم الواقعيّ. غير أنّنا، في مواجهتنا مسائل متزايدة التعقيد، سنجد أنّ Lisp — أو أيّ لغة برمجةٍ ثابتةٍ في الحقيقة — غير كافيةٍ لحاجاتنا. فيجب أن نلجأ باستمرارٍ إلى لغاتٍ جديدةٍ كي نُعبّر عن أفكارنا بفعّيّةٍ أكبر. وإرساء لغاتٍ جديدةٍ استراتيجيّةٌ قويّةٌ لضبط التعقيد في التصميم الهندسيّ؛ فيمكننا غالبًا أن نعزّز قدرتنا على التعامل مع مسألةٍ معقّدةٍ بتبنّي لغةٍ جديدةٍ تمكّننا من وصف المسألة (ومن ثمّ التفكير فيها) بطريقةٍ مختلفة، باستخدام أوّليّاتٍ ووسائل تركيبٍ ووسائل تجريدٍ تصلح خصوصًا للمسألة المطروحة.[1]

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

التجريد فوق اللغوي (metalinguistic abstraction) — إرساء لغاتٍ جديدةٍ — يلعب دورًا مهمًّا في جميع فروع التصميم الهندسيّ. وهو مهمٌّ بشكلٍ خاصٍّ في البرمجة الحاسوبيّة، لأنّه في البرمجة لا نستطيع صياغة لغاتٍ جديدةٍ فحسب، بل نستطيع أيضًا تنفيذ هذه اللغات ببناء مُقيّمات. والمُقيِّم (evaluator) (أو المفسّر (interpreter)) للغة برمجةٍ ما هو إجراءٌ يؤدّي، حين يُطبَّق على تعبيرٍ من اللغة، الأفعال المطلوبة لتقييم ذلك التعبير.

ولا مبالغة في اعتبار هذه أهمّ فكرةٍ في البرمجة:

إنّ المُقيِّم، الذي يحدّد معنى التعابير في لغة برمجةٍ ما، هو بمثابة برنامجٍ آخر.

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

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

نبدأ الآن جولةً في التقنيّة التي تُرسى بها اللغات من حيث لغاتٍ أخرى. وسنستخدم في هذا الفصل Lisp أساسًا، منفّذين المُقيّمات كإجراءات Lisp. وLisp تصلح خصوصًا لهذه المهمّة، بسبب قدرتها على تمثيل التعابير الرمزيّة والتلاعب بها. وسنخطو الخطوة الأولى في فهم كيف تُنفَّذ اللغات ببناء مُقيّمٍ لـLisp نفسها. واللغة التي ينفّذها مُقيّمنا ستكون مجموعةً جزئيّةً من لهجة Scheme من Lisp التي نستخدمها في هذا الكتاب. ورغم أنّ المُقيّم الموصوف في هذا الفصل مكتوبٌ للهجة Lisp معيّنة، فإنّه يحتوي البنية الأساسيّة لمُقيّم أيّ لغةٍ موجّهةٍ بالتعبيرات ومصمَّمةٍ لكتابة البرامج لآلةٍ تسلسليّة. (في الحقيقة، تحتوي معظم معالجات اللغات في أعماقها على مُقيّم Lisp صغير.) وقد بُسِّط المُقيّم لأغراض التوضيح والمناقشة، وحُذفت بعض الخصائص التي يكون من المهمّ تضمينها في نظام Lisp ذي جودةٍ إنتاجيّة. ومع ذلك، فإنّ هذا المُقيّم البسيط كافٍ لتنفيذ معظم البرامج في هذا الكتاب.[2]

إحدى المزايا المهمّة لجعل المُقيّم متاحًا بوصفه برنامج Lisp هي أنّنا نستطيع تنفيذ قواعد تقييمٍ بديلةٍ بوصفها على أنّها تعديلاتٌ على برنامج المُقيّم. وأحد المواضع التي نستطيع فيها استخدام هذه القوّة على نحوٍ مفيدٍ هو كسب تحكّمٍ إضافيٍّ في الطرق التي تجسّد بها النماذج الحسابيّة مفهوم الزمن، الذي كان محوريًّا للنقاش في الفصل 3. فهناك، خفّفنا من بعض تعقيدات الحالة والإحلال باستخدام المجاري لفصل تمثيل الزمن في العالم عن الزمن في الحاسوب. وسنغيّر في 4.2 اللغة الأساسيّة لتوفير نهجٍ أكثر أناقة، وذلك بتعديل المُقيّم ليوفّر التقييم بالترتيب الاعتياديّ (normal-order evaluation).

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

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

4.1 المُقيّم التعاكسيّ

سيكون مُقيّمنا لـLisp منفَّذًا بوصفه برنامج Lisp. وقد يبدو الأمر دورانيًّا — التفكير في تقييم برامج Lisp باستخدام مُقيّمٍ هو نفسه منفَّذٌ بـLisp. غير أنّ التقييم عمليّةٌ، فلذا فإنّ وصف عمليّة التقييم باستخدام Lisp أمرٌ ملائم، وهي التي هي أداتنا لوصف العمليّات في النهاية.[3] ويُقال عن المُقيّم المكتوب باللغة ذاتها التي يقيّمها إنّه تعاكسيّ (metacircular).

والمُقيّم التعاكسيّ هو في جوهره صياغةٌ بـScheme لنموذج البيئة للتقييم الموصوف في 3.2. وأذكّر أنّ النموذج له جزءان أساسيّان:

  1. ولتقييم تركيبةٍ (تعبيرٌ مركّبٌ غيرُ صيغةٍ خاصّة)، قيّم التعابير الجزئيّة ثمّ طبّق قيمة التعبير الجزئيّ للمشغّل على قيم التعابير الجزئيّة للعوامل.
  2. ولتطبيق إجراءٍ مركّبٍ على مجموعةٍ من المعطيات، قيّم جسم الإجراء في بيئةٍ جديدة. ولتكوين هذه البيئة، وسّع جزء البيئة من كائن الإجراء بإطارٍ تُربط فيه الوسائط الشكليّة للإجراء بالمعطيات التي يُطبَّق عليها الإجراء.

يصف هذان العصران جوهر عمليّة التقييم، دورةً أساسيّةً تُختزل فيها التعابير المطلوب تقييمها في بيئاتٍ إلى إجراءاتٍ تُطبَّق على معطيات، تُختزل بدورها إلى تعابير جديدةٍ تُقيَّم في بيئاتٍ جديدة، وهكذا، حتّى ننزل إلى رموزَ تُبحث قيمُها في البيئة، وإلى إجراءاتٍ أوّليّةٍ تُطبَّق مباشرةً (انظر الشكل 4.1).[4] وستتجسّد دورة التقييم هذه بتفاعل الإجراءين الحرجيّين في المُقيّم، وهما eval وapply، الموصوفين في 4.1.1 (انظر الشكل 4.1).

الشكل 4.1: تكشف دورة eval-apply عن جوهر لغة الحاسوب.

سيعتمد تنفيذ المُقيّم على إجراءاتٍ تُعرّف الصياغة (syntax) للتعبيرات المطلوب تقييمها. وسنستخدم تجريد البيانات لجعل المُقيّم مستقلًّا عن تمثيل اللغة. فإنّا، مثلًا، بدلًا من الالتزام بخيارٍ يقضي بأنّ الإحلال يُُمثَّل بقائمةٍ تبدأ بالرمز set! نستخدم مُسَيِّمًا مجرّدًا assignment? لاختبار الإحلال، ونستخدم محدِّدين مجرّدين assignment-variable وassignment-value للنفاذ إلى أجزاء الإحلال. وسيُوصف تنفيذ التعابير بالتفصيل في 4.1.2. وهناك أيضًا عمليّات، موصوفةٌ في 4.1.3، تُحدّد تمثيل الإجراءات والبيئات. فإنّ make-procedure مثلًا يبني الإجراءات المركّبة، وlookup-variable-value ينفذ إلى قيم المتغيّرات، وapply-primitive-procedure يطبّق إجراءً أوّليًّا على قائمةٍ معطاةٍ من المعطيات.

4.1.1 لبّ المُقيّم

يمكن وصف عمليّة التقييم على أنّها تفاعلٌ بين إجراءين: eval وapply.

التقييم (eval)

يأخذ eval تعبيرًا وبيئةً كمعطيين. وهو يُصنّف التعبير ويوجّه تقييمه. وeval مبنيٌّ على هيئة تحليل حالاتٍ للنوع الصياغيّ للتعبير المطلوب تقييمه. ولكي نحفظ الإجراء عامًّا، نُعبّر عن تحديد نوع التعبير تجريدًا، دون التزامٍ بأيّ تمثيلٍ معيّنٍ لأنواع التعبيرات المختلفة. ولكلّ نوعٍ من التعبيرات مُسَيِّمٌ يختبره ووسيلةٌ مجرّدةٌ لتحديد أجزائه. وهذه الصياغة المجرّدة (abstract syntax) تجعل من السهل رؤية كيف نستطيع تغيير صياغة اللغة باستخدام المُقيّم نفسه، لكن مع مجموعةٍ مختلفةٍ من إجراءات الصياغة.

التعابير الأوّليّة

  • فبالنسبة للتعابير ذاتيّة التقييم، كالأعداد، يُعيد eval التعبير نفسه.
  • ويجب على eval البحث عن المتغيّرات في البيئة لإيجاد قيمها.

الصيغ الخاصّة

  • فبالنسبة للتعابير المُقتبسة، يُعيد eval التعبير الذي كان مُقتبَسًا.
  • والإحلال إلى متغيّر (أو تعريفه) يجب أن يستدعي eval تعاوديًّا لحساب القيمة الجديدة التي تُربط بالمتغيّر. ويجب تعديل البيئة لتغيير (أو إنشاء) ربط المتغيّر.
  • وتعبير if يتطلّب معالجةً خاصّةً لأجزائه، بحيث يُقيَّم الناتج إذا كان المُسَيِّم صحيحًا، ويُقيَّم البديل خلافًا لذلك.
  • وتعبير lambda يجب أن يُحوَّل إلى إجراءٍ قابلٍ للتطبيق بحزم الوسائط والجسم اللذين يُحدّدهما تعبير lambda مع بيئة التقييم معًا.
  • وتعبير begin يتطلّب تقييم تتابع تعابيره بالترتيب الذي تظهر به.
  • وتحليل الحالات (cond) يُحوَّل إلى عشٍّ من تعابير if ثمّ يُقيَّم.

التركيبات

  • وبالنسبة لتطبيق إجراءٍ، يجب على eval تقييم جزء المشغّل وعوامل التركيبة تعاوديًّا. والإجراء والمعطيات الناتجان يُمرَّران إلى apply، الذي يتعامل مع تطبيق الإجراء الفعليّ.

وها هو تعريف eval:

(define (eval exp env)
  (cond ((self-evaluating? exp) 
         exp)
        ((variable? exp) 
         (lookup-variable-value exp env))
        ((quoted? exp) 
         (text-of-quotation exp))
        ((assignment? exp) 
         (eval-assignment exp env))
        ((definition? exp) 
         (eval-definition exp env))
        ((if? exp) 
         (eval-if exp env))
        ((lambda? exp)
         (make-procedure 
          (lambda-parameters exp)
          (lambda-body exp)
          env))
        ((begin? exp)
         (eval-sequence 
          (begin-actions exp) 
          env))
        ((cond? exp) 
         (eval (cond->if exp) env))
        ((application? exp)
         (apply (eval (operator exp) env)
                (list-of-values 
                 (operands exp) 
                 env)))
        (else
         (error "Unknown expression 
                 type: EVAL" exp))))

وللتّوضيح، نُفّذ eval على هيئة تحليل حالاتٍ باستخدام cond. وعيب هذا أنّ إجراءنا يتعامل مع أنواعٍ قليلةٍ فقط من التعابير القابلة للتمييز، ولا يمكن تعريف أنواعٍ جديدةٍ دون تعديل تعريف eval. وفي معظم تنفيذات Lisp، يُجرى التوجيه بحسب نوع التعبير بأسلوبٍ موجَّهٍ بالبيانات. وهذا يسمح للمستخدم بإضافة أنواعٍ جديدةٍ من التعابير يستطيع eval تمييزها، دون تعديل تعريف eval نفسه. (انظر التمرين 4.3.)

التطبيق (apply)

يأخذ apply معطيين، إجراءً وقائمةً من المعطيات التي يجب أن يُطبَّق عليها الإجراء. وapply يُصنّف الإجراءات إلى نوعين: فهو يستدعي apply-primitive-procedure لتطبيق الأوّليّات؛ ويطبّق الإجراءات المركّبة بتقييم التعابير التي تكوّن جسم الإجراء تِباعًا. وتُبنى البيئة لتقييم جسم إجراءٍ مركّبٍ بتوسيع البيئة الأساسيّة التي يحملها الإجراء لتضمّ إطارًا يربط وسائط الإجراء بالمعطيات التي سيُطبَّق عليها الإجراء. وها هو تعريف apply:

(define (apply procedure arguments)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedure 
          procedure 
          arguments))
        ((compound-procedure? procedure)
         (eval-sequence
           (procedure-body procedure)
           (extend-environment
             (procedure-parameters 
              procedure)
             arguments
             (procedure-environment 
              procedure))))
        (else
         (error "Unknown procedure 
                 type: APPLY" 
                procedure))))

معطيات الإجراء

حين يعالج eval تطبيق إجراءٍ، فإنّه يستخدم list-of-values لإنتاج قائمة المعطيات التي سيُطبَّق عليها الإجراء. وlist-of-values تأخذ عوامل التركيبة معطىً واحدًا. وهي تُقيّم كلّ عاملٍ وتُعيد قائمة القيم المقابلة:[5]

(define (list-of-values exps env)
  (if (no-operands? exps)
      '()
      (cons (eval (first-operand exps) env)
            (list-of-values 
             (rest-operands exps) 
             env))))

التعابير الشرطيّة

يُقيّم eval-if جزء المُسَيِّم من تعبير if في البيئة المعطاة. فإن كانت النتيجة صحيحة، قيّم eval-if الناتج، ويُقيّم البديل خلافًا ذلك:

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

يُبرز استخدام true? في eval-if مسألة الصلة بين لغةٍ منفَّذةٍ ولغة تنفيذٍ. فـif-predicate يُقيَّم في اللغة التي يجري تنفيذها، لذا يُنتج قيمةً في تلك اللغة. ومُسَيِّم المفسّر true? يترجم تلك القيمة إلى قيمةٍ يمكن اختبارها بـif في لغة التنفيذ: فإنّ تمثيل الصّحاح في المُقيّم التعاكسيّ قد لا يكون هو نفسه تمثيله في Scheme الأساسيّة.[6]

التتابعات

إنّ eval-sequence يُستخدم من apply لتقييم تتابع التعابير في جسم الإجراء، ومن eval لتقييم تتابع التعابير في تعبير begin. وهو يأخذ تتابع تعابيرٍ وبيئةً كمعطيين، ويُقيّم التعابير بالترتيب الذي تقع به. والقيمة المُعادة هي قيمة التعبير الأخير.

(define (eval-sequence exps env)
  (cond ((last-exp? exps) 
         (eval (first-exp exps) env))
        (else 
         (eval (first-exp exps) env)
         (eval-sequence (rest-exps exps) 
                        env))))

الإحلالات والتعريفات

يتعامل الإجراء التالي مع الإحلالات إلى المتغيّرات. فهو يستدعي eval لإيجاد القيمة التي سيُحَلّ محلًّا، وينقل المتغيّر والقيمة الناتجة إلى set-variable-value! ليُثبَّتا في البيئة المعيَّنة.

(define (eval-assignment exp env)
  (set-variable-value! 
   (assignment-variable exp)
   (eval (assignment-value exp) env)
   env)
  'ok)

وتُعالَج تعريفات المتغيّرات بالطريقة ذاتها.[7]

(define (eval-definition exp env)
  (define-variable! 
    (definition-variable exp)
    (eval (definition-value exp) env)
    env)
  'ok)

اخترنا هنا إعادة الرمز ok قيمةً للإحلال أو للتعريف.[8]

التمرين 4.1: لاحِظ أنّنا لا نستطيع معرفة ما إذا كان المُقيّم التعاكسيّ يقيّم العوامل من اليسار إلى اليمين أم من اليمين إلى اليسار. فإنّ ترتيب تقييمه موروثٌ من Lisp الأساسيّة: فإن قُيّمت المعطيات الممرَّرة إلى cons في list-of-values من اليسار إلى اليمين، قيّم list-of-values العوامل من اليسار إلى اليمين؛ وإن قُيّمت معطيات cons من اليمين إلى اليسار، قيّم list-of-values العوامل من اليمين إلى اليسار. اكتب نسخةً من list-of-values تُقيّم العوامل من اليسار إلى اليمين بصرف النظر عن ترتيب التقييم في Lisp الأساسيّة. واكتب أيضًا نسخةً من list-of-values تُقيّم العوامل من اليمين إلى اليسار.

4.1.2 تمثيل التعابير

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

وها هو توصيف صياغة لغتنا:

والعناصر ذاتيّة التقييم الوحيدة هي الأعداد والسلاسل المحرفيّة:

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))

والمتغيّرات يمثّلها الرموز:

(define (variable? exp) (symbol? exp))

والتعبيرات المُقتبسة لها الصيغة (quote ⟨text-of-quotation⟩):[9]

(define (quoted? exp)
  (tagged-list? exp 'quote))

(define (text-of-quotation exp)
  (cadr exp))

يُعرَّف Quoted? من حيث الإجراء tagged-list?، الذي يحدّد القوائم التي تبدأ برمزٍ معيَّن:

(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

تأخذ الإحلالات الصيغة (set! ⟨var⟩ ⟨value⟩):

(define (assignment? exp)
  (tagged-list? exp 'set!))

(define (assignment-variable exp) 
  (cadr exp))

(define (assignment-value exp) (caddr exp))

وللتعريفات الصيغة

(define ⟨var⟩ ⟨value⟩)

أو الصيغة

(define (⟨var⟩ ⟨param₁⟩ … ⟨paramₙ⟩)
  ⟨body⟩)

أمّا الصيغة الأخيرة (تعريف الإجراء القياسيّ) فهي سكّرٌ نحويٌّ (syntactic sugar) لِـ

(define ⟨var⟩
  (lambda (⟨param₁⟩ … ⟨paramₙ⟩)
    ⟨body⟩))

وأمّا إجراءات الصياغة المقابلة فهي الآتية:

(define (definition? exp)
  (tagged-list? exp 'define))

(define (definition-variable exp)
  (if (symbol? (cadr exp))
      (cadr exp)
      (caadr exp)))

(define (definition-value exp)
  (if (symbol? (cadr exp))
      (caddr exp)
      (make-lambda 
       (cdadr exp)   ; formal parameters
       (cddr exp)))) ; body

وتعبيرات lambda قوائمٌ تبدأ بالرمز lambda:

(define (lambda? exp) 
  (tagged-list? exp 'lambda))
(define (lambda-parameters exp) (cadr exp))
(define (lambda-body exp) (cddr exp))

ونُوفّر أيضًا بانيًا لتعبيرات lambda، يُستخدم من definition-value الوارد أعلاه:

(define (make-lambda parameters body)
  (cons 'lambda (cons parameters body)))

وتبدأ التعابير الشرطيّة بـif، ولها مُسَيِّمٌ ونتيجةٌ وبديلٌ (اختياريّ). فإن لم يكن للتعبير جزءٌ بديلٌ، فإنّنا نُوفّر false بديلًا.[10]

(define (if? exp) (tagged-list? exp 'if))
(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))

ونُوفّر أيضًا بانيًا لتعبيرات if، ليُستخدم من cond->if في تحويل تعابير cond إلى تعابير if:

(define (make-if predicate 
                 consequent 
                 alternative)
  (list 'if 
        predicate 
        consequent 
        alternative))

ويحزم begin تتابع التعابير في تعبيرٍ واحد. ونحن نُدرج عمليّات صياغةٍ على تعابير begin لاستخراج التتابع الفعليّ من تعبير begin، فضلًا عن محدِّداتٍ تُعيد أوّل تعبيرٍ في التتابع وبقيّة تعابيره.[11]

(define (begin? exp) 
  (tagged-list? exp 'begin))
(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

ونُدرج أيضًا بانيًا sequence->exp (لاستخدامه من cond->if) يحوّل تتابعًا إلى تعبيرٍ واحد، مستخدمًا begin عند الضرورة:

(define (sequence->exp seq)
  (cond ((null? seq) seq)
        ((last-exp? seq) (first-exp seq))
        (else (make-begin seq))))

(define (make-begin seq) (cons 'begin seq))

تطبيق الإجراء هو أيّ تعبيرٍ مركّبٍ ليس من أنواع التعابير المذكورة أعلاه. فإنّ car التعبير هو المشغّل، وcdr هو قائمة العوامل:

(define (application? exp) (pair? exp))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))
(define (no-operands? ops) (null? ops))
(define (first-operand ops) (car ops))
(define (rest-operands ops) (cdr ops))

التعابير المشتقّة

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

(cond ((> x 0) x)
      ((= x 0) (display 'zero) 0)
      (else (- x)))

إلى مسألة تقييم التعبير الآتي الذي يتضمّن تعابير if وbegin:

(if (> x 0)
    x
    (if (= x 0)
        (begin (display 'zero) 0)
        (- x)))

إنّ تنفيذ تقييم cond على هذا النحو يُبسّط المُقيّم لأنّه يُنقص عدد الصيغ الخاصّة التي يلزم تحديد عمليّة تقييمها صراحةً.

نحن نُدرج إجراءات صياغةٍ تستخرج أجزاء تعبير cond، وإجراءًا cond->if يحوّل تعابير cond إلى تعابير if. ويبدأ تحليل الحالات بـcond وله قائمةٌ من بنود المسيّم والإجراء. والبند بندُ else إذا كان مُسَيِّمه هو الرمز else.[12]

(define (cond? exp) 
  (tagged-list? exp 'cond))
(define (cond-clauses exp) (cdr exp))
(define (cond-else-clause? clause)
  (eq? (cond-predicate clause) 'else))
(define (cond-predicate clause) 
  (car clause))
(define (cond-actions clause) 
  (cdr clause))
(define (cond->if exp)
  (expand-clauses (cond-clauses exp)))
(define (expand-clauses clauses)
  (if (null? clauses)
      'false     ; no else clause
      (let ((first (car clauses))
            (rest (cdr clauses)))
        (if (cond-else-clause? first)
            (if (null? rest)
                (sequence->exp 
                 (cond-actions first))
                (error "ELSE clause isn't 
                        last: COND->IF"
                       clauses))
            (make-if (cond-predicate first)
                     (sequence->exp 
                      (cond-actions first))
                     (expand-clauses 
                      rest))))))

تُسمّى التعابير (كـcond) التي نختار تنفيذها بوصفها تحويلاتٍ صياغيّةً التعابير المشتقّة (derived expressions). وتعبيرات let مشتقّةٌ هي أيضًا (انظر التمرين 4.6).[13]

التمرين 4.2: يخطط لويس ريزونر لإعادة ترتيب بنود cond في eval بحيث يظهر بند تطبيقات الإجراءات قبل بند الإحلالات. وهو يُحاجّ أنّ ذلك سيجعل المفسّر أكثر كفاءة: فإنّ البرامج تحتوي عادةً على تطبيقاتَ أكثرَ من الإحلالات والتعريفات وغيرها، وممّا يعني أنّ eval المعدّل سيفحص بنودًا أقلّ عادةً من eval الأصليّ قبل تحديد نوع التعبير. فما الخطأ في خطة لويس؟ (تلميح: ماذا سيفعل مُقيّم لويس بالتعبير (define x 3)؟)

لويس مستاءٌ لأنّ خطته لم تُجْدِ نفعًا. وهو على استعدادٍ لأن يبذل أيّ جهدٍ لجعل مُقيّمه يتعرّف على تطبيقات الإجراءات قبل أن يفحص معظم الأنواع الأخرى من التعابير. ساعده بتغيير صياغة اللغة المقيَّمة بحيث تبدأ تطبيقات الإجراءات بـcall. فمثلًا، بدلًا من (factorial 3) سنكتب الآن (call factorial 3)، وبدلًا من (+ 1 2) سنكتب (call + 1 2).

التمرين 4.3: أعِد كتابة eval بحيث يُجرى التوجيه بحسب نوع التعبير بأسلوبٍ موجَّهٍ بالبيانات. قارن ذلك بإجراء التفاضل الموجَّه بالبيانات الوارد في التمرين 2.73. (ويجوز لك استخدام car التعبير المركّب بوصفه نوع التعبير، كما يليق بالصياغة المنفّذة في هذا القسم.)

التمرين 4.4: تذكّر تعريفات الصيغتين الخاصّتين and وor الواردة في الفصل 1: and: تُقيَّم التعابير من اليسار إلى اليمين. فإن قُيِّم أيّ تعبيرٍ إلى القيمة خاطئة، أُعيدت خاطئة؛ ولا تُقيَّم أيّ من التعابير الباقية. فإن قُيِّمت جميع التعابير إلى قيمٍ صحيحة، أُعيدت قيمة التعبير الأخير. فإن لم تكن هناك تعابيرٌ، أُعيدت صحيحة.

or: تُقيَّم التعابير من اليسار إلى اليمين. فإن قُيِّم أيّ تعبيرٍ إلى قيمةٍ صحيحة، أُعيدت تلك القيمة؛ ولا تُقيَّم أيّ من التعابير الباقية. فإن قُيِّمت جميع التعابير إلى خاطئة، أو إن لم تكن هناك تعابيرٌ، أُعيدت خاطئة.

ثبِّت and وor بوصفهما صيغتين خاصّتين جديدتين للمقيّم، بتعريف إجراءات صياغةٍ مناسبةٍ وإجراءَي تقييمٍ eval-and وeval-or. وبدلًا من ذلك، أظهِر كيف يمكن تنفيذ and وor بوصفهما تعبيرين مشتقّين.

تسمح Scheme بصياغةٍ إضافيّةٍ لبنود cond، وهي (⟨test⟩ => ⟨recipient⟩). فإن قُيِّم ⟨الاختبار⟩ إلى قيمةٍ صحيحة، قُيِّم ⟨المستلم⟩. ويلزم أن تكون قيمته إجراءً بوسيطٍ واحد؛ ثمّ يُستدعى هذا الإجراء على قيمة ⟨الاختبار⟩، وتُعاد النتيجة قيمةً لتعبير cond. فمثلًا

(cond ((assoc 'b '((a 1) (b 2))) => cadr)
      (else false))

تُعيد 2. عدّل معالجة cond بحيث تدعم هذه الصياغة الممتدّة.

تعبيرات let تعبيراتٌ مشتقّة، لأنّ

(let ((⟨var₁⟩ ⟨exp₁⟩) … (⟨varₙ⟩ ⟨expₙ⟩))
  ⟨body⟩)

مكافئٌ لِـ

((lambda (⟨var₁⟩ … ⟨varₙ⟩)
   ⟨body⟩)
 ⟨exp₁⟩
 …
 ⟨expₙ⟩)

نفّذ تحويلًا صياغيًّا let->combination يُختزل به تقييم تعابير let إلى تقييم تركيباتٍ من النوع الموضّح أعلاه، وأضِف البند المناسب إلى eval ليتعامل مع تعابير let.

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

(let* ((x 3)
       (y (+ x 2))
       (z (+ x y 5)))
  (* x z))

فإنّه يُعيد 39. اشرح كيف يمكن إعادة كتابة تعبير let* مجموعةً من تعابير let متداخلة، واكتب إجراءً let*->nested-lets يُجرِي هذا التحويل. فإن كنّا قد نفّذنا let بالفعل (التمرين 4.6) وأردنا توسيع المقيّم ليتعامل مع let*، فهل يكفي إضافةُ بندٍ إلى eval يكون فعله

(eval (let*->nested-lets exp) env)

أم يلزمنا أن نوسّع let* صراحةً بدلالة تعابيرَ غير مشتقّة؟

التمرين 4.8: الـlet المسمّى (named let) صيغةٌ بديلةٌ من let له الشكل

(let ⟨var⟩ ⟨bindings⟩ ⟨body⟩)

أمّا ⟨الروابط⟩ و⟨الجسم⟩ فهما كما في let الاعتياديّ، إلّا أنّ ⟨المتغيّر⟩ يُقيَّد داخل ⟨الجسم⟩ بإجراءٍ جسمه هو ⟨الجسم⟩ ووسائطه الشكليّة هي المتغيّرات الواردة في ⟨الروابط⟩. وبذلك، يمكن للمرء أن ينفّذ ⟨الجسم⟩ مرّاتٍ متعدّدةً بنداء الإجراء المسمّى ⟨المتغيّر⟩. فعلى سبيل المثال، يمكن إعادة كتابة إجراء فيبوناتشي التكراريّ (1.2.2) بـlet المسمّى كما يلي:

(define (fib n)
  (let fib-iter ((a 1) (b 0) (count n))
    (if (= count 0)
        b
        (fib-iter (+ a b) 
                  a 
                  (- count 1)))))

عدّل let->combination الوارد في التمرين 4.6 ليدعم let المسمّى أيضًا.

التمرين 4.9: تدعم لغاتٌ كثيرةٌ أصنافًا متنوّعةً من بنى التكرار، كـdo وfor وwhile وuntil. وفي Scheme، يمكن التعبير عن العمليّات التكراريّة بدلالة نداءات إجراءاتٍ اعتياديّة، لذا فإنّ بنى التكرار الخاصّة لا تُقدّم أيّ مكسبٍ جوهريّ في القوّة الحسابيّة. ومن جهةٍ أخرى، فإنّ مثل هذه البنى ملائمةٌ غالبًا. صمّم بعض بنى التكرار، وأعطِ أمثلةً على استخدامها، وبيّن كيف يمكن تنفيذها تعابيرَ مشتقّة.

التمرين 4.10: باستخدامنا تجريد البيانات، استطعنا كتابة إجراء eval مستقلًّا عن الصياغة الخاصّة للغة التي ستُقيَّم. ولتوضيح هذا، صمّم ونفّذ صياغةً جديدةً لـScheme بتعديل الإجراءات الواردة في هذا القسم، دون تغيير eval أو apply.

4.1.3 بنيات بيانات المقيّم

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

اختبار المُسَيِّمات

وأمّا في التعابير الشرطيّة، فنحن نقبل بوصفها صحيحةً أيّ شيءٍ ليس هو الكائن false الصريح.

(define (true? x)
  (not (eq? x false)))

(define (false? x)
  (eq? x false))

تمثيل الإجراءات

ولكي نتعامل مع الإجراءات الأوّليّة، نفترض أنّ الإجراءات الآتية متاحةٌ لدينا:

  • يُطبّق (apply-primitive-procedure ⟨proc⟩ ⟨args⟩) الإجراء الأوّليّ المعطى على قيم المعطيات الواردة في القائمة ⟨args⟩ ويُعيد نتيجة التطبيق.
  • يختبر (primitive-procedure? ⟨proc⟩) ما إذا كان ⟨proc⟩ إجراءً أوّليًّا.

وتوصف هذه الآليّات للتعامل مع الأوّليّات بتوسّعٍ أكثر في 4.1.4.

وتُبنى الإجراءات المركّبة من الوسائط الشكليّة وأجساد الإجراءات والبيئات باستخدام البانيّ make-procedure:

(define (make-procedure parameters body env)
  (list 'procedure parameters body env))
(define (compound-procedure? p)
  (tagged-list? p 'procedure))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))

العمليّات على البيئات

يحتاج المقيّم إلى عمليّاتٍ للتلاعب بالبيئات. فكما هو موضّح في 3.2، البيئة تسلسلٌ من الإطارات، حيث كلّ إطارٍ جدولٌ من الروابط التي تربط المتغيّرات بقيمها المقابلة. ونحن نستخدم العمليّات الآتية للتلاعب بالبيئات:

  • تُعيد (lookup-variable-value ⟨var⟩ ⟨env⟩) القيمة المربوطة بالرمز ⟨var⟩ في البيئة ⟨env⟩، أو تشير إلى خطأٍ إذا كان المتغيّر غير مقيَّد.
  • تُعيد (extend-environment ⟨variables⟩ ⟨values⟩ ⟨base-env⟩) بيئةً جديدةً، تتألّف من إطارٍ جديدٍ تُربط فيه الرموز الواردة في القائمة ⟨variables⟩ بالعناصر المقابلة في القائمة ⟨values⟩، حيث البيئة المحيطة هي البيئة ⟨base-env⟩.
  • تُضيف (define-variable! ⟨var⟩ ⟨value⟩ ⟨env⟩) إلى أوّل إطارٍ في البيئة ⟨env⟩ ربطًا جديدًا يربط المتغيّر ⟨var⟩ بالقيمة ⟨value⟩.
  • تُغيّر (set-variable-value! ⟨var⟩ ⟨value⟩ ⟨env⟩) ربط المتغيّر ⟨var⟩ في البيئة ⟨env⟩ بحيث يصبح المتغيّر مقيَّدًا الآن بالقيمة ⟨value⟩، أو تشير إلى خطأٍ إذا كان المتغيّر غير مقيَّد.

ولتنفيذ هذه العمليّات نُمثّل البيئة قائمةً من الإطارات. والبيئة المحيطة ببيئةٍ ما هي cdr القائمة. والبيئة الخالية هي ببساطةٍ القائمة الخالية.

(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))
(define the-empty-environment '())

ويُمثَّل كلّ إطارٍ من إطارات البيئة بزوجٍ من القوائم: قائمة المتغيّرات المقيَّدة في ذلك الإطار وقائمة القيم المرتبطة بها.[14]

(define (make-frame variables values)
  (cons variables values))
(define (frame-variables frame) (car frame))
(define (frame-values frame) (cdr frame))
(define (add-binding-to-frame! var val frame)
  (set-car! frame (cons var (car frame)))
  (set-cdr! frame (cons val (cdr frame))))

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

(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" 
                 vars 
                 vals)
          (error "Too few arguments supplied" 
                 vars 
                 vals))))

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

(define (lookup-variable-value var env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop 
              (enclosing-environment env)))
            ((eq? var (car vars))
             (car vals))
            (else (scan (cdr vars) 
                        (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error "Unbound variable" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))

ولضبط متغيّرٍ على قيمةٍ جديدةٍ في بيئةٍ محدَّدةٍ، نبحث عن المتغيّر، تمامًا كما في lookup-variable-value، ونُغيّر القيمة المقابلة حين نجده.

(define (set-variable-value! var val env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop 
              (enclosing-environment env)))
            ((eq? var (car vars))
             (set-car! vals val))
            (else (scan (cdr vars) 
                        (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error "Unbound variable: SET!" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))

ولتقييد متغيّرٍ، نبحث في الإطار الأوّل عن ربطٍ للمتغيّر، ونُغيّر الربط إذا كان موجودًا (تمامًا كما في set-variable-value!). فإن لم يوجد ربطٌ كهذا، نُضيف واحدًا إلى الإطار الأوّل.

(define (define-variable! var val env)
  (let ((frame (first-frame env)))
    (define (scan vars vals)
      (cond ((null? vars)
             (add-binding-to-frame! 
              var val frame))
            ((eq? var (car vars))
             (set-car! vals val))
            (else (scan (cdr vars) 
                        (cdr vals)))))
    (scan (frame-variables frame)
          (frame-values frame))))

إنّ الطريقة الموصوفة هنا ليست إلّا واحدةً من طرائق متعدّدةٍ معقولةٍ لتمثيل البيئات. وبما أنّنا استخدمنا تجريد البيانات لعزل بقيّة المقيّم عن اختيار التمثيل التفصيليّ، فبوسعنا تغيير تمثيل البيئة إن أردنا. (انظر التمرين 4.11.) ففي نظام Lisp ذي جودةٍ إنتاجيّة، يكون لسرعة عمليّات البيئة في المقيّم - وخاصّةً عمليّة النفاذ إلى المتغيّرات - أثرٌ كبيرٌ في أداء النظام. والتمثيل الموصوف هنا، على الرغم من بساطته المفاهيميّة، غير كفؤٍ ولا يُستخدم عادةً في نظامٍ إنتاجيّ.[15]

التمرين 4.11: بدلًا من تمثيل الإطار بزوجٍ من القوائم، نستطيع تمثيله قائمةً من الروابط، حيث كلّ رابطٍ زوجٌ من الاسم والقيمة. أعِد كتابة عمليّات البيئة لتستخدم هذا التمثيل البديل.

التمرين 4.12: يمكن التعبير عن الإجراءات define-variable! وset-variable-value! وlookup-variable-value بدلالة إجراءاتٍ أكثر تجريدًا لاجتياز بنية البيئة. عرّف تجريداتٍ تُلخّص الأنماط المشتركة، وأعِد تعريف الإجراءات الثلاثة بدلالة هذه التجريدات.

التمرين 4.13: تسمح لنا Scheme بإنشاء روابط جديدة للمتغيّرات بواسطة define، لكنّها لا توفّر أيّ وسيلةٍ للتخلّص من الروابط. نفّذ للمقيّم صيغةً خاصّةً make-unbound! تُزيل ربط رمزٍ معطىً من البيئة التي يُقيَّم فيها تعبير make-unbound!. وهذه المسألة غير محدَّدةٍ تمامًا. فعلى سبيل المثال، هل ينبغي لنا أن نُزيل الربط الواقع في الإطار الأوّل من البيئة فقط؟ أكمل التحديد وبرّر أيّ خياراتٍ تتّخذها.

4.1.4 تشغيل المقيّم بوصفه برنامجًا

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

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

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

(define (setup-environment)
  (let ((initial-env
         (extend-environment 
          (primitive-procedure-names)
          (primitive-procedure-objects)
          the-empty-environment)))
    (define-variable! 'true true initial-env)
    (define-variable! 'false false initial-env)
    initial-env))

(define the-global-environment 
  (setup-environment))

لا يهمّ كيف نُمثّل كائنات الإجراءات الأوّليّة، ما دام apply قادرًا على تمييزها وتطبيقها باستخدام الإجراءين primitive-procedure? وapply-primitive-procedure. وقد اخترنا تمثيل الإجراء الأوّليّ قائمةً تبدأ بالرمز primitive وتحتوي إجراءً في Lisp الكامنة يُنفّذ ذلك الأوّليّ.

(define (primitive-procedure? proc)
  (tagged-list? proc 'primitive))

(define (primitive-implementation proc) 
  (cadr proc))

وسيحصل setup-environment على أسماء الأوّليّات وإجراءات التنفيذ من قائمة:[16]

(define primitive-procedures
  (list (list 'car car)
        (list 'cdr cdr)
        (list 'cons cons)
        (list 'null? null?)
        ⟨more primitives⟩ ))

(define (primitive-procedure-names)
  (map car primitive-procedures))

(define (primitive-procedure-objects)
  (map (lambda (proc) 
         (list 'primitive (cadr proc)))
       primitive-procedures))

ولتطبيق إجراءٍ أوّليّ، نُطبّق ببساطةٍ إجراء التنفيذ على المعطيات، باستخدام نظام Lisp الكامن:[17]

(define (apply-primitive-procedure proc args)
  (apply-in-underlying-scheme
   (primitive-implementation proc) args))

ولتسهيل تشغيل المقيّم الدائريّ (metacircular evaluator)، نُوفّر حلقة قيادة (driver loop) تحاكي حلقة القراءة والتقييم والطباعة (read-eval-print loop) في نظام Lisp الكامن. فهي تطبع طلبًا (prompt)، وتقرأ تعبيرَ إدخالٍ، وتُقيّم هذا التعبير في البيئة العامّة، وتطبع النتيجة. ونحن نُسبق كلّ نتيجةٍ مطبوعةٍ بطلب إخراج (output prompt) حتّى نُميّز قيمة التعبير عن أيّ مخرجاتٍ أخرى قد تُطبَع.[18]

(define input-prompt  ";;; M-Eval input:")
(define output-prompt ";;; M-Eval value:")

(define (driver-loop)
  (prompt-for-input input-prompt)
  (let ((input (read)))
    (let ((output 
           (eval input 
                 the-global-environment)))
      (announce-output output-prompt)
      (user-print output)))
  (driver-loop))

(define (prompt-for-input string)
  (newline) (newline) 
  (display string) (newline))

(define (announce-output string)
  (newline) (display string) (newline))

نستخدم إجراء طبعٍ خاصّ، user-print، لتجنّب طبع جزء البيئة من إجراءٍ مركّب، الذي قد يكون قائمةً طويلةً جدًّا (أو قد يحتوي حتّى دورات).

(define (user-print object)
  (if (compound-procedure? object)
      (display 
       (list 'compound-procedure
             (procedure-parameters object)
             (procedure-body object)
             '<procedure-env>))
      (display object)))

والآن، فإنّ كلّ ما يلزمنا لتشغيل المقيّم هو تهيئة البيئة العامّة وبدء حلقة القيادة. وإليك تفاعلًا نموذجيًّا:

(define the-global-environment 
  (setup-environment))

(driver-loop)

;;; M-Eval input:
(define (append x y)
  (if (null? x)
      y
      (cons (car x) (append (cdr x) y))))

;;; M-Eval value:
ok

;;; M-Eval input:
(append '(a b c) '(d e f))

;;; M-Eval value:
(a b c d e f)

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

4.1.5 البيانات بوصفها برامج

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

(define (factorial n)
  (if (= n 1)
      1
      (* (factorial (- n 1)) n)))

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

الشكل 4.2: برنامج العامليّات، منظورًا إليه بوصفه آلةً مجرّدة.

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

الشكل 4.3: المقيّم يحاكي آلة العامليّات.

من هذا المنظور، يتبيّن أنّ مقيّمنا آلةٌ عامّة (universal machine). فهو يُحاكي آلاتٍ أخرى حين تُوصَف هذه الآلات ببرامج Lisp.[19] وهذا مدهش. فحاوِل أن تتخيّل مقيّمًا مماثلاً للدوائر الكهربائيّة. سيكون هذا دائرةً تأخذ إشارةً تُرمّز مخططات دائرةٍ أخرى، كالمرشّح مثلًا، إدخالًا. فبإعطائها هذا الإدخال، سيتصرّف مقيّم الدوائر بسلوك المرشّح ذي الوصف نفسه. ومثل هذه الدائرة الكهربائيّة العامّة مركّبةٌ إلى حدّ لا يكاد يُتصوّر. ومن الجدير بالدهشة أنّ مقيّم البرامج برنامجٌ بسيطٌ إلى حدٍّ ما.[20]

وجانبٌ آخر مدهش في المقيّم أنّه يعمل بوصفه جسرًا بين كائنات البيانات التي تتلاعب بها لغة البرمجة لدينا ولغة البرمجة ذاتها. فتخيّل أنّ برنامج المقيّم (المنفّذ بـLisp) يعمل، وأنّ مستخدمًا يكتب تعابير للمقيّم ويراقب النتائج. فمن منظور المستخدم، تعبيرُ إدخالٍ كـ(* x x) هو تعبيرٌ في لغة البرمجة، ينبغي للمقيّم أن ينفّذه. وأمّا من منظور المقيّم، فالتعبير ليس إلّا قائمةً (في هذه الحالة، قائمةً من ثلاثة رموز: * وx وx) يُتلاعب بها وفقًا لمجموعةٍ محدَّدةٍ بدقّةٍ من القواعد.

وكون برامج المستخدم بياناتِ المقيّم لا يلزم أن يكون مصدرًا للالتباس. ففي الحقيقة، يكون إهمال هذا التمييز ملائمًا أحيانًا، ومنح المستخدم القدرة على تقييم كائن بيانات صراحةً بوصفه تعبير Lisp، وذلك بجعل eval متاحًا للاستخدام في البرامج. فلهجات Lisp كثيرةٌ تُوفّر إجراء eval أوّليًّا يأخذ تعبيرًا وبيئةً معطيين ويُقيّم التعبير نسبةً إلى البيئة.[21] وهكذا،

(eval '(* 5 5) user-initial-environment)

و

(eval (cons '* (list 5 5)) 
      user-initial-environment)

فكلاهما سيُعيد 25.[22]

التمرين 4.15: إذا أُعطي إجراءٌ ذو وسيطٍ واحدٍ p وكائنٌ a، فإنّه يُقال إنّ p «يتوقّف» على a إذا أعاد تقييم التعبير (p a) قيمةً (بخلاف الانتهاء برسالة خطأٍ أو الدوران إلى الأبد). أظهر أنّ من المحال كتابة إجراءٍ halts? يحسم بصورةٍ صحيحةٍ ما إذا كان p يتوقّف على a لأيّ إجراءٍ p وأيّ كائنٍ a. واستخدم الاستدلال الآتي: لو كان لديك إجراءٌ halts? كهذا، لأمكنك تنفيذ البرنامج الآتي:

(define (run-forever)
  (run-forever))

(define (try p)
  (if (halts? p p)
      (run-forever)
      'halted))

والآن، تأمّل تقييم التعبير (try try) وأظهر أنّ أيّ نتيجةٍ ممكنةٍ (سواءً التوقّف أو الدوران إلى الأبد) تُخالف السلوك المقصود من halts?.[23]

4.1.6 التعريفات الداخليّة

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

تأمّل إجراءً له تعريفاتٌ داخليّة، كالآتي:

(define (f x)
  (define (even? n)
    (if (= n 0)
        true
        (odd? (- n 1))))
  (define (odd? n)
    (if (= n 0)
        false
        (even? (- n 1))))
  ⟨rest of body of f⟩)

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

وقد صف أنّ مفسّرنا سيُقيّم نداءات f بصورةٍ صحيحة، لكن لسببٍ «عرضيّ»: وذلك بأنّ تعريفات الإجراءات الداخليّة تأتي أوّلًا، فلن تُقيَّم أيّ نداءاتٍ لهذه الإجراءات حتّى تُعرَّف جميعها. ومن ثمّ، سيكون odd? قد عُرِّف بحين تُنفَّذ even?. في الواقع، وستُعطي آليّة التقييم التسلسليّ لدينا النتيجة ذاتها التي تُعطيها آليّةٌ تُنفّذ التعريف المتزامن مباشرةً، لأيّ إجراءٍ تأتي فيه التعريفات الداخليّة أوّلًا في الجسم، وحيث لا يستخدم تقييم تعابير قيم المتغيّرات المُعرَّفة أيًّا من المتغيّرات المُعرَّفة فعلًا. (ولمثالٍ على إجراءٍ لا يتّبع هذه القيود، بحيث لا يكون التعريف التسلسليّ مكافئًا للتعريف المتزامن، انظر التمرين 4.19.)[24]

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

(lambda ⟨vars⟩
  (define u ⟨e1⟩)
  (define v ⟨e2⟩)
  ⟨e3⟩)

سيُحوَّل إلى

(lambda ⟨vars⟩
  (let ((u '*unassigned*)
        (v '*unassigned*))
    (set! u ⟨e1⟩)
    (set! v ⟨e2⟩)
    ⟨e3⟩))

حيث *unassigned* رمزٌ خاصّ يجعل النفاذ إلى متغيّرٍ يُشير إلى خطأٍ إذا حُول استخدام قيمة المتغيّر الذي لم يُحلَّ بعد.

وتُبيّن التمرين 4.18 استراتيجيّةً بديلةً لمسح التعريفات الداخليّة. وبخلاف التحويل الموضّح أعلاه، فإنّ هذا التحويل يفرض القيد القائل إنّه يمكن تقييم قيم المتغيّرات المُعرَّفة دون استخدام أيّ من قيم المتغيّرات.[25]

التمرين 4.16: في هذا التمرين نُنفّذ الطريقة الموصوفة للتوّ لتفسير التعريفات الداخليّة. ونفترض أنّ المقيّم يدعم let (انظر التمرين 4.6). عدّل lookup-variable-value (4.1.3) ليُشير إلى خطأٍ إذا كانت القيمة التي يجدها هي الرمز *unassigned*.

اكتب إجراءً scan-out-defines يأخذ جسم إجراءٍ ويُعيد جسمًا مكافئًا له لا يحتوي تعريفاتٍ داخليّةً، بإجراء التحويل الموصوف أعلاه. ثبّت scan-out-defines في المفسّر، إمّا في make-procedure وإمّا في procedure-body (انظر 4.1.3). فأيّ الموضعين أفضل؟ ولماذا؟

التمرين 4.17: ارسم مخطّطات للبيئة السائدة عند تقييم التعبير ⟨e3⟩ في الإجراء الوارد في النصّ، مقارنًا كيف ستُنظَّم حين تُفسَّر التعريفات تسلسليًّا وكيف ستُنظَّم إذا مُسِحَت التعريفات كما هو موضّح. فلماذا هناك إطارٌ إضافيّ في البرنامج المحوَّل؟ واشرح لماذا لا يمكن لهذا الفرق في بنية البيئة أن يُحدِث فرقًا في سلوك برنامجٍ صحيحٍ أبدًا. وصمّم طريقةً تجعل المفسّر يُنفّذ قاعدة النطاق «المتزامن» للتعريفات الداخليّة دون بناء الإطار الإضافيّ.

التمرين 4.18: تأمّل استراتيجيّةً بديلةً لمسح التعريفات تُترجم المثال الوارد في النصّ إلى

(lambda ⟨vars⟩
  (let ((u '*unassigned*)
        (v '*unassigned*))
    (let ((a ⟨e1⟩)
          (b ⟨e2⟩))
      (set! u a)
      (set! v b))
    ⟨e3⟩))

ويهدف a وb هنا إلى تمثيل أسماء متغيّراتٍ جديدة، يخلقها المفسّر، لا تظهر في برنامج المستخدم. وتأمّل الإجراء solve الوارد في 3.5.4:

(define (solve f y0 dt)
  (define y (integral (delay dy) y0 dt))
  (define dy (stream-map f y))
  y)

فهل يعمل هذا الإجراء إذا مُسِحَت التعريفات الداخليّة كما هو موضّح في هذا التمرين؟ وماذا لو مُسِحَت كما هو موضّح في النصّ؟ اشرح.

التمرين 4.19: يختلف بن بِتدل وأليسا ب. هاكر وإيفا لو أتور في النتيجة المرغوبة لتقييم التعبير

(let ((a 1))
  (define (f x)
    (define b (+ a x))
    (define a 5)
    (+ a b))
  (f 10))

يؤكّد بن أنّ النتيجة ينبغي أن تُحصَل باستخدام القاعدة التسلسليّة لـdefine: إذ يُعرَّف b بأنّه 11، ثمّ يُعرَّف a بأنّه 5، فالنتيجة 16. وتاعتراض أليسا أنّ التعاوب المتبادل يتطلّب قاعدة النطاق المتزامن للتعريفات الإجرائيّة الداخليّة، وأنّه من غير المعقول معاملة أسماء الإجراءات معاملةً مختلفةً عن الأسماء الأخرى. وبالتالي، فهي تُؤيّد الآليّة المنفّذة في التمرين 4.16. وهذا سيؤدّي إلى أنّ a غير مُحلًّ حين يُحتسب قيمة b. ومن ثمّ، ففي رأي أليسا ينبغي للإجراء أن يُنتج خطأً. وإلى إيفا رأيٌ ثالث. فهي تقول إنّه إذا كان المقصود من تعريفي a وb أن يكونا متزامنين حقًّا، فينبغي استخدام القيمة 5 لـa في تقييم b. ومن ثمّ، ففي رأي إيفا ينبغي أن يكون a هو 5 وb هو 15، وأن تكون النتيجة 20. فأيّ هذه الآراء تؤيّده (إن أُيّد أيٌّ منها)؟ وهل تستطيع ابتكار طريقةٍ لتنفيذ التعريفات الداخليّة بحيث تتصرف كما تفضّل إيفا؟[26]

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

(define (f x)
  (letrec
      ((even?
        (lambda (n)
          (if (= n 0)
              true
              (odd? (- n 1)))))
       (odd?
        (lambda (n)
          (if (= n 0)
              false
              (even? (- n 1))))))
    ⟨rest of body of f⟩))

تعبيرات Letrec، التي لها الصيغة

(letrec ((⟨var₁⟩ ⟨exp₁⟩) … (⟨varₙ⟩ ⟨expₙ⟩))
  ⟨body⟩)

هي تنويعٌ على let تُقيَّم فيه التعابير ⟨expk⟩⟨ e x p_{k} ⟩ التي تُوفّر القيم الأوّليّة للمتغيّرات ⟨vark⟩⟨ v a r_{k} ⟩ في بيئةٍ تتضمّن جميع روابط letrec. وهذا يسمح بالتعاود في الروابط، كالتعاود المتبادل بين even? وodd? في المثال أعلاه، أو بتقييم عامليّ العدد 10 بـ

(letrec
    ((fact
      (lambda (n)
        (if (= n 1)
            1
            (* n (fact (- n 1)))))))
  (fact 10))
  1. نفّذ letrec بوصفه تعبيرًا مشتقًّا، بتحويل تعبير letrec إلى تعبير let كما هو موضّحٌ في النصّ أعلاه أو في التمرين 4.18. أي أنّه ينبغي إنشاء متغيّرات letrec بـlet ثم إسناد قيمها إليها بـset!.
  2. لويس ريزونر في حيرةٍ من كلّ هذه الضجّة حول التعريفات الداخليّة. فكما يرى الأمر، إن كنت لا تحبّ استخدام define داخل إجراءٍ، فيمكنك استخدام let فحسب. أوضح ما هو الفضفاض في استدلاله برسم مخطّط بيئةٍ يُظهر البيئة التي يُقيَّم فيها ⟨بقية جسم f``⟩ أثناء تقييم التعبير (f 5)، إذ عُرِّف f كما في هذا التمرين. وارسم مخطّط بيئةٍ للتقييم ذاته، لكن باستخدام let بدلًا من letrec في تعريف f.

التمرين 4.21: من المدهون أنّ حدس لويس في التمرين 4.20 صحيح. فمن الممكن فعلًا تحديد إجراءاتٍ متعاودةٍ دون استخدام letrec (أو حتّى define)، مع أنّ طريقة إنجاز هذا أدقّ بكثيرٍ ممّا تصوّره لويس. والتعبير التالي يحتسب عامليّ العدد 10 بتطبيق إجراء عامليّ متعاود:[27]

((lambda (n)
   ((lambda (fact) (fact fact n))
    (lambda (ft k)
      (if (= k 1)
          1
          (* k (ft ft (- k 1)))))))
 10)

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

(define (f x)
  (define (even? n)
    (if (= n 0)
        true
        (odd? (- n 1))))
  (define (odd? n)
    (if (= n 0)
        false
        (even? (- n 1))))
  (even? x))

املأ التعابير الناقصة لإتمام تعريفٍ بديلٍ لـf لا يستخدم تعريفاتٍ داخليّةً ولا letrec:

(define (f x)
  ((lambda (even? odd?)
     (even? even? odd? x))
   (lambda (ev? od? n)
     (if (= n 0) 
         true 
         (od? ⟨??⟩ ⟨??⟩ ⟨??⟩)))
   (lambda (ev? od? n)
     (if (= n 0) 
         false 
         (ev? ⟨??⟩ ⟨??⟩ ⟨??⟩)))))

4.1.7 فصل تحليل الصياغة عن التنفيذ

المقيّم المنفّذ أعلاه بسيط، لكنه غير كفؤٍ على الإطلاق، لأنّ تحليل صياغة التعابير متشابكٌ مع تنفيذها. لذا فإذا نُفّذ برنامجٌ مرّاتٍ كثيرة، تُحلَّل صياغته مرّاتٍ كثيرة. فتأمّل، مثلًا، تقييم (factorial 4) باستخدام تعريف factorial التالي:

(define (factorial n)
  (if (= n 1)
      1
      (* (factorial (- n 1)) n)))

في كلّ مرّةٍ يُنادى فيها factorial، ينبغي للمقيّم أن يحدّد أنّ الجسم تعبير if وأن يستخرج المسيّم. وحينئذٍ فقط يستطيع تقييم المسيّم والتصرّف بحسب قيمته. وفي كلّ مرّةٍ يقيّم فيها التعبير (* (factorial (- n 1)) n)، أو التعبيران الجزئيّان (factorial (- n 1)) و(- n 1)، ينبغي للمقيّم أن يُجري تحليل الحالات في eval لتحديد أنّ التعبير تطبيق، وأن يستخرج مشغّله وعوامله. وهذا التحليل مكلف. وأداؤه مُكرَّرًا مُهدر.

يمكننا تحويل المقيّم ليكون أكفأَ بشكلٍ ملحوظٍ بترتيب الأمور بحيث لا يُجرى تحليل الصياغة إلّا مرّةً واحدة.[28] فنحن نقسّم eval - الذي يأخذ تعبيرًا وبيئةً - إلى جزأين. والإجراء analyze يأخذ التعبير وحده، فيُجري تحليل الصياغة ويُعيد إجراءً جديدًا، هو إجراء التنفيذ (execution procedure)، الذي يغلّف العمل الواجب أداؤه في تنفيذ التعبير المحلَّل. وإجراء التنفيذ يأخذ بيئةً كمعطىً ويُكمل التقييم. وهذا يوفّر عملًا لأنّ analyze سيُنادى مرّةً واحدةً فقط على تعبيرٍ، بينما قد يُنادى إجراء التنفيذ مرّاتٍ كثيرة.

وبالفصل بين التحليل والتنفيذ، يصبح eval الآن

(define (eval exp env) ((analyze exp) env))

إنّ نتيجة نداء analyze هي إجراء التنفيذ الواجب تطبيقه على البيئة. والإجراء analyze هو تحليل الحالات ذاته الذي يؤدّيه eval الأصليّ الوارد في 4.1.1، إلّا أنّ الإجراءات التي نُوزّع إليها تؤدّي التحليل فحسب، لا التقييم الكامل:

(define (analyze exp)
  (cond ((self-evaluating? exp)
         (analyze-self-evaluating exp))
        ((quoted? exp) 
         (analyze-quoted exp))
        ((variable? exp) 
         (analyze-variable exp))
        ((assignment? exp) 
         (analyze-assignment exp))
        ((definition? exp) 
         (analyze-definition exp))
        ((if? exp) 
         (analyze-if exp))
        ((lambda? exp) 
         (analyze-lambda exp))
        ((begin? exp) 
         (analyze-sequence 
          (begin-actions exp)))
        ((cond? exp) 
         (analyze (cond->if exp)))
        ((application? exp) 
         (analyze-application exp))
        (else
         (error "Unknown expression 
                 type: ANALYZE" 
                exp))))

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

(define (analyze-self-evaluating exp)
  (lambda (env) exp))

وبالنسبة لتعبيرٍ مُقتبَس، فيمكننا أن نكسب قليلًا من الكفاءة باستخراج نصّ الاقتباس مرّةً واحدةً فقط، في مرحلة التحليل، بدلًا من مرحلة التنفيذ.

(define (analyze-quoted exp)
  (let ((qval (text-of-quotation exp)))
    (lambda (env) qval)))

أمّا البحث عن قيمة متغيّرٍ فينبغي أن يظلّ يجري في مرحلة التنفيذ، لأنّ ذلك يعتمد على معرفة البيئة.[29]

(define (analyze-variable exp)
  (lambda (env) 
    (lookup-variable-value exp env)))

وينبغي لـanalyze-assignment أن يؤجّل إسناد المتغيّر فعلًا حتّى التنفيذ، حين تكون البيئة قد قُدّمت. لكنّ حقيقة أنّ تعبير assignment-value يمكن تحليله (تعاوديًّا) أثناء التحليل هي مكسبٌ كبيرٌ في الكفاءة، لأنّ تعبير assignment-value سيُحلَّل الآن مرّةً واحدةً فقط. والشيء ذاته يصدق على التعريفات.

(define (analyze-assignment exp)
  (let ((var (assignment-variable exp))
        (vproc (analyze 
                (assignment-value exp))))
    (lambda (env)
      (set-variable-value! 
       var (vproc env) env)
      'ok)))

(define (analyze-definition exp)
  (let ((var (definition-variable exp))
        (vproc (analyze 
                (definition-value exp))))
    (lambda (env)
      (define-variable! var (vproc env) env)
      'ok)))

وبالنسبة لتعبيرات if، نستخرج المسيّم والناتج والبديل ونحلّلها في وقت التحليل.

(define (analyze-if exp)
  (let ((pproc (analyze (if-predicate exp)))
        (cproc (analyze (if-consequent exp)))
        (aproc (analyze (if-alternative exp))))
    (lambda (env)
      (if (true? (pproc env))
          (cproc env)
          (aproc env)))))

ويُحقّق تحليل تعبير lambda مكسبًا كبيرًا في الكفاءة أيضًا: فنحن نحلّل جسم lambda مرّةً واحدةً فقط، مع أنّ الإجراءات الناتجة عن تقييم lambda قد تُطبَّق مرّاتٍ كثيرة.

(define (analyze-lambda exp)
  (let ((vars (lambda-parameters exp))
        (bproc (analyze-sequence 
                (lambda-body exp))))
    (lambda (env) 
      (make-procedure vars bproc env))))

أمّا تحليل تسلسل تعابير (كما في begin أو جسم تعبير lambda) فهو أكثر تعقيدًا.[30] فكلّ تعبيرٍ في التسلسل يُحلَّل، مُنتجًا إجراء تنفيذ. وهذه الإجراءات تُدمج لإنتاج إجراء تنفيذٍ يأخذ بيئةً كمعطىً وينادي كلّ إجراء تنفيذٍ منفردٍ، بالبيئة معطىً له، تتاليًّا.

(define (analyze-sequence exps)
  (define (sequentially proc1 proc2)
    (lambda (env) (proc1 env) (proc2 env)))
  (define (loop first-proc rest-procs)
    (if (null? rest-procs)
        first-proc
        (loop (sequentially first-proc 
                            (car rest-procs))
              (cdr rest-procs))))
  (let ((procs (map analyze exps)))
    (if (null? procs)
        (error "Empty sequence: ANALYZE"))
    (loop (car procs) (cdr procs))))

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

(define (analyze-application exp)
  (let ((fproc (analyze (operator exp)))
        (aprocs (map analyze (operands exp))))
    (lambda (env)
      (execute-application 
       (fproc env)
       (map (lambda (aproc) (aproc env))
            aprocs)))))

(define (execute-application proc args)
  (cond ((primitive-procedure? proc)
         (apply-primitive-procedure proc args))
        ((compound-procedure? proc)
         ((procedure-body proc)
          (extend-environment 
           (procedure-parameters proc)
           args
           (procedure-environment proc))))
        (else (error "Unknown procedure type: 
                      EXECUTE-APPLICATION"
                     proc))))

ويستخدم مقيّمنا الجديد بنى البيانات ذاتها وإجراءات الصياغة وإجراءات دعم وقت التشغيل الواردة في 4.1.2، و4.1.3، و4.1.4.

التمرين 4.22: وسّع المقيّم الوارد في هذا القسم ليدعم الصيغة الخاصّة let. (انظر التمرين 4.6.)

التمرين 4.23: أليسا ب. هاكر لا تفهم لماذا يلزم أن يكون analyze-sequence بهذا التعقيد. فجميع إجراءات التحليل الأخرى تحويلاتٌ مباشرةٌ لإجراءات التقييم المقابلة (أو بنود eval) في 4.1.1. وقد توقّعت أن يكون analyze-sequence على هذه الصورة:

(define (analyze-sequence exps)
  (define (execute-sequence procs env)
    (cond ((null? (cdr procs)) 
           ((car procs) env))
          (else ((car procs) env)
                (execute-sequence 
                 (cdr procs) env))))
  (let ((procs (map analyze exps)))
    (if (null? procs)
        (error "Empty sequence: 
                ANALYZE"))
    (lambda (env) 
      (execute-sequence procs env))))

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

قارن بين النسختين من analyze-sequence. فتأمّل، مثلًا، الحالة الشائعة (النموذجيّة في أجسام الإجراءات) التي يكون التسلسل فيها تعبيرًا واحدًا فحسب. فأيّ عمل سيؤدّيه إجراء التنفيذ الذي يُنتجه برنامج أليسا؟ وماذا عن إجراء التنفيذ الذي يُنتجه البرنامج الوارد في النصّ أعلاه؟ وكيف تقارن النسختان بالنسبة لتسلسلٍ من تعبيرين؟

التمرين 4.24: صمّم ونفّذ بعض التجارب لمقارنة سرعة المقيّم الحَلْقيّ المُحيط (metacircular evaluator) الأصليّ بالنسخة الواردة في هذا القسم. واستخدم نتائجك لتقدير نسبة الوقت التي يُقضى في التحليل مقابل التنفيذ بالنسبة لمختلف الإجراءات.

4.2 تنويعات على Scheme — التقييم المتأخّر

والآن، بعد أن أصبح لدينا مقيّمٌ معبَّرٌ عنه كبرنامج Lisp، فبوسعنا أن نجرّب خياراتٍ بديلةً في تصميم اللغات بتعديل المقيّم تعديلًا بسيطًا. فلا ريب في أنّ اللغات الجديدة تُبتكر غالبًا بكتابة مقيّمٍ يضمّن اللغة الجديدة داخل لغةٍ عالية المستوى قائمة. فمثلًا، إذا أردنا مناقشة جانبٍ ما من تعديلٍ مقترحٍ على Lisp مع عضوٍ آخر في مجتمع Lisp، فبوسعنا أن نُزوّده بمقيّمٍ يجسّد التغيير. وبوسع متلقّيه حينئذٍ أن يجرّب المقيّم الجديد وأن يُرسل ملاحظاته في هيئة تعديلاتٍ إضافيّة. فإنّ أساس التنفيذ عالي المستوى لا يجعل اختبار المقيّم وتنقيحه أسهلَ فحسب؛ بل إنّ الضمّ أيضًا يُتيح للمصمّم أن يستعير[31] خصائص من اللغة الأساس، تمامًا كما يستخدم مقيّم Lisp المضمَّن لدينا الأوّليّات وبنية التحكّم من Lisp الأساس. ولا يلزم المصمّم أن يتكلّف عناء بناء تنفيذٍ كاملٍ بلغةٍ منخفضة المستوى أو في عتادٍ إلّا لاحقًا (إن لزم الأمر). وفي هذا القسم والقسم التالي نستكشف بعض التنويعات على Scheme التي توفّر قوّةً تعبيريّةً إضافيّةً كبيرة.

4.2.1 الترتيب الاعتياديّ والترتيب التطبيقيّ

في 1.1، حيث بدأنا مناقشتنا لنماذج التقييم، لاحظنا أنّ Scheme لغةٌ تطبيقيّة الترتيب، أي أنّ جميع معطيات إجراءات Scheme تُقيَّم حين يُطبَّق الإجراء. وبالمقابل، فإنّ لغات الترتيب الاعتياديّ تؤخّر تقييم معطيات الإجراءات حتّى تحتاج قيم المعطيات الفعليّة. ويُسمّى تأخير تقييم معطيات الإجراءات حتّى اللحظة الأخيرة الممكنة (كاللحظة التي تتطلّبها عمليّة أوّليّة مثلًا) التقييم المتأخّر (lazy evaluation).[32] وتأمّل الإجراء

(define (try a b)
  (if (= a 0) 1 b))

إنّ تقييم (try 0 (/ 1 0)) يُطلق خطأً في Scheme. أمّا مع التقييم المتأخّر، فلن يقع خطأ. وكان تقييم التعبير سيُعيد 1، لأنّ المعطى (/ 1 0) لم يكن ليُقيَّم قطّ.

ومثالٌ يستثمر التقييم المتأخّر هو تعريف إجراءٍ يُدعى unless

(define (unless condition 
                usual-value 
                exceptional-value)
  (if condition 
      exceptional-value 
      usual-value))

يمكن استخدامه في تعابيرَ مثل

(unless (= b 0)
        (/ a b)
        (begin 
          (display "exception: returning 0")
          0))

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

إذا دُخل جسم الإجراء قبل تقييم معطىً ما فإنّا نقول إنّ الإجراء غير صارم (non-strict) في ذلك المعطى. وإذا قُيِّم المعطى قبل الدخول إلى جسم الإجراء فإنّا نقول إنّ الإجراء صارم (strict) في ذلك المعطى.[33] ففي لغةٍ تطبيقيّة الترتيب خالصة، تكون جميع الإجراءات صارمةً في كلّ معطى. وفي لغةٍ اعتياديّة الترتيب خالصة، تكون جميع الإجراءات المركّبة غير صارمةٍ في كلّ معطى، وقد تكون الإجراءات الأوّليّة صارمةً أو غير صارمةٍ. وهناك أيضًا لغات (انظر التمرين 4.31) تمنح المبرمجين تحكّمًا مفصّلًا في صرامة الإجراءات التي يُعرّفونها.

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

التمرين 4.25: افترض أنّنا (في Scheme التطبيقيّة الترتيب الاعتياديّة) نُعرّف unless كما هو موضّحٌ أعلاه، ثم نُعرّف factorial بدلالة unless كما يلي

(define (factorial n)
  (unless (= n 1)
          (* n (factorial (- n 1)))
          1))

فماذا يحدث إذا حاولنا تقييم (factorial 5)؟ وهل ستعمل تعريفاتنا في لغةٍ اعتياديّة الترتيب؟

التمرين 4.26: يختلف بن بِتدل وأليسا ب. هاكر حول أهمّيّة التقييم المتأخّر لتنفيذ أشياءَ مثل unless. ويشير بن إلى أنّ من الممكن تنفيذ unless بالترتيب التطبيقيّ بوصفه صيغةً خاصّة. وتُجيب أليسا بأنّه لو فُعل ذلك، لصار unless صياغةً فحسب، لا إجراءً يمكن استخدامه بالاشتراك مع الإجراءات ذات الرتبة العليا. فأكمل التفاصيل على جانبي الحجّة. وأُظهر كيف يُنفَّذ unless بوصفه تعبيرًا مشتقًّا (مثل cond أو let)، وأعطِ مثالًا على موقفٍ قد يكون من المفيد فيه توفّر unless بوصفه إجراءً، بدلًا من صيغةٍ خاصّة.

4.2.2 مفسّرٌ بالتقييم المتأخّر

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

والفكرة الأساسيّة هي أنّ المفسّر، عند تطبيق إجراءٍ ما، ينبغي أن يُحدّد أيّ المعطيات ستُقيَّم وأيّها ستُؤخَّر. والمعطيات المؤجَّلة لا تُقيَّم؛ بل تتحوّل إلى كائناتٍ تُسمّى المؤجّلات (thunks).[34] وينبغي للمؤجَّل أن يحتوي المعلومات المطلوبة لإنتاج قيمة المعطى عند الحاجة إليها، كأنّه قُيِّمَ في وقت التطبيق. ومن ثمّ، ينبغي للمؤجَّل أن يحتوي تعبير المعطى والبيئة التي يجري فيها تقييم تطبيق الإجراء.

ويُسمّى عمليّة تقييم التعبير في مؤجَّلٍ ما الإجبار (forcing).[35] وبصورةٍ عامّة، لن يُجبَر مؤجَّلٌ إلّا عندما تُحتاج قيمته: عندما يُمرَّر إلى إجراءٍ أوّليّ سيستخدم قيمة المؤجَّل؛ وعندما يكون هو قيمة مسيّمٍ في تعبير شرطيّ؛ وعندما يكون هو قيمة مشغّلٍ على وشك أن يُطبَّق بوصفه إجراءً. ومن الخيارات التصميميّة المتاحة لنا أن نُحقّق حفظ النتائج (memoization) للمؤجّلات أم لا، كما فعلنا مع الكائنات المؤجّلة في 3.5.1. وبحفظ النتائج، فإنّ المؤجَّل يخزّن القيمة التي تُحتسب أوّل مرّةٍ يُجبَر فيها. والإجبارات اللاحقة تُعيد القيمة المخزّونة فحسب دون تكرار الاحتساب. وسنجعل مفسّرنا يحفظ النتائج، لأنّ ذلك أكفأُ لتطبيقاتٍ كثيرة. غير أنّ هناك اعتباراتٍ خفيّة هنا.[36]

تعديل المقيّم

ويتمثّل الفرق الرئيسيّ بين المقيّم المتأخّر والوارد في 4.1 في التعامل مع تطبيقات الإجراءات في eval وapply.

ويصبح بند application? في eval

((application? exp)
 (apply (actual-value (operator exp) env)
        (operands exp)
        env))

وهذا هو ذاته تقريبًا بند application? في eval في 4.1.1. لكنّنا، من أجل التقييم المتأخّر، ننادي apply بتعبيرات العوامل، بدلًا من المعطيات التي تُنتج بتقييمها. ولأنّنا سنحتاج إلى البيئة لبناء المؤجّلات إذا كانت المعطيات ستُؤخَّر، فعلينا أن نُمرّرها أيضًا. ونحن لا نزال نُقيّم المشغّل، لأنّ apply يحتاج الإجراء الفعليّ الواجب تطبيقه حتّى يتوزّع بحسب نوعه (أوّليّ مقابل مركّب) ويطبّقه.

وكلّما احتجنا إلى القيمة الفعليّة لتعبيرٍ، فإنّا نستخدم

(define (actual-value exp env)
  (force-it (eval exp env)))

بدلًا من eval فحسب، حتّى إن كانت قيمة التعبير مؤجَّلًا، فإنّه سيُجبَر.

وإصدارنا الجديد من apply هو أيضًا ذاته تقريبًا الإصدار الوارد في 4.1.1. والفرق هو أنّ eval يمرّر تعبيرات عوامل غير مُقيَّمة: فبالنسبة للإجراءات الأوّليّة (التي هي صارمة)، نُقيّم جميع المعطيات قبل تطبيق الإجراء الأوّليّ؛ وبالنسبة للإجراءات المركّبة (التي هي غير صارمة) نؤخّر جميع المعطيات قبل تطبيق الإجراء.

(define (apply procedure arguments env)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedure
          procedure
          (list-of-arg-values 
           arguments 
           env)))  ; changed
        ((compound-procedure? procedure)
         (eval-sequence
          (procedure-body procedure)
          (extend-environment
           (procedure-parameters procedure)
           (list-of-delayed-args 
            arguments 
            env)   ; changed
           (procedure-environment procedure))))
        (else (error "Unknown procedure 
                      type: APPLY" 
                     procedure))))

والإجراءات التي تُعالج المعطيات هي تمامًا مثل list-of-values الوارد في 4.1.1، إلّا أنّ list-of-delayed-args يؤخّر المعطيات بدلًا من تقييمها، وأنّ list-of-arg-values يستخدم actual-value بدلًا من eval:

(define (list-of-arg-values exps env)
  (if (no-operands? exps)
      '()
      (cons (actual-value 
             (first-operand exps) 
             env)
            (list-of-arg-values 
             (rest-operands exps)
             env))))

(define (list-of-delayed-args exps env)
  (if (no-operands? exps)
      '()
      (cons (delay-it 
             (first-operand exps) 
             env)
            (list-of-delayed-args 
             (rest-operands exps)
             env))))

والموضع الآخر الذي ينبغي أن نُغيّر فيه المقيّم هو التعامل مع if، حيث ينبغي لنا أن نستخدم actual-value بدلًا من eval للحصول على قيمة تعبير المسيّم قبل اختبار ما إذا كانت صحيحة أو خاطئة:

(define (eval-if exp env)
  (if (true? (actual-value (if-predicate exp) 
                           env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

وأخيرًا، ينبغي لنا تغيير إجراء driver-loop (4.1.4) لاستخدام actual-value بدلًا من eval، حتّى إن انتقلت قيمةٌ مؤجَّلةٌ إلى حلقة القراءة والتقييم والطباعة، فإنّها ستُجبَر قبل طباعتها. ونحن نغيّر أيضًا المُحفّزات للإشارة إلى أنّ هذا هو المقيّم المتأخّر:

(define input-prompt  ";;; L-Eval input:")
(define output-prompt ";;; L-Eval value:")

(define (driver-loop)
  (prompt-for-input input-prompt)
  (let ((input (read)))
    (let ((output (actual-value 
                   input 
                   the-global-environment)))
      (announce-output output-prompt)
      (user-print output)))
  (driver-loop))

وبعد إجراء هذه التغييرات، بوسعنا تشغيل المقيّم واختباره. فإنّ التقييم الناجح لتعبير try المناقَش في 4.2.1 يدلّ على أنّ المفسّر يؤدّي تقييمًا متأخّرًا:

(define the-global-environment 
  (setup-environment))

(driver-loop)

;;; L-Eval input:
(define (try a b) (if (= a 0) 1 b))

;;; L-Eval value:
ok

;;; L-Eval input:
(try 0 (/ 1 0))

;;; L-Eval value:
1

تمثيل المؤجّلات

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

(define (force-it obj)
  (if (thunk? obj)
      (actual-value (thunk-exp obj) 
                    (thunk-env obj))
      obj))

وإحدى الطرائق السهلة لحزم تعبيرٍ ببيئةٍ هي جعل قائمةٍ تحتوي التعبير والبيئة. وبذلك، نُنشئ مؤجَّلًا كما يلي:

(define (delay-it exp env)
  (list 'thunk exp env))
(define (thunk? obj) (tagged-list? obj 'thunk))
(define (thunk-exp thunk) (cadr thunk))
(define (thunk-env thunk) (caddr thunk))

في واقع الأمر، ما نريده لمفسّرنا ليس هذا تمامًا، بل مؤجّلاتٌ حُفِظت نتائجها. وحين يُجبَر مؤجَّلٌ، فإنّا سنُحوّله إلى مؤجَّلٍ مُقيَّمٍ باستبدال التعبير المخزّون بقيمته وتغيير وسم thunk حتّى يمكن التعرّف عليه بوصفه مُقيَّمًا بالفعل.[37]

(define (evaluated-thunk? obj)
  (tagged-list? obj 'evaluated-thunk))

(define (thunk-value evaluated-thunk) 
  (cadr evaluated-thunk))

(define (force-it obj)
  (cond ((thunk? obj)
         (let ((result
                (actual-value 
                 (thunk-exp obj)
                 (thunk-env obj))))
           (set-car! obj 'evaluated-thunk)
           ;; replace exp with its value:
           (set-car! (cdr obj) result) 
           ;; forget unneeded env:
           (set-cdr! (cdr obj) '()) 
           result))
        ((evaluated-thunk? obj)
         (thunk-value obj))
        (else obj)))

لاحِظ أنّ الإجراء delay-it ذاته يعمل مع حفظ النتائج (memoization) ودونه.

التمرين 4.27: لنفترض أنّنا أدخلنا التعريفات الآتية إلى المقيّم الكسول:

(define count 0)
(define (id x) (set! count (+ count 1)) x)

أعطِ القيم الناقصة في تسلسل التفاعلات الآتي، ووضّح إجاباتك.[38]

(define w (id (id 10)))

;;; L-Eval input:
count

;;; L-Eval value:
⟨response⟩

;;; L-Eval input:
w

;;; L-Eval value:
⟨response⟩

;;; L-Eval input:
count

;;; L-Eval value:
⟨response⟩

التمرين 4.28: يستخدم eval الإجراء actual-value بدلًا من eval لتقييم المشغّل قبل تمريره إلى apply، بغية إجبار قيمة المشغّل. أعطِ مثالًا يُظهر الحاجة إلى هذا الإجبار.

التمرين 4.29: اعرض برنامجًا يتوقّع المرء أن يعمل أبطأَ كثيرًا دون حفظ النتائج مقارنةً به معه. وفكّر أيضًا في التفاعل الآتي، حيث الإجراء id معرَّفٌ كما في التمرين 4.27 وcount يبدأ من 0:

(define (square x) (* x x))

;;; L-Eval input:
(square (id 10))

;;; L-Eval value:
⟨response⟩

;;; L-Eval input:
count

;;; L-Eval value:
⟨response⟩

أعطِ الإجابتين في الحالتين: حين يحفظ المقيّم النتائج وحين لا يحفظها.

التمرين 4.30: سي دي. فيكت، وهو مبرمج C سابق، قلقٌ من أن بعض التأثيرات الجانبيّة قد لا تحدث أبدًا، لأنّ المقيّم الكسول لا يُجبر التعابير في تسلسل. فإنّ قيمة تعبيرٍ في تسلسل غير الأخير لا تُستخدم (والتعبير موجودٌ هناك لأجل أثره فحسب، كالإحلال في متغيّر أو الطباعة)، فلا يمكن أن يكون هناك استخدامٌ لاحقٌ لتلك القيمة (كتمريرها معطىً إلى إجراء أوّليّ، مثلًا) يُسبّب إجبارها. ولهذا يظنّ سي أنّنا حين نُقيّم التسلسلات، ينبغي أن نُجبر جميع التعابير في التسلسل عدا الأخير. وهو يقترح تعديل eval-sequence الوارد في 4.1.1 ليستخدم actual-value بدلًا من eval:

(define (eval-sequence exps env)
  (cond ((last-exp? exps) 
         (eval (first-exp exps) env))
        (else 
         (actual-value (first-exp exps) 
                       env)
         (eval-sequence (rest-exps exps) 
                        env))))

يرى بن بِتدل أنّ سي مخطئ. وهو يُري سي الإجراء for-each الموصوف في التمرين 2.23، الذي يُعطي مثالًا مهمًّا على تسلسلٍ ذي تأثيراتٍ جانبيّة:

(define (for-each proc items)
  (if (null? items)
      'done
      (begin (proc (car items))
             (for-each proc 
                       (cdr items)))))

وهو يدّعي أنّ المقيّم الوارد في النصّ (مع eval-sequence الأصليّ) يتعامل مع هذا تعاملًا صحيحًا:

;;; L-Eval input:
(for-each
 (lambda (x) (newline) (display x))
 (list 57 321 88))
57
321
88

;;; L-Eval value:
done

وضّح لماذا بن مُحقّ في ما يقولُه عن سلوك for-each.

يتّفق سي مع أنّ بن مُحقّ في مثال for-each، لكنّه يقول إنّ ذلك ليس نوع البرنامج الذي كان يفكّر فيه حين اقترح تغييره على eval-sequence. وهو يُعرِّف الإجراءين الآتيين في المقيّم الكسول:

(define (p1 x)
  (set! x (cons x '(2))) x)

(define (p2 x)
  (define (p e) e x)
  (p (set! x (cons x '(2)))))

ما قيمتا (p1 1) و(p2 1) مع eval-sequence الأصليّ؟ وما كانت القيمتان ستكونان مع التغيير الذي اقترحه سي على eval-sequence؟

ويُشير سي أيضًا إلى أنّ تغيير eval-sequence على النحو الذي يقترحه لا يؤثّر في سلوك المثال الوارد في الجزء (أ). فوضّح لماذا هذا صحيح. وكيف تظنّ أنّ التسلسلات ينبغي أن تُعالَج في المقيّم الكسول؟ فأيّّ نهجٍ ترتاح له: نهج سي، أم النهج الوارد في النصّ، أم نهجٌ آخر؟

التمرين 4.31: النهج المتّخذ في هذا القسم غير مريحٍ إلى حدّ ما، لأنّه يُحدث تغييرًا غير متوافق في Scheme. وقد يكون من الأجمل تنفيذ التقييم الكسول بوصفه توسعةً متوافقة صعودًا (upward-compatible extension)، أي بحيث تعمل برامج Scheme العاديّة كما كانت تعمل سابقًا. ويمكننا فعل ذلك بتوسيع صياغة تصريحات الإجراءات للسماح للمستخدم بالتحكّم في تأخير الوسائط أو عدمه. وبينما نحن في ذلك، فلا بأس من أن نمنح المستخدم أيضًا الخيار بين التأخير مع حفظ النتائج ودونه. فمثلًا، التعريف

(define (f a (b lazy) c (d lazy-memo))
  …)

سيُعرِّف f بأنّه إجراءٌ من أربعة وسائط، حيث يُقيّم الوسيط الأوّل والثالث حين يُستدعى الإجراء، ويُؤخَّر الوسيط الثاني، ويُؤخَّر الوسيط الرابع ويُحفَظ نتيجته. وبذلك، ستُنتج تعاريف الإجراءات العاديّة السلوك ذاته الذي تُنتجه برامج Scheme العاديّة، بينما ستُنتج إضافة تصريح lazy-memo إلى كلّ وسيطٍ من وسائط كلّ إجراءٍ مركّبٍ سلوك المقيّم الكسول المعرَّف في هذا القسم. وصمّم ونفّذ التغييرات المطلوبة لإنتاج توسعةٍ كهذه على Scheme. وسيلزمك تنفيذ إجراءات صياغةٍ جديدةٍ للتعامل مع الصياغة الجديدة لـdefine. وينبغي لك أيضًا أن تُرتّب لما يجعل eval أو apply يحدّد متى تُؤخَّر الوسائط، ويكون إجبارها أو تأخيرها وفقًا لذلك، كما ينبغي أن تُرتّب لما يجعل الإجبار يحفظ النتائج أو لا، بحسب الحالة.

4.2.3 المجاري بوصفها قوائم كسولة

في 3.5.1، أظهرنا كيف نُنفّذ المجاري بوصفها قوائم مؤخّرة. وقد أدخلنا صيغتين خاصّتين هما delay وcons-stream، وتسمحان لنا ببناء «وعد» بحساب الـcdr لمجرى، دون الوفاء بذلك الوعد فعلًا إلّا لاحقًا. وكان بإمكاننا استخدام هذه التقنيّة العامّة المتمثّلة في إدخال صيغٍ خاصّةٍ كلّما احتجنا إلى مزيدٍ من التحكّم في عمليّة التقييم، لكنّ ذلك عسير. فأوّلًا، الصيغة الخاصّة ليست كائنًا من الرتبة الأولى (first-class) كالإجراء، فلا نستطيع استخدامها مع الإجراءات من الرتبة العليا.[39] وفضلًا عن ذلك، فقد أُجبرنا على إنشاء المجاري كنوعٍ جديدٍ من كائنات البيانات شبيهٍ بالقوائم غير مطابقٍ لها، وهذا استدعى منّا إعادة تنفيذ عمليّات قائمةٍ عاديّةٍ كثيرة (map، وappend، وما شابه) للاستخدام مع المجاري.

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

(define (cons x y) (lambda (m) (m x y)))
(define (car z) (z (lambda (p q) p)))
(define (cdr z) (z (lambda (p q) q)))

وبصدد هذه العمليّات الأساسيّة، فإنّ التعريفات القياسيّة لعمليّات القوائم ستعمل مع القوائم اللاّنهائيّة (المجاري) كما تعمل مع المنتهيّة، ويمكن تنفيذ عمليّات المجاري بوصفها عمليّات قائمة. وهاك بعض الأمثلة:

(define (list-ref items n)
  (if (= n 0)
      (car items)
      (list-ref (cdr items) (- n 1))))

(define (map proc items)
  (if (null? items)
      '()
      (cons (proc (car items))
            (map proc (cdr items)))))

(define (scale-list items factor)
  (map (lambda (x) (* x factor))
       items))

(define (add-lists list1 list2)
  (cond ((null? list1) list2)
        ((null? list2) list1)
        (else (cons (+ (car list1) 
                       (car list2))
                    (add-lists
                     (cdr list1) 
                     (cdr list2))))))

(define ones (cons 1 ones))

(define integers 
  (cons 1 (add-lists ones integers)))

;;; L-Eval input:
(list-ref integers 17)

;;; L-Eval value:
18

لاحِظ أنّ هذه القوائم الكسولة أكسلُ حتّى من مجاري الفصل 3: فالـcar للقائمة، كما الـcdr، مؤخَّر.[41] وفي الحقيقة، فإنّ مجرّد الوصول إلى car أو cdr زوجٍ كسولٍ لا يستلزم إجبار قيمة عنصر القائمة. فلن تُجبَر القيمة إلّا حين تحتاج إليها فعلًا—مثلًا للاستخدام معطىً إلى أوّليّة، أو لتُطبَع جوابًا.

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

(define (integral integrand initial-value dt)
  (define int
    (cons initial-value
          (add-lists (scale-list integrand dt) 
                     int)))
  int)

(define (solve f y0 dt)
  (define y (integral dy y0 dt))
  (define dy (map f y))
  y)

;;; L-Eval input:
(list-ref (solve (lambda (x) x) 1 0.001) 1000)

;;; L-Eval value:
2.716924

التمرين 4.32: أعطِ بعض الأمثلة التي توضّح الفرق بين مجاري الفصل 3 والقوائم الكسولة «الأكسل» الموصوفة في هذا القسم. فكيف يمكنك استثمار هذا الكسل الإضافيّ؟

التمرين 4.33: يختبر بن بِتدل تنفيذ القائمة الكسولة المعطى أعلاه بتقييم التعبير

(car '(a b c))

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

التمرين 4.34: عدّل حلقة المُشغّل للمقيّم بحيث تُطبَع الأزواج الكسولة والقوائم الكسولة على نحوٍ معقول. (فماذا ستفعل حيال القوائم اللاّنهائيّة؟) وقد يلزمك أيضًا تعديل تمثيل الأزواج الكسولة حتّى يتمكّن المقيّم من التعرّف عليها بغرض طبعها.

4.3 تنويعات على Scheme — الحوسبة غير الحتميّة

في هذا القسم، نوسّع مقيّم Scheme لدعم نموذج برمجةٍ يُسمّى الحوسبة غير الحتميّة (nondeterministic computing)، وذلك ببناء تسهيلٍ داخل المقيّم لدعم البحث الآليّ. وهذا تغييرٌ في اللغة أعمقُ بكثيرٍ من إدخال التقييم الكسول في 4.2.

والحوسبة غير الحتميّة، كمعالجة المجاري، مفيدةٌ في تطبيقات «التوليد والاختبار». فتأمّل مهمّة الانطلاق بقائمتَي أعدادٍ صحيحةٍ موجبةٍ وإيجاد زوجٍ من الأعداد—واحدٍ من القائمة الأولى وآخر من القائمة الثانية—مجموعه أوّليّ. وقد رأينا كيف نتعامل مع هذا باستخدام عمليّات التسلسلات المنتهيّة في 2.2.3 وباستخدام المجاري اللاّنهائيّة في 3.5.3. وكان نهجنا توليد تسلسل جميع الأزواج الممكنة وترشيحها لاختيار الأزواج التي مجموعها أوّليّ. وسواءٌ كنّا نُولّد تسلسل الأزواج كلّه أوّلًا كما في الفصل 2، أو نُشابك التوليد والترشيح كما في الفصل 3، فذلك لا يمسّ الصورة الجوهريّة لكيفيّة تنظيم الاحتساب.

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

(define (prime-sum-pair list1 list2)
  (let ((a (an-element-of list1))
        (b (an-element-of list2)))
    (require (prime? (+ a b)))
    (list a b)))

قد يبدو كأنّ هذا الإجراء لا يفعل سوى إعادة صياغة المسألة، بدلًا من تحديد طريقةٍ لحلّها. ومع ذلك، فهذا برنامجٌ غير حتميّ مشروع.[42]

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

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

ويُسمّى مقيّم البرامج غير الحتميّة المنفّذ أدناه مقيّم amb لأنّه مبنيٌّ على صيغةٍ خاصّةٍ جديدةٍ تُسمّى amb. ويمكننا كتابة تعريف prime-sum-pair المعطى أعلاه في حلقة مُشغّل مقيّم amb (مع تعاريف prime? وan-element-of وrequire) وتشغيل الإجراء على النحو الآتي:

;;; Amb-Eval input:
(prime-sum-pair '(1 3 5 8) '(20 35 110))

;;; Starting a new problem
;;; Amb-Eval value:
(3 20)

والقيمة المُعادة تحقّقت بعد أن اختار المقيّم عناصر من كلٍّ من القائمتين تكرارًا، حتّى أُجري اختيارٌ ناجح.

يُقدّم القسم 4.3.1 الصيغة amb ويشرح كيف تدعم عدم الحتميّة من خلال آليّة البحث الآليّ في المقيّم. ويعرض القسم 4.3.2 أمثلةً على البرامج غير الحتميّة، ويعطي القسم 4.3.3 تفاصيل كيفيّة تنفيذ مقيّم amb بتعديل مقيّم Scheme العاديّ.

4.3.1 Amb والبحث

لتمديد Scheme لدعم عدم الحتميّة، نُدخل صيغةً خاصّةً جديدةً تُسمّى amb.[43] فالتعبير

(amb ⟨e₁⟩ ⟨e₂⟩ … ⟨eₙ⟩)

يُعيد قيمة أحد التعابير الـnn ⟨ei⟩⟨ e_{i} ⟩ «على نحوٍ غامض». فمثلًا، التعبير

(list (amb 1 2 3) (amb 'a 'b))

قد يكون له ستّ قيم ممكنة:

(1 a) (1 b) (2 a) (2 b) (3 a) (3 b)

إنّ Amb مع اختيارٍ واحدٍ يُنتج قيمةً عاديّةً (وحيدة).

إنّ Amb دون اختيارات—التعبير (amb)—تعبيرٌ لا قيمة مقبولة له. وتشغيليًّا، يمكننا التفكير في (amb) كتعبيرٍ يُسبّب «فشل» الاحتساب عند تقييمه: فيُنقطع الاحتساب ولا تُنتَج أيّ قيمة. وباستخدام هذه الفكرة، نستطيع التعبير عن المقتضى القائل إنّ تعبير مُسَيِّمٍ معيَّن p لا بدّ أن يكون صحيحًا، على النحو الآتي:

(define (require p)
  (if (not p) (amb)))

وباستخدام amb وrequire، نستطيع تنفيذ إجراء an-element-of المستخدم أعلاه:

(define (an-element-of items)
  (require (not (null? items)))
  (amb (car items) 
       (an-element-of (cdr items))))

يفشل an-element-of إذا كانت القائمة فارغة. وإلّا فهو يُعيد إمّا أوّل عنصر من القائمة وإمّا عنصرًا مختارًا من بقيّة القائمة، على نحوٍ غامض.

ويمكننا أيضًا التعبير عن مدىاتٍ لاّنهائيّةٍ من الخيارات. فالإجراء الآتي يُعيد مُحتمَلًا أيّ عددٍ صحيحٍ أكبر أو مساوٍ لعددٍ معطى nn :

(define (an-integer-starting-from n)
  (amb n (an-integer-starting-from (+ n 1))))

وهذا شبيهٌ بإجراء المجرى integers-starting-from الموصوف في 3.5.2، لكن بفرقٍ مهمّ: فإجراء المجرى يُعيد كائنًا يمثّل تسلسل جميع الأعداد الصحيحة التي تبدأ من nn ، بينما يُعيد إجراء amb عددًا صحيحًا واحدًا.[44]

وبصورةٍ مجرّدة، يمكننا أن نتخيّل أنّ تقييم تعبير amb يُسبّب انقسام الزمن إلى فروع، حيث يستمرّ الاحتساب في كلّ فرعٍ بإحدى القيم الممكنة للتعبير. ونقول إنّ amb يُمثّل نقطة اختيارٍ غير حتميّة (nondeterministic choice point). فلو كانت لدينا آلةٌ بعددٍ كافٍ من المعالجات القابلة للتخصيص ديناميكيًّا، لأمكننا تنفيذ البحث تنفيذًا مباشرًا. فيسير التنفيذ كما في آلةٍ تسلسليّةٍ، حتّى يُصادَف تعبير amb، وعند تلك النقطة تُخصَّص معالجاتٌ إضافيّةٌ وتُهيّأ لمواصلة جميع التنفيذات المتوازية التي يستلزمها الاختيار. ويسير كلّ معالجٍ تسلسليًّا كأنّه الاختيار الوحيد، حتّى إمّا أن ينتهي بمصادفة فشل، أو أن يتفرّع أكثر، أو أن يُكمل.[45]

ومن جهةٍ أخرى، فإن كانت لدينا آلةٌ لا تستطيع تنفيذ إلّا عمليّةً واحدة (أو عدّة عمليّاتٍ متزامنةٍ قليلة)، فلا بدّ من النظر في البدائل تسلسليًّا. ويمكن للمرء أن يتخيّل تعديل مقيّم ليتخيّر فرعًا عشوائيًّا ليتبعه كلّما صادف نقطة اختيار. غير أنّ الاختيار العشوائيّ قد يؤدّي بسهولةٍ إلى قيمٍ فاشلة. وقد نجرّب تشغيل المقيّم مرّةً بعد أخرى، باختياراتٍ عشوائيّةٍ وعلى أمل العثور على قيمةٍ غير فاشلة، لكنّ الأفضلَ البحث المنهجيّ (systematic search) في جميع مسارات التنفيذ الممكنة. ومقيّم amb الذي سنطوّره ونعمل به في هذا القسم يُنفّذ بحثًا منهجيًّا على النحو الآتي: حين يصادف المقيّم تطبيقًا لـamb، فهو يختار أوّلًا البديل الأوّل. وهذا الاختيار قد يؤدّي بذاته إلى اختيارٍ لاحق. وسيختار المقيّم دائمًا البديل الأوّل في كلّ نقطة اختيار. فإن أفضى اختيارٌ إلى فشل، فإنّ المقيّم يتراجع رجعيًّا (backtracks) تلقائيًّا[46] إلى أحدث نقطة اختيار ويجرّب البديل التالي. فإن نفدت البدائل في أيّ نقطة اختيار، رجع المقيّم إلى نقطة الاختيار السابقة واستأنف من هناك. وتؤدّي هذه العمليّة إلى استراتيجيّة بحثٍ تُعرف بـالبحث بالعمق أوّلًا (depth-first search) أو التتبّع الرجعيّ الزمنيّ (chronological backtracking).[47]

حلقة المُشغّل

ولحلقة مُشغّل مقيّم amb بعض الخصائص غير المعتادة. فهي تقرأ تعبيرًا وتطبع قيمة أوّل تنفيذٍ غير فاشل، كما في مثال prime-sum-pair المعروض أعلاه. فإن أردنا رؤية قيمة التنفيذ الناجح التالي، فبوسعنا أن نطلب من المفسّر أن يتراجع رجعيًّا ويحاول توليد تنفيذٍ ثانٍ غير فاشل. ويُشار إلى ذلك بكتابة الرمز try-again. فلو أُعطي أيّ تعبيرٍ غير try-again، سيبدأ المفسّر مسألةً جديدةً، مُتخلّيًا عن البدائل غير المستكشفة في المسألة السابقة. وهاك مثالًا على تفاعل:

;;; Amb-Eval input:
(prime-sum-pair '(1 3 5 8) '(20 35 110))

;;; Starting a new problem
;;; Amb-Eval value:
(3 20)

;;; Amb-Eval input:
try-again

;;; Amb-Eval value:
(3 110)

;;; Amb-Eval input:
try-again

;;; Amb-Eval value:
(8 35)

;;; Amb-Eval input:
try-again

;;; There are no more values of
(prime-sum-pair 
 (quote (1 3 5 8)) 
 (quote (20 35 110)))

;;; Amb-Eval input:
(prime-sum-pair '(19 27 30) '(11 36 58))

;;; Starting a new problem
;;; Amb-Eval value:
(30 11)

التمرين 4.35: اكتب إجراءً an-integer-between يُعيد عددًا صحيحًا بين حدّين معطيين. ويمكن استخدام ذلك لتنفيذ إجراءٍ يجد الثلاثيات الفيثاغورسيّة، أي ثلاثيات من الأعداد الصحيحة (i,j,k)( i , j , k ) بين الحدّين المعطيين بحيثُ i≤ji ≤ j وi2+j2=k2i^{2} + j^{2} = k^{2} ، على النحو الآتي:

(define (a-pythagorean-triple-between low high)
  (let ((i (an-integer-between low high)))
    (let ((j (an-integer-between i high)))
      (let ((k (an-integer-between j high)))
        (require (= (+ (* i i) (* j j)) 
                    (* k k)))
        (list i j k)))))

التمرين 4.36: ناقش التمرين 3.69 كيف تُولَّد مجرة جميع الثلاثيات الفيثاغورسيّة، دون حدٍّ أعلى لحجم الأعداد الصحيحة التي تُبحث. فوضّح لماذا لا يصلح مجرّد استبدال an-integer-between بـan-integer-starting-from في الإجراء الوارد في التمرين 4.35 سبيلًا مناسبًا لتوليد ثلاثياتٍ فيثاغورسيّةٍ اعتراضيّة. واكتب إجراءً يُنجز ذلك فعلًا. (أي اكتب إجراءً يجعل تكرار كتابة try-again يُولّد من حيث المبدأ جميع الثلاثيات الفيثاغورسيّة في النهاية.)

التمرين 4.37: يدّعي بن بِتدل أنّ الطريقة الآتية لتوليد الثلاثيات الفيثاغورسيّة أكفأُ من تلك الواردة في التمرين 4.35. فأهل مُحقّ؟ (تلميح: فكّر في عدد الاحتمالات التي يجب استكشافها.)

(define (a-pythagorean-triple-between low high)
  (let ((i (an-integer-between low high))
        (hsq (* high high)))
    (let ((j (an-integer-between i high)))
      (let ((ksq (+ (* i i) (* j j))))
        (require (>= hsq ksq))
        (let ((k (sqrt ksq)))
          (require (integer? k))
          (list i j k))))))

4.3.2 أمثلة على البرامج غير الحتميّة

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

ألغاز منطقيّة

اللغز الآتي (مأخوذٌ من دينسمان 1968) يمثّل نموذجًا لصنفٍ كبيرٍ من الألغاز المنطقيّة البسيطة:

يسكن بيكر، وكوبر، وفلتشر، وميلر، وسميث في طوابق مختلفةٍ من عمارةٍ سكنيّةٍ لا تحتوي إلّا خمسة طوابق. ولا يسكن بيكر في الطابق العلويّ. ولا يسكن كوبر في الطابق السفليّ. ولا يسكن فلتشر في الطابق العلويّ ولا في السفليّ. ويسكن ميلر في طابقٍ أعلى من طابق كوبر. ولا يسكن سميث في طابقٍ مجاور لطابق فلتشر. ولا يسكن فلتشر في طابقٍ مجاور لطابق كوبر. فأين يسكن كلٌّ منهم؟

ويمكننا تحديد من يسكن في كلّ طابقٍ على سبيلٍ مباشرٍ بحصر جميع الاحتمالات وفرض القيود المعطاة:[48]

(define (multiple-dwelling)
  (let ((baker (amb 1 2 3 4 5))
        (cooper (amb 1 2 3 4 5))
        (fletcher (amb 1 2 3 4 5))
        (miller (amb 1 2 3 4 5))
        (smith (amb 1 2 3 4 5)))
    (require
     (distinct? (list baker cooper fletcher 
                      miller smith)))
    (require (not (= baker 5)))
    (require (not (= cooper 1)))
    (require (not (= fletcher 5)))
    (require (not (= fletcher 1)))
    (require (> miller cooper))
    (require
     (not (= (abs (- smith fletcher)) 1)))
    (require 
     (not (= (abs (- fletcher cooper)) 1)))
    (list (list 'baker baker)
          (list 'cooper cooper)
          (list 'fletcher fletcher)
          (list 'miller miller)
          (list 'smith smith))))

وإنّ تقييم التعبير (multiple-dwelling) يُنتج النتيجة

((baker 3) (cooper 2) (fletcher 4)
 (miller 5) (smith 1))

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

التمرين 4.38: عدّل إجراء multiple-dwelling لإسقاط المقتضى القائل إنّ سميث وفلتشر لا يسكنان في طابقين متجاورين. فكم حلًّا يوجد لهذا اللغز المعدّل؟

التمرين 4.39: فهل يؤثّر ترتيب القيود في إجراء multiple-dwelling في الجواب؟ وهل يؤثّر في الوقت اللازم لإيجاد جواب؟ فإن كنت تظنّ أنّه يؤثّر، فأظهِر برنامجًا أسرع متحصّلٌ من المعطى بإعادة ترتيب القيود. وإن كنت تظنّ أنّه لا يؤثّر، فحاجج قضيّتك.

التمرين 4.40: في مسألة العمارة السكنيّة، فكم مجموعةً من الإحلالات للأشخاص في الطوابق، قبل المقتضى القاضي بأن تكون إحلالات الطوابق متمايزةً وبعده؟ إنّ توليد جميع الإحلالات الممكنة للأشخاص في الطوابق وترك الأمر بعد ذلك للتتبّع الرجعيّ ليستنفدها أمرٌ غير كفؤٍ على الإطلاق. فمثلًا، أكثر القيود يعتمد على متغيّرٍ أو متغيّرَي شخص-طابق فحسب، وبوسعه بالتالي أن يُفرض قبل أن تُختار طوابقٌ لجميع الأشخاص. واكتب وأظهِر إجراءً غير حتميًّ أكفأُ بكثيرٍ يحلّ هذه المسألة بناءً على توليد تلك الاحتمالات غير المستنفدة بحكم قيودٍ سابقةٍ فحسب. (تلميح: سيستلزم هذا عشًّا من تعابير let.)

التمرين 4.41: اكتب برنامج Scheme عاديًّا لحلّ لغز العمارة السكنيّة.

التمرين 4.42: حُلّ لغز «الكاذبات» الآتي (مأخوذٌ من فيليبس 1934): جلست خمسُ تلميذاتٍ لامتحانٍ. وقد أظهر والدا هنّ - بحسب ما ظنّن - اهتمامًا مفرطًا بالنتيجة. ولذلك اتّفقن على أنّ كلَّ واحدةٍ منهنّ، حين تكتب إلى أهلها عن الامتحان، تصدر عبارةً صحيحةً واحدةً وعبارةً غير صحيحةٍ واحدة. والآتي هي المقاطع ذات الصلة من رسائلهنّ: بيتي: «كانت كيتي الثانية في الامتحان. وأنا لم أكن سوى الثالثة.»

إثيل: «سيسرّك أن تسمعي أنّي كنت الأولى. وكانت جوان الثانية.» جوان: «كنتُ الثالثة، وإثيل المسكينة كانت الأخيرة.» كيتي: «كنتُ الثانية. وكانت ماري الرابعة فقط.» ماري: «كنتُ الرابعة. وقد نالت بيتي الصدارة.»

فما هو الترتيب الذي جاءت به التلميذات الخمس فعلًا؟

التمرين 4.43: استخدم مقيّم amb لحلّ اللغز الآتي:[49] يملك والد ماري آن مور يختًا، وكذلك يفعل كلٌّ من أصحابه الأربعة: العقيد داونينغ، والسيد هال، والسير بارناكِل هود، والدكتور باركر. ولكلٍّ من الخمسة أيضًا ابنةٌ واحدة، وقد سمّى كلٌّ منهم يخته باسم ابنةٍ لأحد الآخرين. فاليخت المسمّى Gabrielle هو يخت السير بارناكِل، والسيد مور يملك Lorna؛ أمّا السيد هال فيملك Rosalind. واليخت Melissa، المملوك للعقيد داونينغ، مسمّىً باسم ابنة السير بارناكِل. وأبو Gabrielle يملك اليخت المسمّى باسم ابنة الدكتور باركر. فمن هو أبو Lorna؟

حاول كتابة البرنامج بحيث يعمل بكفاءةٍ (انظر التمرين 4.40). وحدّد أيضًا عدد الحلول الموجودة إذا لم يُقَل لنا إنّ اسم عائلة ماري آن هو مور.

التمرين 4.44: لقد وصف التمرين 2.42 «لغز الملكات الثماني»، وهو وضع ملكاتٍ على رقعة شطرنجٍ بحيث لا يهاجم بعضهنّ بعضًا. اكتب برنامجًا غير حتميٍّ لحلّ هذا اللغز.

تحليل اللغة الطبيعيّة

تبدأ البرامج المصمّمة لقبول اللغة الطبيعيّة كدخل (input) عادةً بمحاولة تحليل (parse) الدخل، أي مطابقته مع بنيةٍ نحويّةٍ ما. فمثلًا، قد نحاول التعرّف على جملٍ بسيطةٍ تتكوّن من أداة تعريفٍ تليها اسمٌ يليه فعل، مثل «القطّ يأكل». ولإنجاز تحليلٍ كهذا، ينبغي أن نكون قادرين على تحديد أقسام كلام الكلمات المفردة. ويستطيع المرء البدء ببعض القوائم التي تصنّف كلماتٍ متنوّعة:[50]

(define nouns 
  '(noun student professor cat class))

(define verbs 
  '(verb studies lectures eats sleeps))

(define articles '(article the a))

ونحتاج أيضًا إلى نحوٍ (grammar)، أي مجموعةٍ من القواعد تصف كيف تُركّب العناصر النحويّة من عناصر أبسط. فقد ينصّ نحوٌ بسيطٌ جدًّا على أنّ الجملة تتكوّن دائمًا من قطعتين - عبارةٌ اسميّةٌ (noun phrase) تليها فعل - وعلى أنّ العبارة الاسميّة تتكوّن من أداة تعريفٍ تليها اسمٌ. وبهذا النحو، تُحلَّل الجملة «القطّ يأكل» على النحو الآتي:

(sentence
 (noun-phrase (article the) (noun cat))
 (verb eats))

ويستطيع المرء أن يولّد تحليلًا كهذا ببرنامجٍ بسيطٍ له إجراءاتٌ منفصلةٌ لكلّ قاعدةٍ من القواعد النحويّة. فلتحليل جملةٍ، نحدّد قطعتَيها المكوّنتين ونعيد قائمةً بهذين العنصرين، موسومةً بالرمز sentence:

(define (parse-sentence)
  (list 'sentence
         (parse-noun-phrase)
         (parse-word verbs)))

وتُحلَّل العبارة الاسميّة، بالمثل، بإيجاد أداة تعريفٍ تليها اسمٌ:

(define (parse-noun-phrase)
  (list 'noun-phrase
        (parse-word articles)
        (parse-word nouns)))

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

(define (parse-word word-list)
  (require (not (null? *unparsed*)))
  (require (memq (car *unparsed*) 
                 (cdr word-list)))
  (let ((found-word (car *unparsed*)))
    (set! *unparsed* (cdr *unparsed*))
    (list (car word-list) found-word)))

ولابتداء التحليل، كلّ ما علينا فعله هو ضبط *unparsed* ليكون الدخل بأكمله، ومحاولة تحليل جملةٍ، والتحقّق من أنّ لا شيء بقيَ:

(define *unparsed* '())
(define (parse input)
  (set! *unparsed* input)
  (let ((sent (parse-sentence)))
    (require (null? *unparsed*))
    sent))

ويستطيع المرء الآن تجربة المحلّل والتحقّق من أنّه يعمل من أجل جملة اختبارنا البسيطة:

;;; Amb-Eval input:
(parse '(the cat eats))

;;; Starting a new problem
;;; Amb-Eval value:
(sentence 
 (noun-phrase (article the) (noun cat))
 (verb eats))

ومقيّم amb مفيدٌ هنا لأنّه ملائمٌ للتعبير عن قيود التحليل بمعونة require. أمّا البحث الآليّ والتتبّع الرجعيّ فيُجدِيان ثمارهما حقًّا حين ننظر في قواعد نحويّةٍ أكثر تعقيدًا، حيث تكون هناك خياراتٌ لكيفيّة تفكيك الوحدات.

فلنُضِف إلى نحوّنا قائمةً بحروف الجرّ:

(define prepositions 
  '(prep for to in by with))

ولنُعرّف عبارة حرف الجرّ (prepositional phrase) (مثلًا «للقطّ») على أنّها حرف جرٍّ يتبعه عبارةٌ اسميّةٌ:

(define (parse-prepositional-phrase)
  (list 'prep-phrase
        (parse-word prepositions)
        (parse-noun-phrase)))

ويسعنا الآن تعريف الجملة على أنّها عبارةٌ اسميّةٌ تليها عبارةٌ فعليّة (verb phrase)، حيث يمكن أن تكون العبارة الفعليّة إمّا فعلًا وإمّا عبارةً فعليّةً موسّعةً بعبارة حرف جرٍّ:[52]

(define (parse-sentence)
  (list 'sentence
         (parse-noun-phrase)
         (parse-verb-phrase)))

(define (parse-verb-phrase)
  (define (maybe-extend verb-phrase)
    (amb 
     verb-phrase
     (maybe-extend 
      (list 'verb-phrase
            verb-phrase
            (parse-prepositional-phrase)))))
  (maybe-extend (parse-word verbs)))

وبمناسبة الحديث، يستطيع المرء أيضًا أن يُفصّل تعريف العبارات الاسميّة للسماح بأمورٍ مثل «قطةٍ في الصفّ». فما كنّا نسمّيه عبارةً اسميّةً، سنسمّيه الآن عبارةً اسميّةً بسيطة، وستكون العبارة الاسميّة إمّا عبارةً اسميّةً بسيطةً وإمّا عبارةً اسميّةً موسّعةً بعبارة حرف جرٍّ:

(define (parse-simple-noun-phrase)
  (list 'simple-noun-phrase
        (parse-word articles)
        (parse-word nouns)))

(define (parse-noun-phrase)
  (define (maybe-extend noun-phrase)
    (amb 
     noun-phrase
     (maybe-extend 
      (list 'noun-phrase
            noun-phrase
            (parse-prepositional-phrase)))))
  (maybe-extend (parse-simple-noun-phrase)))

ويسمح لنا نحوّنا الجديد بتحليل جملٍ أكثر تعقيدًا. فمثلًا

(parse '(the student with the cat 
         sleeps in the class))

يُنتج

(sentence
 (noun-phrase
  (simple-noun-phrase (article the) 
                      (noun student))
  (prep-phrase (prep with)
               (simple-noun-phrase
                (article the)
                (noun cat))))
 (verb-phrase
  (verb sleeps)
  (prep-phrase (prep in)
               (simple-noun-phrase
                (article the)
                (noun class)))))

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

(parse '(the professor lectures to 
         the student with the cat))

يُنتج

(sentence
 (simple-noun-phrase (article the) 
                     (noun professor))
 (verb-phrase
  (verb-phrase
   (verb lectures)
   (prep-phrase (prep to)
                (simple-noun-phrase
                 (article the) 
                 (noun student))))
  (prep-phrase (prep with)
               (simple-noun-phrase
                (article the) 
                (noun cat)))))

وطلب المحاولة مرّةً أخرى من المقيّم يُنتج

(sentence
 (simple-noun-phrase (article the) 
                     (noun professor))
 (verb-phrase (verb lectures)
              (prep-phrase 
               (prep to)
               (noun-phrase
                (simple-noun-phrase
                 (article the) 
                 (noun student))
                (prep-phrase 
                 (prep with)
                 (simple-noun-phrase
                  (article the) 
                  (noun cat)))))))

التمرين 4.45: بالنحو المعطى أعلاه، يمكن تحليل الجملة الآتية بخمس طرائق مختلفة: «الأستاذ يلقي محاضرةً للطالب في الصفّ مع القطّ». أعطِ التحليلات الخمس، واشرح الفروق في ظلال المعنى بينها.

التمرين 4.46: إنّ المقيّمين في 4.1 و4.2 لا يحدّدان الترتيب الذي تُقيَّم به العوامل. وسنرى أنّ مقيّم amb يقيّمها من اليسار إلى اليمين. فسرح لماذا لن يعمل برنامج التحليل لدينا لو قُيِّمت العوامل بترتيبٍ آخر.

التمرين 4.47: يقترح لويس ريزنر أنّهُ، إذ كانت العبارة الفعليّة إمّا فعلًا وإمّا عبارةً فعليّةً تليها عبارة حرف جرٍّ، فسيكون تعريف إجراء parse-verb-phrase أبسطَ كثيرًا على النحو الآتي (وبالمثل بالنسبة للعبارات الاسميّة):

(define (parse-verb-phrase)
  (amb (parse-word verbs)
       (list 
        'verb-phrase
        (parse-verb-phrase)
        (parse-prepositional-phrase))))

فهل يعمل هذا؟ وهل يتغيّر سلوك البرنامج إذا بدّلنا ترتيب التعابير في الـamb؟

التمرين 4.48: وسّع النحو المعطى أعلاه ليتعامل مع جملٍ أكثر تعقيدًا. فمثلًا، يستطيع المرء توسيع العبارات الاسميّة والعبارات الفعليّة لتشمل الصفات والظروف، أو التعامل مع الجمل المركّبة.[53]

التمرين 4.49: أليسا ب. هاكر أكثر اهتمامًا بتوليد جملٍ مثيرةٍ للاهتمامٍ من اهتمامها بتحليلها. وهي تستدلّ بأنّه بمجرّد تغيير إجراء parse-word بحيث يتجاهل «جملة الدخل» ويكون ناجحًا دائمًا ويولّد كلمةً مناسبةً بدلًا من ذلك، فسيمكننا استخدام البرامج التي بنيناها للتحليل لأداء التوليد بدلًا منه. نفّذ فكرة أليسا، وأظهِر أوّل نصف دستةٍ أو نحوه من الجمل المولّدة.[54]

4.3.3 تنفيذ مقيّم Amb

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

وسنبني مقيّم amb من أجل Scheme غير الحتميّة بتعديل المقيّم المُحلِّل الوارد في 4.1.7.[55] فكما في المقيّم المُحلِّل، يُنجَز تقييم التعبير بنداء إجراءٍ تنفيذيٍّ يُنتَج بتحليل ذلك التعبير. وسيكون الفرق بين تأويل Scheme العاديّة وتأويل Scheme غير الحتميّة كلَّه في الإجراءات التنفيذيّة.

الإجراءات التنفيذيّة وإجراءات الاستمرار

تذكّر أنّ الإجراءات التنفيذيّة في المقيّم العاديّ تأخذ معطىً واحدًا: بيئة التنفيذ. أمّا الإجراءات التنفيذيّة في مقيّم amb فتأخذ ثلاثة معطيات: البيئة، وإجراءين يُسمّيان استمرارين (continuation procedures). وسينتهي تقييم التعبير بنداء أحد هذين الاستمرارين: فإن أدّى التقييم إلى قيمة، دُعي استمرار النجاح (success continuation) بتلك القيمة؛ وإن أدّى التقييم إلى اكتشاف طريقٍ مسدود، دُعي استمرار الفشل (failure continuation). وبناء الاستمرارات الملائمة ونداؤها هي الآليّة التي يُنفّذ بها المقيّم غير الحتميّ التتبّع الرجعيّ.

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

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

ويُثار فشلٌ أثناء التقييم (أي يُدعى استمرار الفشل) حين يرفض برنامج المستخدم خطّ الهجوم الحالي رفضًا صريحًا (فمثلًا، قد يؤدّي نداء require إلى تنفيذ (amb)، وهو تعبيرٌ يفشل دائمًا - انظر 4.3.1). واستمرار الفشل المتاح عند تلك النقطة سيؤدّي إلى أن تختار أحدث نقطة اختيارٍ بديلًا آخر. فإن لم تكن هناك بدائل أخرى يُنظر فيها عند تلك النقطة، أُثير فشلٌ عند نقطة اختيارٍ سابقة، وهكذا. وتُستدعى استمرارات الفشل أيضًا من حلقة المُشغّل استجابةً لطلب try-again، لإيجاد قيمةٍ أخرى للتعبير.

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

وباختصار، تُبنى استمرارات الفشل بواسطة

  • تعابير amb - لتوفير آليّةٍ لإجراء اختياراتٍ بديلةٍ إذا أدّى الاختيار الحالي الذي أجراه تعبير amb إلى طريقٍ مسدود؛
  • مُشغّل المستوى الأعلى - لتوفير آليّةٍ للإبلاغ عن الفشل حين تنفد الاختيارات؛
  • الإحلالات - لاعتراض الفشل والتراجع عن الإحلالات أثناء التتبّع الرجعيّ.

ولا تُبدأ الفشل إلّا عند مصادفة طريقٍ مسدود. ويقع ذلك

  • إذا نفّذ برنامج المستخدم (amb)؛
  • إذا كتب المستخدم try-again عند مُشغّل المستوى الأعلى.

وتُدعى استمرارات الفشل أيضًا أثناء معالجة فشلٍ:

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

بنية المقيّم

إنّ إجراءات الصياغة وتمثيل البيانات الخاصّة بمقيّم amb، وكذلك إجراء analyze الأساسيّ، مطابقةٌ لتلك الواردة في مقيّم 4.1.7، إلّا في كوننا نحتاج إلى إجراءات صياغةٍ إضافيّةٍ للتعرّف على الصيغة الخاصّة amb:[56]

(define (amb? exp) (tagged-list? exp 'amb))
(define (amb-choices exp) (cdr exp))

وينبغي لنا أيضًا أن نُضيف إلى التوزيع في analyze بندًا يتعرّف على هذه الصيغة الخاصّة ويولّد إجراءً تنفيذيًّا ملائمًا:

((amb? exp) (analyze-amb exp))

وإجراء المستوى الأعلى ambeval (الشبيه بإصدار eval المعطى في 4.1.7) يحلّل التعبير المعطى ويطبّق إجراء التنفيذ الناتج على البيئة المعطاة، مع استمرارين معطيين:

(define (ambeval exp env succeed fail)
  ((analyze exp) env succeed fail))

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

(lambda (env succeed fail)
  ;; succeed is (lambda (value fail) …)
  ;; fail is (lambda () …)
  …)

فمثلًا، تنفيذ

(ambeval ⟨exp⟩
         the-global-environment
         (lambda (value fail) value)
         (lambda () 'failed))

سيحاول تقييم التعبير المعطى وسيعيد إمّا قيمة التعبير (إذا نجح التقييم) وإمّا الرمز failed (إذا فشل التقييم). أمّا نداء ambeval في حلقة المُشغّل المعروضة أدناه فيستخدم إجراءات استمرارٍ أعقدَ بكثيرٍ، تواصل الحلقة وتدعم طلب try-again.

ويَنشأ معظم تعقيد مقيّم amb من ميكانيكا تمرير الاستمرارات فيما تنداي الإجراءات التنفيذيّة بعضها بعضًا. وعند المرور على الشيفرة الآتية، ينبغي أن تقارن كلّ إجراءٍ تنفيذيٍّ بالإجراء المقابل له في المقيّم العاديّ المعطى في 4.1.7.

التعابير البسيطة

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

(define (analyze-self-evaluating exp)
  (lambda (env succeed fail)
    (succeed exp fail)))

(define (analyze-quoted exp)
  (let ((qval (text-of-quotation exp)))
    (lambda (env succeed fail)
      (succeed qval fail))))

(define (analyze-variable exp)
  (lambda (env succeed fail)
    (succeed (lookup-variable-value exp env)
             fail)))

(define (analyze-lambda exp)
  (let ((vars (lambda-parameters exp))
        (bproc (analyze-sequence 
                (lambda-body exp))))
    (lambda (env succeed fail)
      (succeed (make-procedure vars bproc env)
               fail))))

لاحِظ أنّ البحث عن قيمة متغيّرٍ «ينجح» دائمًا. فإن فشل lookup-variable-value في العثور على المتغيّر، فإنّه يُشير إلى خطأٍ، كالعادة. و«فشلٌ» كهذا يدلّ على علةٍ في البرنامج - إشارةٌ إلى متغيّرٍ غير مقيّد؛ وهو ليس دلالةً على وجوب تجربة اختيارٍ غير حتميٍّ آخر بدلًا من الاختيار المُجرَّب حاليًّا.

التعابير الشرطيّة والتسلسلات

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

(define (analyze-if exp)
  (let ((pproc (analyze (if-predicate exp)))
        (cproc (analyze (if-consequent exp)))
        (aproc (analyze (if-alternative exp))))
    (lambda (env succeed fail)
      (pproc env
             ;; success continuation for evaluating
             ;; the predicate to obtain pred-value
             (lambda (pred-value fail2)
               (if (true? pred-value)
                   (cproc env succeed fail2)
                   (aproc env succeed fail2)))
             ;; failure continuation for
             ;; evaluating the predicate
             fail))))

والتسلسلات تُعالَج أيضًا بالطريقة ذاتها كما في المقيّم السابق، إلّا في المناورات التي تلزم في الإجراء الفرعيّ sequentially لتمرير الاستمرارات. أي أنّنا، لتنفيذ a تتابعيًّا ثم b، ندعو a باستمرار نجاحٍ يدعو b.

(define (analyze-sequence exps)
  (define (sequentially a b)
    (lambda (env succeed fail)
      (a env
         ;; success continuation for calling a
         (lambda (a-value fail2)
           (b env succeed fail2))
         ;; failure continuation for calling a
         fail)))
  (define (loop first-proc rest-procs)
    (if (null? rest-procs)
        first-proc
        (loop (sequentially first-proc 
                            (car rest-procs))
              (cdr rest-procs))))
  (let ((procs (map analyze exps)))
    (if (null? procs)
        (error "Empty sequence: ANALYZE"))
    (loop (car procs) (cdr procs))))

التعريفات والإحلالات

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

(define (analyze-definition exp)
  (let ((var (definition-variable exp))
        (vproc (analyze 
                (definition-value exp))))
    (lambda (env succeed fail)
      (vproc env
             (lambda (val fail2)
               (define-variable! var val env)
               (succeed 'ok fail2))
             fail))))

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

أمّا إذا نجح vproc، ومضينا في إجراء الإحلال، فينبغي أن ننظر في احتمال أن يفشل هذا الفرع من الاحتساب لاحقًا، ممّا سيقتضي منّا التراجع رجعيًّا خارج الإحلال. وبذلك، ينبغي لنا أن نُرتّب للتراجع عن الإحلال كجزءٍ من عمليّة التتبّع الرجعيّ.[57]

ويُنجَز هذا بإعطاء vproc استمرار نجاحٍ (مُعلَّمٍ بالتعليق «1» أدناه) يحفظ القيمة القديمة للمتغيّر قبل إحلال القيمة الجديدة فيه والمواصلة انطلاقًا من الإحلال. وأمّا استمرار الفشل الممرَّر مع قيمة الإحلال (المُعلَّم بالتعليق «2» أدناه) فيستعيد القيمة القديمة للمتغيّر قبل مواصلة الفشل. أي أنّ الإحلال الناجح يُوفّر استمرارَ فشلٍ يعترض فشلًا لاحقًا؛ فأيّ فشلٍ كان سيدعو fail2 لولا ذلك، يدعو هذا الإجراء بدلًا منه، للتراجع عن الإحلال قبل دعاء fail2 فعلًا.

(define (analyze-assignment exp)
  (let ((var (assignment-variable exp))
        (vproc (analyze 
                (assignment-value exp))))
    (lambda (env succeed fail)
      (vproc env
             (lambda (val fail2)    ; *1*
               (let ((old-value
                      (lookup-variable-value 
                       var 
                       env)))
                 (set-variable-value!
                  var 
                  val 
                  env)
                 (succeed 
                  'ok
                  (lambda ()    ; *2*
                    (set-variable-value! 
                     var
                     old-value
                     env)
                    (fail2)))))
               fail))))

تطبيقات الإجراءات

وإجراء التنفيذ الخاصّ بالتطبيقات لا يحتوي على أفكارٍ جديدة، إلّا التعقيد الفنّيّ في إدارة الاستمرارات. وينشأ هذا التعقيد في analyze-application، نظرًا للحاجة إلى متابعة استمراري النجاح والفشل بينما نقيّم العوامل. ونستخدم إجراء get-args لتقييم قائمة العوامل، بدلًا من map البسيط كما في المقيّم العاديّ.

(define (analyze-application exp)
  (let ((fproc (analyze (operator exp)))
        (aprocs (map analyze (operands exp))))
    (lambda (env succeed fail)
      (fproc env
             (lambda (proc fail2)
               (get-args 
                aprocs
                env
                (lambda (args fail3)
                  (execute-application
                   proc args succeed fail3))
                fail2))
             fail))))

وفي get-args، لاحِظ كيف يُنجَز النزول بـcdr في قائمة إجراءات التنفيذ aproc والبناء بـcons لقائمة args الناتجة، بدعاء كلّ aproc في القائمة باستمرار نجاحٍ يدعو get-args تعاوديًّا. ولكلّ نداءٍ من هذه النداءات التعاوديّة لـget-args استمرارُ نجاحٍ قيمتُه cons للمعطى المحصَل عليه حديثًا على قائمة المعطيات المتراكمة:

(define (get-args aprocs env succeed fail)
  (if (null? aprocs)
      (succeed '() fail)
      ((car aprocs) 
       env
       ;; success continuation for this aproc
       (lambda (arg fail2)
         (get-args 
          (cdr aprocs)
          env
          ;; success continuation for
          ;; recursive call to get-args
          (lambda (args fail3)
            (succeed (cons arg args)
                     fail3))
          fail2))
       fail)))

يَتمّ تطبيق الإجراء الفعليّ، الذي يؤدّاه execute-application، بالطريقة ذاتها كما في المقيّم الاعتياديّ، باستثناء ضرورة إدارة الاستمراريّات (continuations).

(define (execute-application 
         proc args succeed fail)
  (cond ((primitive-procedure? proc)
         (succeed 
          (apply-primitive-procedure 
           proc args)
          fail))
        ((compound-procedure? proc)
         ((procedure-body proc)
          (extend-environment 
           (procedure-parameters proc)
           args
           (procedure-environment proc))
          succeed
          fail))
        (else (error "Unknown procedure type: 
                      EXECUTE-APPLICATION"
                     proc))))

تقييم تعابير amb

صيغة amb الخاصّة هي العنصر الأساسيّ في اللغة غير الحتميّة (nondeterministic). وهنا نرى جوهر عمليّة التفسير وسبب تتبّع الاستمراريّات. فإنّ إجراء التنفيذ الخاص بـamb يُعرّف حلقةً try-next تتنقّل بين إجراءات التنفيذ لجميع القيم الممكنة لتعبير amb. ويُستدعى كلّ إجراء تنفيذٍ مع استمراريّة فشلٍ تُجرّب التالي منها. وعندما لا تعود هناك بدائلٌ لتُجرَّب، يفشل تعبير amb كلَّه.

(define (analyze-amb exp)
  (let ((cprocs
         (map analyze (amb-choices exp))))
    (lambda (env succeed fail)
      (define (try-next choices)
        (if (null? choices)
            (fail)
            ((car choices) 
             env
             succeed
             (lambda ()
               (try-next (cdr choices))))))
      (try-next cprocs))))

حلقة المشغّل

حلقة المشغّل (driver loop) لمقيّم amb معقّدة، بسبب الآليّة التي تسمح للمستخدم بأن يُعيد المحاولة في تقييم تعبيرٍ ما. ويستخدم المشغّل إجراءً يُسمّى internal-loop، يأخذ كمعطىً إجراءً try-again. والمقصود أنّ نداء try-again ينبغي أن يمضي إلى البديل غير المُجرَّب التالي في التقييم غير الحتميّ. وإجراء Internal-loop إمّا أن يستدعي try-again استجابةً لكتابة المستخدم try-again في حلقة المشغّل، وإلّا فيبدأ تقييمًا جديدًا بنداء ambeval.

تبلّغ استمراريّة الفشل الخاصة بهذا النداء لـambeval المستخدمَ بأنّه لا تعود هناك قيم، وتستدعي حلقة المشغّل من جديد.

أمّا استمراريّة النجاح الخاصة بنداء ambeval فأخفى أثرًا. فنحن نطبع القيمة المتحصّلة ثمّ نستدعي الحلقة الداخليّة من جديد مع إجراء try-again يكون قادرًا على تجريب البديل التالي. وهذا الإجراء next-alternative هو الوسيط الثاني الذي مُرِّر إلى استمراريّة النجاح. واعتياديًّا، نفكّر في هذا الوسيط الثاني بوصفه استمراريّة فشلٍ تُستخدَم إن فشل فرع التقييم الحاليّ لاحقًا. لكن في هذه الحالة، يكون قد أُتمّ تقييمٌ ناجح، فيمكننا استدعاء فرع البديل «الفاشل» باحثين عن تقييماتٍ ناجحةٍ إضافيّة.

(define input-prompt  ";;; Amb-Eval input:")
(define output-prompt ";;; Amb-Eval value:")

(define (driver-loop)
  (define (internal-loop try-again)
    (prompt-for-input input-prompt)
    (let ((input (read)))
      (if (eq? input 'try-again)
          (try-again)
          (begin
            (newline)
            (display 
             ";;; Starting a new problem ")
            (ambeval 
             input
             the-global-environment
             ;; ambeval success
             (lambda (val next-alternative)
               (announce-output 
                output-prompt)
               (user-print val)
               (internal-loop 
                next-alternative))
             ;; ambeval failure
             (lambda ()
               (announce-output
                ";;; There are no 
                 more values of")
               (user-print input)
               (driver-loop)))))))
  (internal-loop
   (lambda ()
     (newline)
     (display 
      ";;; There is no current problem")
     (driver-loop))))

يستخدم النداء الأوّل لـinternal-loop إجراء try-again يشتكي من عدم وجود مسألةٍ جارية، ويعيد تشغيل حلقة المشغّل. وهذا هو السلوك الذي سيحدث إن كتب المستخدم try-again حين لا يكون هناك تقييمٌ جارٍ.

التمرين 4.50: نفّذ صيغةً خاصّةً جديدةً تُسمّى ramb تشبه amb إلّا في أنّها تبحث في البدائل بترتيبٍ عشوائيّ، بدلًا من ترتيبها من اليسار إلى اليمين. وأظهر كيف يمكن أن يساعد هذا في مسألة أليسا الواردة في التمرين 4.49.

التمرين 4.51: نفّذ نوعًا جديدًا من الإحلال يُسمّى permanent-set! لا يُتراجع عنه عند الفشل. فمثلًا، يمكننا اختيار عنصرين متمايزين من قائمةٍ وعدّ عدد المحاولات اللازمة لإتيان اختيارٍ ناجح على النحو الآتي:

(define count 0)
(let ((x (an-element-of '(a b c)))
      (y (an-element-of '(a b c))))
  (permanent-set! count (+ count 1))
  (require (not (eq? x y)))
  (list x y count))

;;; Starting a new problem
;;; Amb-Eval value:
(a b 2)

;;; Amb-Eval input:
try-again

;;; Amb-Eval value:
(a c 3)

ما القيم التي كانت ستُعرَض لو كنّا قد استخدمنا set! هنا بدلًا من permanent-set!؟

التمرين 4.52: نفّذ بنيةً جديدةً تُسمّى if-fail تسمح للمستخدم بالإمساك بفشل تعبيرٍ ما. يأخذ If-fail تعبيرين. فهو يقيّم التعبير الأوّل كما هو معتاد ويعيد كما هو معتاد إن نجح التقييم. لكن إن فشل التقييم، فتُعاد قيمة التعبير الثاني، كما في المثال التالي:

;;; Amb-Eval input:
(if-fail 
 (let ((x (an-element-of '(1 3 5))))
   (require (even? x))
   x)
 'all-odd)

;;; Starting a new problem
;;; Amb-Eval value:
all-odd

;;; Amb-Eval input:
(if-fail
 (let ((x (an-element-of '(1 3 5 8))))
   (require (even? x))
   x)
 'all-odd)

;;; Starting a new problem
;;; Amb-Eval value:
8

التمرين 4.53: باستخدام permanent-set! كما هو موصوف في التمرين 4.51 وif-fail كما في التمرين 4.52، فما ستكون عليه نتيجة تقييم

(let ((pairs '()))
  (if-fail 
   (let ((p (prime-sum-pair 
             '(1 3 5 8) 
             '(20 35 110))))
     (permanent-set! pairs 
                     (cons p pairs))
     (amb))
   pairs))

التمرين 4.54: لو كنّا لم نُدرك أنّ require يمكن تنفيذه كإجراءٍ اعتياديّ يستخدم amb، يُعرَّف من قِبل المستخدم كجزءٍ من برنامجٍ غير حتميّ، لَكُنّا اضطررنا إلى تنفيذه كصيغةٍ خاصّة. وهذا كان سيتطلّب إجراءات صياغة

(define (require? exp) 
  (tagged-list? exp 'require))

(define (require-predicate exp) 
  (cadr exp))

وبندًا جديدًا في التوزيع داخل analyze

((require? exp) (analyze-require exp))

وكذلك الإجراء analyze-require الذي يعالج تعابير require. أكمل التعريف التالي لـanalyze-require.

(define (analyze-require exp)
  (let ((pproc (analyze 
                (require-predicate exp))))
    (lambda (env succeed fail)
      (pproc env
             (lambda (pred-value fail2)
               (if ⟨??⟩
                   ⟨??⟩
                   (succeed 'ok fail2)))
             fail))))

4.4 البرمجة المنطقيّة

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

معظم لغات البرمجة، بما في ذلك Lisp، مُنظّمةٌ حول احتساب قيم الدوالّ الرياضيّة. واللغات الموجَّهة بالتعابير (كالـLisp وFortran وAlgol) تستثمر «المجاز» القائل إنّ تعبيرًا يصف قيمة دالّةٍ يمكن تفسيره أيضًا بوصفه وسيلةً لاحتساب تلك القيمة. وبسبب هذا، فإنّ معظم لغات البرمجة منحازةٌ انحيازًا شديدًا نحو الاحتسابات أحاديّة الاتجاه (الاحتسابات ذات المدخلات والمخرجات المحدَّدة تحديدًا جيّدًا). ولكن ثمّة لغات برمجةٍ مختلفةٌ جذريًّا تُرخِي هذا الانحياز. وقد رأينا مثالًا على ذلك في 3.3.5، حيث كانت كائنات الاحتساب قيودًا حسابيّة. ففي نظام القيود، لا يكون اتجاه الاحتساب وترتيبه محدَّدين تحديدًا جيّدًا؛ وعند أداء عمليّةٍ حسابيّةٍ ينبغي إذاً للنظام أن يوفّر معرفة «كيفيّة» أكثر تفصيلًا ممّا سيكون عليه الأمر في احتسابٍ حسابيٍّ اعتياديّ. لكن هذا لا يعني أنّ المستخدم مُخَلَّصٌ كلّيًّا من مسؤوليّة توفير المعرفة الإجرائيّة. فثمّة شبكات قيودٍ كثيرةٌ تُنفّذ المجموعة ذاتها من القيود، وعلى المستخدم أن يختار من مجموعة الشبكات المتكافئة رياضيًّا شبكةً مناسبةً لتحديد عمليّةٍ حسابيّةٍ معيّنة.

إنّ مقيّم البرامج غير الحتميّ الوارد في 4.3 يبتعد أيضًا عن الرأي القائل إنّ البرمجة تتمحور حول بناء خوارزميّاتٍ لاحتساب دوالّ أحاديّة الاتجاه. ففي لغةٍ غير حتميّةٍ، يمكن أن يكون للتعبيرات أكثر من قيمةٍ واحدة، ونتيجةً لذلك، فإنّ الاحتساب يتعامل مع العلاقات بدلًا من الدوالّ أحاديّة القيمة. وتوسّع البرمجة المنطقيّة هذه الفكرة بجمع رؤية علائقيّةٍ للبرمجة مع نوعٍ قويّ من مطابقة الأنماط الرمزيّة يُسمّى التوحيد (unification).[58]

فمثلًا، تأمّل العمليّة append، التي تأخذ قائمتين كمعطيين وتجمع عناصرها لتشكيل قائمةٍ واحدة. وفي لغةٍ إجرائيّةٍ مثل Lisp، كان بوسعنا تعريف append بدلالة بانٍ القائمة الأساسيّ cons، كما فعلنا في 2.2.1:

(define (append x y)
  (if (null? x) 
      y 
      (cons (car x) (append (cdr x) y))))

يمكن اعتبار هذا الإجراء ترجمةً إلى Lisp للقاعدتين التاليتين، تغطّي الأولى منهما حالة كون القائمة الأوّل فارغة، وتتولّى الثانية حالة كون القائمة غير فارغة، وهي cons من جزءين:

  • لأيّ قائمة y، فإنّ القائمة الفارغة وy تُنتجان بـappend القائمة y.
  • لأيّ u وv وy وz، فإنّ (cons u v) وy تُنتجان بـappend القائمة (cons u z) إذا كانت v وy تُنتجان بـappend القائمة z.[59]

وباستخدام إجراء append، يمكننا الإجابة عن أسئلةَ مثل

أوجد الحاصل من append لقائمتَي (a b) و(c d).

لكنّ القاعدتين ذاتهما كافيتان أيضًا للإجابة عن الأنواع التالية من الأسئلة، التي لا يستطيع الإجابة عنها:

أوجد قائمةً y يُلحِقها append مع (a b) لإنتاج (a b c d). أوجد جميع x وy التي يُنتج append عليها القائمة (a b c d).

في لغة البرمجة المنطقيّة، يكتب المبرمج «إجراء» append بالنصّ على القاعدتين المتعلّقتين بـappend الواردتين أعلاه. وتُوفَّر معرفة «الكيفيّة» تلقائيًّا بواسطة المفسّر للسماح باستخدام هذا الزوج الوحيد من القواعد في الإجابة عن جميع الأنواع الثلاثة من الأسئلة حول append.[60]

للّغات البرمجة المنطقيّة المعاصرة (بما في ذلك اللغة التي نُنفّذها هنا) قصورٌ جوهريّ، إذ إنّ طرائقها العامّة في «الكيفيّة» قد تقودها إلى حلقاتٍ لانهائيّةٍ زائفة أو إلى سلوكيّاتٍ غير مرغوبةٍ أخرى. والبرمجة المنطقيّة مجالٌ نشطٌ للبحث في علم الحاسوب.[61]

سبق لنا في هذا الفصل أن استكشفنا تقنيّة تنفيذ المفسّرات ووصفنا العناصر الأساسيّة لأيّ مفسّرٍ للغة شبيهة بـLisp (وبالحريّ، لأيّ مفسّرٍ لأيّ لغةٍ تقليديّة). والآن سنطبّق هذه الأفكار لمناقشة مفسّرٍ للغة البرمجة المنطقيّة. ونُسمّي هذه اللغة لغة الاستفسار (query language)، لأنّها مفيدةٌ للغايةٍ في استرجاع المعلومات من قواعد البيانات بصياغة استفسارات، أي أسئلة، معبَّرًا عنها باللغة. وحتّى وإن كانت لغة الاستفسار مختلفةً جدًّا عن Lisp، فسنرى أنّ من الملائم وصف اللغة بدلالة الإطار العامّ ذاته الذي استخدمناه طوال الوقت: كمجموعةٍ من العناصر الأوّليّة، جنبًا إلى جنبٍ مع وسائل تركيبٍ تُمكّننا من دمج عناصر بسيطةٍ لإنشاء عناصر أكثر تعقيدًا، ووسائل تجريدٍ تُمكّننا من اعتبار العناصر المعقّدة وحداتٍ مفاهيميّةً واحدة. ومفسّر لغة البرمجة المنطقيّة أعقدّ بكثيرٍ من مفسّر لغةٍ مثل Lisp. ومع ذلك، فسنرى أنّ مفسّر لغة الاستفسار لدينا يحتوي على العديد من العناصر ذاتها الموجودة في مفسّر 4.1. وعلى وجه الخصوص، فسيكون هناك جزء «eval» يُصنّف التعابير بحسب النوع، وجزء «apply» يُنفّذ آليّة تجريد اللغة (الإجراءات في حالة Lisp، والقواعد في حالة البرمجة المنطقيّة). وتلعب أيضًا دورًا مركزيًّا في التنفيذ بنية بيانات الإطار، التي تُحدّد التطابق بين الرموز وقيمها المرتبطة. وجانب آخر مثيرٌ للاهتمام في تنفيذنا للغة الاستفسار هو أنّنا نستخدم المجاري استخدامًا كبيرًا، وهي التي قُدِّمت في الفصل 3.

4.4.1 الاسترجاع الاستنباطيّ للمعلومات

تتفوّق البرمجة المنطقيّة في توفير واجهاتٍ لقواعد البيانات لاسترجاع المعلومات. ولغة الاستفسار التي سنُنفّذها في هذا الفصل مصمّمةٌ للاستخدام على هذا النحو.

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

نموذج من قاعدة بيانات

تحتوي قاعدة بيانات الموظّفين لشركة Microshaft على تأكيدات (assertions) حول موظّفي الشركة. وهذه معلومات عن بن بِتدل، ساحر الحاسوب المقيم:

(address (Bitdiddle Ben) 
         (Slumerville (Ridge Road) 10))
(job (Bitdiddle Ben) (computer wizard))
(salary (Bitdiddle Ben) 60000)

كلّ تأكيدٍ قائمةٌ (ثلاثيّة في هذه الحالة) عناصرها قد تكون هي نفسها قوائم.

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

(address (Hacker Alyssa P) 
         (Cambridge (Mass Ave) 78))
(job (Hacker Alyssa P) (computer programmer))
(salary (Hacker Alyssa P) 40000)
(supervisor (Hacker Alyssa P) (Bitdiddle Ben))

(address (Fect Cy D) 
         (Cambridge (Ames Street) 3))
(job (Fect Cy D) (computer programmer))
(salary (Fect Cy D) 35000)
(supervisor (Fect Cy D) (Bitdiddle Ben))

(address (Tweakit Lem E) 
         (Boston (Bay State Road) 22))
(job (Tweakit Lem E) (computer technician))
(salary (Tweakit Lem E) 25000)
(supervisor (Tweakit Lem E) (Bitdiddle Ben))

وهناك أيضًا مبرمجٌ متدرّب، تشرف عليه أليسا:

(address (Reasoner Louis) 
         (Slumerville (Pine Tree Road) 80))
(job (Reasoner Louis) 
     (computer programmer trainee))
(salary (Reasoner Louis) 30000)
(supervisor (Reasoner Louis) 
            (Hacker Alyssa P))

كلّ هؤلاء الناس في قسم الحاسوب، كما تشير إلى ذلك كلمة computer كونها أوّل عنصرٍ في أوصاف وظائفهم.

بن موظّفٌ عالي المستوى. ومشرفُه هو كبير الشركة نفسه:

(supervisor (Bitdiddle Ben) (Warbucks Oliver))
(address (Warbucks Oliver) 
         (Swellesley (Top Heap Road)))
(job (Warbucks Oliver) 
     (administration big wheel))
(salary (Warbucks Oliver) 150000)

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

(address (Scrooge Eben) 
         (Weston (Shady Lane) 10))
(job (Scrooge Eben) 
     (accounting chief accountant))
(salary (Scrooge Eben) 75000)
(supervisor (Scrooge Eben) (Warbucks Oliver))

(address (Cratchet Robert) 
         (Allston (N Harvard Street) 16))
(job (Cratchet Robert) (accounting scrivener))
(salary (Cratchet Robert) 18000)
(supervisor (Cratchet Robert) (Scrooge Eben))

وهناك أيضًا سكرتيرة لكبير الشركة:

(address (Aull DeWitt) 
         (Slumerville (Onion Square) 5))
(job (Aull DeWitt) (administration secretary))
(salary (Aull DeWitt) 25000)
(supervisor (Aull DeWitt) (Warbucks Oliver))

وتحتوي قاعدة البيانات أيضًا على تأكيداتٍ حول أنواع الوظائف التي يمكن أن يؤدّيها أشخاصٌ يشغلون أنواعًا أخرى من الوظائف. فمثلًا، يمكن لساحر الحاسوب أن يؤدّي وظيفتَي مبرمج الحاسوب وفنّيّ الحاسوب كليهما:

(can-do-job (computer wizard) 
            (computer programmer))

(can-do-job (computer wizard) 
            (computer technician))

يمكن لمبرمج الحاسوب أن يحلّ محلّ متدرّب:

(can-do-job (computer programmer)
            (computer programmer trainee))

وكذلك، كما هو معروفٌ جيّدًا،

(can-do-job (administration secretary)
            (administration big wheel))

الاستفسارات البسيطة

تسمح لغة الاستفسار (query language) للمستخدمين باسترجاع المعلومات من قاعدة البيانات بطرح استفساراتٍ استجابةً لإشارة النظام. فمثلًا، للعثور على جميع مبرمجي الحاسوب، يمكن للمرء أن يقول

;;; Query input:
(job ?x (computer programmer))

سيستجيب النظام بالعناصر التالية:

;;; Query results:
(job (Hacker Alyssa P) (computer programmer))
(job (Fect Cy D) (computer programmer))

يُحدّد الاستفسار المُدخَل أنّنا نبحث عن مدخلاتٍ في قاعدة البيانات تطابق نمطًا معيّنًا. وفي هذا المثال، يُحدّد النمط مدخلاتٍ متألّفة من ثلاثة عناصر، أوّلها الرمز الحرفيّ job، والثاني يمكن أن يكون أيّ شيء، والثالث هو القائمة الحرفيّة (computer programmer). و«أيّ شيء» الذي يمكن أن يكون العنصر الثاني في القائمة المطابَقة يُحدَّد بـمتغيّر نمط، ?x. والصورة العامّة لمتغيّر النمط هي رمزٌ، يُعتبر اسم المتغيّر، يسبقه علامة استفهام. وسنرى أدناه لماذا يكون من المفيد تحديد أسماء لمتغيّرات الأنماط بدلًا من مجرّد وضع ? في الأنماط لتمثيل «أيّ شيء». ويستجيب النظام لاستفسارٍ بسيطٍ بعرض جميع المدخلات في قاعدة البيانات التي تطابق النمط المحدَّد.

ويمكن أن يكون للنمط أكثر من متغيّرٍ واحد. فمثلًا، الاستفسار

(address ?x ?y)

سيسرد عناوين جميع الموظّفين.

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

ويمكن للمتغيّر النمطيّ ذاته أن يظهر أكثر من مرّةٍ واحدة في استفسارٍ ما، مُحدّدًا أنّ «أيّ شيء» ذاته يجب أن يظهر في كلّ موضع. وهذا سبب تسمية المتغيّرات. فمثلًا،

(supervisor ?x ?x)

يجد جميع الناس الذين يشرفون على أنفسهم (وإن لم يكن هناك أيّ تأكيدٍ كهذا في نموذج قاعدة بياناتنا).

الاستفسار

(job ?x (computer ?type))

فإنّه يطابق جميع مدخلات الوظائف التي عنصرها الثالث قائمةٌ ثنائيّة العناصر أوّلها computer:

(job (Bitdiddle Ben) (computer wizard))
(job (Hacker Alyssa P) (computer programmer))
(job (Fect Cy D) (computer programmer))
(job (Tweakit Lem E) (computer technician))

وهذا النمط ذاته لا يطابق

(job (Reasoner Louis) 
     (computer programmer trainee))

لأنّ العنصر الثالث في المدخل قائمةٌ من ثلاثة عناصر، أمّا العنصر الثالث في النمط فيُحدّد أنّه ينبغي أن يكون هناك عنصران. فلو أردنا تغيير النمط بحيث يمكن أن يكون العنصر الثالث أيّ قائمةٍ تبدأ بـcomputer، فبوسعنا أن نُحدّد[62]

(job ?x (computer . ?type))

فمثلًا،

(computer . ?type)

يطابق البيان

(computer programmer trainee)

بحيث يكون ?type هو القائمة (programmer trainee). وهو يطابق أيضًا البيان

(computer programmer)

بحيث يكون ?type هو القائمة (programmer)، ويطابق البيان

(computer)

بحيث يكون ?type هو القائمة الفارغة ().

ويمكننا وصف معالجة لغة الاستفسار للاستفسارات البسيطة على النحو الآتي:

  • يجد النظام جميع الإحالات إلى متغيّرات نمط الاستفسار التي تُحقّق النمط—أي جميع مجموعات القيم للمتغيّرات بحيث إنّ جُسِّمت متغيّرات النماط بالقيم (أي استُبدلت بها)، كانت النتيجة في قاعدة البيانات.
  • يستجيب النظام للاستفسار بسرد جميع تجسيدات نمط الاستفسار مصحوبةً بإحالات المتغيّرات التي تُحقّقه.

لاحِظ أنّ النمط إن لم يكن له متغيّرات، فإنّ الاستفسار يَنزل إلى تحديد ما إذا كان ذلك النمط في قاعدة البيانات. فإن كان كذلك، فإنّ الإحالة الفارغة، التي لا تُحيل أيّ قيمٍ إلى متغيّرات، تُحقّق ذلك النمط لتلك قاعدة البيانات.

التمرين 4.55: أعطِ استفساراتٍ بسيطةً تسترجع المعلومات التالية من قاعدة البيانات: جميع الناس الذين يشرف عليهم بن بِتدل؛

أسماء ووظائف جميع الناس في قسم المحاسبة؛ أسماء وعناوين جميع الناس الذين يسكنون في سلومرفيل.

الاستفسارات المركّبة

تُشكّل الاستفسارات البسيطة العمليّات الأوّليّة للغة الاستفسار. ولكي تُشكَّل عمليّاتٌ مركّبة، توفّر لغة الاستفسار وسائل تركيب. وأحد ما يجعل لغة الاستفسار لغةً للبرمجة المنطقيّة هو أنّ وسائل التركيب تُعاكس وسائل التركيب المستخدمة في تشكيل التعابير المنطقيّة: and وor وnot. (وهنا and وor وnot ليست أوّليّات Lisp، بل عمليّاتٌ مدمجةٌ في لغة الاستفسار.)

ويمكننا استخدام and على النحو الآتي للعثور على عناوين جميع مبرمجي الحاسوب:

(and (job ?person (computer programmer))
     (address ?person ?where))

والناتج الخارج هو

(and (job (Hacker Alyssa P) 
          (computer programmer))
     (address (Hacker Alyssa P) 
              (Cambridge (Mass Ave) 78)))

(and (job (Fect Cy D) (computer programmer))
     (address (Fect Cy D) 
              (Cambridge (Ames Street) 3)))

وبصورةٍ عامّة،

(and ⟨query₁⟩ ⟨query₂⟩ … ⟨queryₙ⟩)

تتحقّقه جميع مجموعات القيم لمتغيّرات النمط التي تُحقّق ⟨query1⟩⟨ q u e r y_{1} ⟩ … ⟨queryn⟩⟨ q u e r y_{n} ⟩ في الوقت ذاته.

أمّا بالنسبة للاستعلامات البسيطة، فإنّ النظام يعالج الاستعلام المركّب بإيجاد جميع الإحالات على متغيّرات النمط التي تُحقّق الاستعلام، ثم يعرض إنشاءات الاستعلام بتلك القيم.

وثمة وسيلة أخرى لبناء استعلاماتٍ مركّبة، وهي باستخدام or. فمثلًا،

(or (supervisor ?x (Bitdiddle Ben))
    (supervisor ?x (Hacker Alyssa P)))

وسيجد جميع الموظفين الذين يشرف عليهم بن بِتدل أو أليسا بي. هاكر:

(or (supervisor (Hacker Alyssa P) 
                (Bitdiddle Ben))
    (supervisor (Hacker Alyssa P) 
                (Hacker Alyssa P)))

(or (supervisor (Fect Cy D) 
                (Bitdiddle Ben))
    (supervisor (Fect Cy D) 
                (Hacker Alyssa P)))

(or (supervisor (Tweakit Lem E) 
                (Bitdiddle Ben))
    (supervisor (Tweakit Lem E) 
                (Hacker Alyssa P)))

(or (supervisor (Reasoner Louis) 
                (Bitdiddle Ben))
    (supervisor (Reasoner Louis) 
                (Hacker Alyssa P)))

وبصورةٍ عامّة،

(or ⟨query₁⟩ ⟨query₂⟩ … ⟨queryₙ⟩)

تتحقّقه جميع مجموعات القيم لمتغيّرات النمط التي تُحقّق واحدًا على الأقلّ من ⟨query1⟩⟨ q u e r y_{1} ⟩ … ⟨queryn⟩⟨ q u e r y_{n} ⟩ .

ويمكن أيضًا تكوين استعلاماتٍ مركّبةً بـnot. فمثلًا،

(and (supervisor ?x (Bitdiddle Ben))
     (not (job ?x (computer programmer))))

فيجد جميع الأشخاص الذين يشرف عليهم بن بِتدل وليسوا مبرمجي حاسوب. وبصورةٍ عامّة،

(not ⟨query₁⟩)

تتحقّقه جميع الإحالات على متغيّرات النمط التي لا تُحقّق ⟨query1⟩⟨ q u e r y_{1} ⟩ .[63]

أمّا الشكل الأخير من أشكال التركيب فيُسمّى lisp-value. وعندما يكون lisp-value العنصر الأوّل في نمط، فإنّه يُحدّد أنّ العنصر التالي مُسَيِّم Lisp يُطبَّق على بقيّة العناصر (المنشأة) كمعطياتٍ. وبصورةٍ عامّة،

(lisp-value ⟨predicate⟩ ⟨arg₁⟩ … ⟨argₙ⟩)

تتحقّقه الإحالات على متغيّرات النمط التي يكون فيها تطبيق ⟨predicate⟩ على ⟨arg1⟩⟨ a r g_{1} ⟩ … ⟨argn⟩⟨ a r g_{n} ⟩ المنشأة صحيحًا. فمثلًا، لإيجاد جميع الأشخاص الذين راتبهم أكبر من $30,000 نستطيع كتابة[64]

(and (salary ?person ?amount)
     (lisp-value > ?amount 30000))

التمرين 4.56: صِغ استعلاماتٍ مركّبةً تسترجع المعلومات التالية: أسماء جميع الأشخاص الذين يشرف عليهم بن بِتدل، مع عناوينهم؛

جميع الأشخاص الذين راتبهم أقلّ من راتب بن بِتدل، مع راتبهم وراتب بن بِتدل؛ جميع الأشخاص الذين يشرف عليهم شخصٌ ليس في قسم الحاسوب، مع اسم المشرف ومهمّته.

القواعد

إلى جانب الاستعلامات الأوّليّة والاستعلامات المركّبة، توفّر لغة الاستعلامات وسائلَ لتجريد الاستعلامات. وهذه تُعطى بـالقواعد. والقاعدة

(rule (lives-near ?person-1 ?person-2)
      (and (address ?person-1 
                    (?town . ?rest-1))
           (address ?person-2 
                    (?town . ?rest-2))
           (not (same ?person-1 ?person-2))))

تُحدّد أنّ شخصين يعيشان قريبًا من بعضهما إذا كانا يعيشان في البلدة نفسها. والبند not الأخير يمنع القاعدة من القول إنّ جميع الناس يعيشون قريبين من ذواتهم. أمّا علاقة same فمعرَّفةٌ بقاعدةٍ بسيطةٍ جدًّا:[65]

(rule (same ?x ?x))

والقاعدة الآتية تُصرّح أنّ الشخص "عجلة" (wheel) في تنظيمٍ إذا كان يُشرف على شخصٍ ما هو بدوره مشرف:

(rule (wheel ?person)
      (and (supervisor ?middle-manager 
                       ?person)
           (supervisor ?x ?middle-manager)))

الصورة العامّة للقاعدة هي

(rule ⟨conclusion⟩ ⟨body⟩)

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

(lives-near ?x (Bitdiddle Ben))

يُنتج

(lives-near (Reasoner Louis) (Bitdiddle Ben))
(lives-near (Aull DeWitt) (Bitdiddle Ben))

ولإيجاد جميع مبرمجي الحاسوب الذين يعيشون قريبين من بن بِتدل، نستطيع أن نسأل

(and (job ?x (computer programmer))
     (lives-near ?x (Bitdiddle Ben)))

وكما في حالة الإجراءات المركّبة، يمكن استخدام القواعد أجزاءً من قواعد أخرى (كما رأينا مع القاعدة lives-near أعلاه) أو حتّى تعريفها تعاوديًّا. فمثلًا، القاعدة

(rule (outranked-by ?staff-person ?boss)
      (or (supervisor ?staff-person ?boss)
          (and (supervisor ?staff-person 
                           ?middle-manager)
               (outranked-by ?middle-manager 
                             ?boss))))

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

التمرين 4.57: عرّف قاعدةً تقول إنّ الشخص 1 يمكن أن يحلّ محلّ الشخص 2 إذا كان الشخص 1 يؤدّي المهمّة نفسها التي يؤدّيها الشخص 2، أو إذا كان من يؤدّي مهمّة الشخص 1 يستطيع أداء مهمّة الشخص 2 أيضًا، وإذا لم يكن الشخص 1 والشخص 2 الشخص نفسه. وباستخدام قاعدتك، أعطِ استعلاماتٍ تجد ما يلي: جميع الأشخاص الذين يمكنهم حلّ محلّ ساي دي. فيكت؛

جميع الأشخاص الذين يمكنهم حلّ محلّ شخصٍ يُدفع له أكثر مما يُدفع لهم، مع الراتبين.

التمرين 4.58: عرّف قاعدةً تقول إنّ الشخص "شخصية كبيرة" (big shot) في قسمٍ إذا كان يعمل في القسم لكن ليس له مشرف يعمل في القسم.

التمرين 4.59: فات بن بِتدل اجتماعٌ واحد أكثر من اللازم. وخشيةً من أن تُكلّفه عادته في نسيان الاجتماعات وظيفته، قرّر بن أن يفعل شيئًا حيال ذلك. فهو يضيف جميع الاجتماعات الأسبوعيّة للشركة إلى قاعدة بيانات Microshaft بتأكيد ما يلي:

(meeting accounting (Monday 9am))
(meeting administration (Monday 10am))
(meeting computer (Wednesday 3pm))
(meeting administration (Friday 1pm))

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

(meeting whole-company (Wednesday 4pm))

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

(rule (meeting-time ?person ?day-and-time)
      ⟨rule-body⟩)

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

التمرين 4.60: بإعطاء الاستعلام

(lives-near ?person (Hacker Alyssa P))

تستطيع أليسا بي. هاكر إيجاد الأشخاص الذين يعيشون قريبين منها، والذين يمكنها أن تركب معهم إلى العمل. ومن جانبٍ آخر، حين تحاول إيجاد جميع أزواج الأشخاص الذين يعيشون قريبين من بعضهم بالاستعلام عن

(lives-near ?person-1 ?person-2)

تلاحِظ أنّ كلّ زوجٍ من الأشخاص الذين يعيشون قريبين من بعضهم يُسجَّل مرّتين؛ فمثلًا،

(lives-near (Hacker Alyssa P) (Fect Cy D))
(lives-near (Fect Cy D) (Hacker Alyssa P))

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

المنطق كبرامج

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

  • لأيّ قائمة y، فإنّ القائمة الفارغة وy تُلحَقَان بـappend لتكوين y.
  • لأيّ u وv وy وz، فإنّ (cons u v) وy تُلحَقَان بـappend لتكوين (cons u z) إذا كانت v وy تُلحَقَان بـappend لتكوين z.

وللتعبير عن هذا في لغة الاستعلامات، نُعرّف قاعدتين لعلاقة

(append-to-form x y z)

التي نستطيع تفسيرها بمعنى أنّ x وy تُلحَقَان بـappend لتكوين z:

(rule (append-to-form () ?y ?y))
(rule (append-to-form (?u . ?v) ?y (?u . ?z))
      (append-to-form ?v ?y ?z))

القاعدة الأولى ليس لها جسم، ما يعني أنّ الخلاصة تتحقّق لأيّ قيمة لـ?y. ولاحِظ كيف تستخدم القاعدة الثانية ترميز الذيل المنقّط لتسمية car وcdr لقائمة.

وبوجود هاتين القاعدتين، نستطيع صياغة استعلاماتٍ تحتسب append قائمتين:

;;; Query input:
(append-to-form (a b) (c d) ?z)

;;; Query results:
(append-to-form (a b) (c d) (a b c d))

والأكثر إدهاشًا، أنّنا نستطيع استخدام القواعد ذاتها لطرح السؤال: «أيّ قائمة، حين تُلحَق بـappend بـ(a b)، تُنتج (a b c d)؟» وهذا يُجرى على النحو الآتي:

;;; Query input:
(append-to-form (a b) ?y (a b c d))

;;; Query results:
(append-to-form (a b) (c d) (a b c d))

ونستطيع أيضًا السؤال عن جميع أزواج القوائم التي تُلحَق بـappend لتكوين (a b c d):

;;; Query input:
(append-to-form ?x ?y (a b c d))

;;; Query results:
(append-to-form () (a b c d) (a b c d))
(append-to-form (a) (b c d) (a b c d))
(append-to-form (a b) (c d) (a b c d))
(append-to-form (a b c) (d) (a b c d))
(append-to-form (a b c d) () (a b c d))

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

التمرين 4.61: تُنفّذ القواعد الآتية علاقة next-to التي تجد العناصر المتجاورة في قائمة:

(rule (?x next-to ?y in (?x ?y . ?u)))
(rule (?x next-to ?y in (?v . ?z))
      (?x next-to ?y in ?z))

فما ستكون الاستجابة للاستعلامات الآتية؟

(?x next-to ?y in (1 (2 3) 4))
(?x next-to 1 in (2 1 3 1))

التمرين 4.62: عرّف قواعد تُنفّذ عمليّة last-pair الواردة في التمرين 2.17، والتي تُعيد قائمةً تحتوي العنصر الأخير لقائمةٍ غير فارغة. افحص قواعدك على استعلامات مثل (last-pair (3) ?x) و(last-pair (1 2 3) ?x) و(last-pair (2 ?x) (3)). فهل تعمل قواعدك بصورةٍ صحيحةٍ على استعلامات مثل (last-pair ?x (3))؟

التمرين 4.63: قاعدة البيانات الآتية (انظر سفر التكوين 4) تتتبّع أنساب نسْل آدا رجوعًا إلى آدم، عن طريق قابيل:

(son Adam Cain) (son Cain Enoch)
(son Enoch Irad) (son Irad Mehujael)
(son Mehujael Methushael)
(son Methushael Lamech)
(wife Lamech Ada) (son Ada Jabal)
(son Ada Jubal)

صِغ قواعدً مثل «إذا كان SS ابنًا لـff ، وكان ff ابنًا لـGG ، فإنّ SS هو حفيد GG » و«إذا كانت WW زوجةً لـMM ، وكان SS ابنًا لـWW ، فإنّ SS هو ابن MM » (وهو ما كان أكثر صدقًا في الأزمنة التوراتيّة ممّا هو عليه اليوم) ممّا يُتيح لنظام الاستعلامات إيجاد حفيد قابيل، وأبناء لامك، وأحفاد متوشائيل. (انظر التمرين 4.69 لقواعدَ تستنبط علاقاتٍ أكثر تعقيدًا.)

4.4.2 كيف يعمل نظام الاستعلامات

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

ينبغي أن يكون واضحًا أنّ مُقيّم الاستعلامات لا بدّ أن يجري نوعًا من البحث لكي يُطابق الاستعلامات ضدّ الحقائق والقواعد في قاعدة البيانات. وإحدى طرائق فعل ذلك هي تنفيذ نظام الاستعلامات برنامجًا غير حتميّ، باستخدام مُقيّم amb الخاصّ بـ4.3 (انظر التمرين 4.78). وهناك احتمالٌ آخر وهو إدارة البحث بمساعدة المجاري. وتنفيذنا يتبع هذا الأسلوب الثاني.

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

مطابقة الأنماط

مُطابِق الأنماط برنامجٌ يختبر ما إذا كان معطًى ما يلائم نمطًا محدّدًا. فمثلًا، فإنّ قائمة البيانات ((a b) c (a b)) تُطابق النمط (?x c ?x) بمتغيّر النمط ?x مربوطًا بـ(a b). وقائمة البيانات نفسها تُطابق النمط (?x ?y ?z) بحيث ?x و?z كلاهما مربوط بـ(a b) و?y مربوط بـc. وهي تُطابق أيضًا النمط ((?x ?y) c (?x ?y)) بحيث ?x مربوط بـa و?y مربوط بـb. لكنها لا تُطابق النمط (?x a ?y)، لأنّ ذلك النمط يُحدّد قائمةً عنصرها الثاني هو الرمز a.

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

فمثلًا، استخدام النمط (?x ?y ?x) لمطابقة (a b a) مع إطارٍ فارغ سيُعيد إطارًا يُحدّد أنّ ?x مربوط بـa و?y مربوط بـb. ومحاولة المطابقة بالنمط نفسه، والمعطى نفسه، وإطارٌ يُحدّد أنّ ?y مربوط بـa ستفشل. ومحاولة المطابقة بالنمط نفسه، والمعطى نفسه، وإطارٌ ?y فيه مربوط بـb و?x غير مربوط ستُعيد الإطار المعطى مُوسَّعًا بربط ?x بـa.

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

(job ?x (computer programmer))

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

مجاري الإطارات

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

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

الشكل 4.4: يعالج الاستعلام مجرى إطاراتٍ.

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

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

يبدو الأناقة الحقيقيّة لتنفيذ مجرى الإطارات واضحةً حين نتعامل مع الاستعلامات المركّبة. فمعالجة الاستعلامات المركّبة تستفيد من قدرة مُطابِقنا على المطالبة بأن تكون المطابقة متّسقةً مع إطارٍ محدّدًا. فمثلًا، للتعامل مع and استعلامين، مثل

(and (can-do-job 
      ?x 
      (computer programmer trainee))
     (job ?person ?x))

(بصورةٍ غير رسميّة، «ابحث عن جميع الأشخاص الذين يستطيعون أداء مهمّة متدرّب مبرمج حاسوب»)، نجد أوّلًا جميع المُدخلات التي تُطابق النمط

(can-do-job ?x (computer programmer trainee))

يُنتج هذا مجرى إطاراتٍ، يحتوي كلٌّ منها ربطًا لـ?x. ثم نجد، لكلّ إطارٍ في المجرى، جميع المُدخلات التي تُطابق

(job ?person ?x)

على نحوٍ متّسقٍ مع الربط المعطى لـ?x. وكلّ مطابقةٍ من هذه المطابقات ستُنتج إطارًا يحتوي روابط لـ?x و?person. ويمكن النظر إلى and استعلامَين بوصفه تركيبًا تواليًّا للاستعلامين المركّبين، كما هو موضّح في الشكل 4.5. فالإطارات التي تمرّ عبر مرشّح الاستعلام الأوّل تُرشَّح وتُوسَّع بعدُ بالاستعلام الثاني.

الشكل 4.5: يُنتَج تركيب and لاستعلامَين بالعمل على مجرى الإطارات تواليًّا.

يُظهر الشكل 4.6 الطريقة المماثلة لاحتساب or استعلامَين تركيبًا توازيًّا للاستعلامين المركّبين. فمجرى الإطارات الداخل يُوسَّع على حدةٍ بواسطة كلّ استعلام. ثم يُدمَج المجريان الناتجان لإنتاج مجرى المخرج النهائيّ.

الشكل 4.6: يُنتَج تركيب or لاستعلامَين بالعمل على مجرى الإطارات توازيًّا ودمج النتائج.

وحتّى من هذا الوصف عالي المستوى، فمن الواضح أنّ معالجة الاستعلامات المركّبة قد تكون بطيئة. فحين قد يُنتج الاستعلام أكثر من إطار مخرجٍ واحدٍ لكلّ إطار دخل، وحين يحصل كلّ استعلامٍ في and على إطارات دخله من الاستعلام السابق، فقد يضطرّ استعلام and، في أسوأ الحالات، إلى أداء عددٍ من المطابقات أُسّيّ في عدد الاستعلامات (انظر التمرين 4.76).[68] فمع أنّ النظم التي تتعامل مع الاستعلامات البسيطة وحدها عمليّةٌ إلى حدٍّ بعيد، فإنّ التعامل مع الاستعلامات المعقّدة عسيرٌ للغاية.[69]

ومن منظور مجرى الإطارات، يعمل not استعلامٍ ما بوصفه مرشّحًا يُزيل جميع الإطارات التي يمكن إشباع الاستعلام بالنسبة لها. فمثلًا، بالنظر إلى النمط

(not (job ?x (computer programmer)))

فنحن نحاول، لكلّ إطارٍ في مجرى الدخل، إنتاج إطارات امتدادٍ تُشبع (job ?x (computer programmer)). ونُزيل من مجرى الدخل جميع الإطارات التي توجد لها امتداداتٌ كهذه. والنتيجة مجرىٌ يتألّف من الإطارات التي فيها وحدها لا يُشبع ربط ?x الشرط (job ?x (computer programmer)). فمثلًا، عند معالجة الاستعلام

(and (supervisor ?x ?y)
     (not (job ?x (computer programmer))))

فسيُنتج البند الأوّل إطاراتٍ بروابط لـ?x و?y. ثم سيُرشّح البند not هذه الإطارات بإزالة جميع الإطارات التي يُشبع فيها ربط ?x القيد القائل إنّ ?x مبرمج حاسوب.[70]

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

التوحيد

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

يأخذ الموحِّد نمطين، كلٌّ منهما يحتوي ثوابت ومتغيّرات، ويُحدّد ما إذا كان من الممكن إحلال قيمٍ بالمتغيّرات تجعل النمطين متساويين. فإن كان ذلك ممكنًا، أعاد إطارًا يحتوي هذه الروابط. فمثلًا، توحيد (?x a ?y) و(?y ?z a) سيُحدّد إطارًا يجِب فيه ربط ?x و?y و?z جميعًا بـa. ومن الجهة الأخرى، سيُخفِق توحيد (?x ?y a) و(?x b ?y)، لأنّه لا توجد قيمة لـ?y تستطيع أن تجعل النمطين متساويين. (فلتساوي عنصري النمطين الثانيين، يلزم أن يكون ?y هو b؛ غير أنّ تساوي عنصريهما الثالثين يقتضي أن يكون ?y هو a.) والموحِّد المستخدم في نظام الاستعلامات، شأنه شأن مُطابِق الأنماط، يأخذ إطارًا مدخلًا ويُجرى توحيداتٍ متّسقةٍ مع هذا الإطار.

وخوارزميّة التوحيد هي أكثر أجزاء نظام الاستعلامات صعوبةً من الناحية الفنّيّة. ومع الأنماط المعقّدة، قد يبدو أنّ أداء التوحيد يقتضي استنباطًا. فتوحيد (?x ?x) و((a ?y c) (a b ?z))، مثلًا، يقتضي من الخوارزميّة أن تستنبط أنّ ?x ينبغي أن يكون (a b c)، و?y ينبغي أن يكون b، و?z ينبغي أن يكون c. ويمكننا اعتبار هذه العمليّة حلًّا لمجموعةٍ من المعادلات بين مركّبات النمط. وبصورةٍ عامّة، هذه معادلات آنيّة، وقد تقتضي معالجةً جوهريّة للحل.[71] فمثلًا، يمكن النظر إلى توحيد (?x ?x) و((a ?y c) (a b ?z)) على أنّه تحديدٌ لمعادلات آنيّة

?x = (a ?y c)
?x = (a b ?z)

وهذه المعادلات تقتضي أنّ

(a ?y c) = (a b ?z)

مما يقتضي أدواره أنّ

a = a, ?y = b, c = ?z,

ومن ثمّ أنّ

?x = (a b c)

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

تأمّل توحيد (?x a) و((b ?y) ?z). فبوسعنا أن نستنبط أنّ ?x = (b ?y) وأنّ a = ?z، لكنّنا لا نستطيع الاستنباط أكثر من ذلك من أجل ?x أو ?y. ولا يُخفِق التوحيد، إذ من الممكن بيقينٍ جعل النمطين متساويين بإحلال قيمٍ في ?x و?y. وحين لا تُقيّد هذه المطابقة بأيّ نحوٍ القيم التي يمكن أن تتّخذها ?y، فلا يُوضع أيّ ربطٍ لـ?y في الإطار الناتج. غير أنّ المطابقة تُقيّد قيمة ?x. فأيًّا كانت قيمة ?y، ينبغي أن يكون ?x هو (b ?y). وبذلك، يُوضع في الإطار ربطٌ لـ?x بالنمط (b ?y). وإذا حُدّدت قيمة ?y لاحقًا وأُضيفت إلى الإطار (بمطابقة أنماطٍ أو بتوحيدٍ يلزم أن يكون متّسقًا مع هذا الإطار)، فإنّ ?x المربوط سابقًا سيُشير إلى هذه القيمة.[72]

تطبيق القواعد

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

(lives-near ?x (Hacker Alyssa P))

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

(rule (lives-near ?person-1 ?person-2)
      (and (address ?person-1 
                    (?town . ?rest-1))
           (address ?person-2 
                    (?town . ?rest-2))
           (not (same ?person-1 ?person-2))))

مما يُنتج إطارًا يُحدّد أنّ ?person-2 مربوط بـ(Hacker Alyssa P) وأنّ ?x ينبغي أن يُربط بـ?person-1 (أي أن تكون له القيمة ذاتها). والآن، بالنسبة إلى هذا الإطار، نُقيّم الاستعلام المركّب المعطى من جسم القاعدة. والمطابقات الناجحة ستُوسّع هذا الإطار بتوفير ربطٍ لـ?person-1، ومن ثمّ قيمةً لـ?x، نستطيع استخدامها لإنشاء نمط الاستعلام الأصليّ.

وبصورةٍ عامّة، يستخدم مُقيّم الاستعلامات الطريقة الآتية لتطبيق قاعدةٍ حين يحاول إثبات نمط استعلامٍ في إطارٍ يُحدّد روابط بعض متغيّرات النمط:

  • وحّد الاستعلام مع استنتاج القاعدة لتُشكَّل، في حال النجاح، امتدادٌ للإطار الأصليّ.
  • وبالنسبة إلى الإطار المُوسَّع، قيّم الاستعلام المشكّل من جسم القاعدة.

لاحِظ مدى تشابه هذا مع طريقة تطبيق إجراءٍ في مُقيّم eval/apply الخاصّ بـLisp:

  • اربط وسائط الإجراء بمعطياته لتُشكَّل إطارًا يُوسّع بيئة الإجراء الأصليّة.
  • وبالنسبة إلى البيئة المُوسَّعة، قيّم التعبير المشكّل من جسم الإجراء.

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

الاستعلامات البسيطة

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

فبالنظر إلى نمط الاستعلام ومجرى إطاراتٍ، نُنتج، لكلّ إطارٍ في مجرى الدخل، مجريين:

  • مجرى إطاراتٍ مُوسَّعةٍ يُحصَل عليها بمطابقة النمط ضدّ جميع التأكيدات في قاعدة البيانات (باستخدام مُطابِق الأنماط)، و
  • مجرى إطاراتٍ مُوسَّعةٍ يُحصَل عليها بتطبيق جميع القواعد الممكنة (باستخدام الموحِّد).[73]

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

مُقيّم الاستعلامات وحلقة المُشغّل

ومع تعقيد عمليّات المطابقة الكامنة، فإنّ النظام مُنظَّمٌ على نحوٍ يُشبه إلى حدٍّ كبيرٍ مُقيّمًا لأيّ لغة. والإجراء الذي ينسّق عمليّات المطابقة يُسمّى qeval، وهو يلعب دورًا يماثل دور إجراء eval في Lisp. وqeval يأخذ كمدخلاتٍ استعلامًا ومجرى إطارات. ومخرجه مجرى إطارات، يقابل المطابقات الناجحة لنمط الاستعلام، يُوسّع إطارًا ما في مجرى الدخل، كما هو موضّح في الشكل 4.4. وشأنه شأن eval، يُصنّف qeval أنواعًا مختلفةً من التعابير (الاستعلامات) ويُرسل إلى إجراءٍ مناسبٍ لكلٍّ منها. فهناك إجراءٌ لكلّ صيغةٍ خاصّة (and وor وnot وlisp-value) وآخرٌ للاستعلامات البسيطة.

وحلقة المُشغّل، التي تماثل إجراء driver-loop الخاصّ ببقيّة المُقيّمات في هذا الفصل، تقرأ الاستعلامات من الطرفيّة. وبالنسبة إلى كلّ استعلام، تستدعي qeval بالاستعلام وبمجرى يتألّف من إطارٍ فارغٍ مفرد. وهذا سيُنتج مجرى جميع المطابقات الممكنة (جميع الامتدادات الممكنة للإطار الفارغ). وبالنسبة إلى كلّ إطارٍ في المجرى الناتج، تُنشئ الاستعلام الأصليّ باستخدام قيم المتغيّرات الموجودة في الإطار. ثم يُطبع مجرى الاستعلامات المنشأة هذا.[74]

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

(assert!
 (job (Bitdiddle Ben)
      (computer wizard)))

(assert!
 (rule (wheel ?person)
       (and (supervisor 
             ?middle-manager ?person)
            (supervisor
             ?x ?middle-manager))))

4.4.3 هل البرمجة المنطقيّة منطقٌ رياضيّ؟

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

(and (job ?x (computer programmer))
     (supervisor ?x ?y))

أو

(and (supervisor ?x ?y)
     (job ?x (computer programmer)))

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

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

ويمكن اعتبار لغة الاستعلامات خاصّنا مجموعةً جزئيّةً كهذه من المنطق الرياضيّ، قابلةً للتفسير إجرائيًّا. فالتأكيد يمثّل حقيقةً بسيطةً (قضيّةٌ أوّليّة). والقاعدة تمثّل استلزامًا مفاده أنّ استنتاج القاعدة يصدق في الحالات التي يصدق فيها جسم القاعدة. وللقاعدة تفسيرٌ إجرائيٌّ طبيعيّ: لإثبات استنتاج القاعدة، أثبِت جسم القاعة. وبذلك، تُحدّد القواعد الاحتسابات. غير أنّّه، إذ يمكن اعتبار القواعد أيضًا عباراتٍ في المنطق الرياضيّ، فبوسعنا تبرير أيّ «استنباط» ينجزه برنامجٌ منطقيّ بالادّعاء بأنّ النتيجة ذاتها كان من الممكن الحصول عليها بالعمل كلّيًّا داخل المنطق الرياضيّ.[76]

الحلقات اللانهائيّة

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

(assert! (married Minnie Mickey))

وإذا سألنا الآن

(married Mickey ?who)

فلن نحصل على أيّ ردّ، لأنّ النظام لا يعلم أنّ كوْن AA متزوّجًا بـBB ، يقتضي أنّ BB متزوّج بـAA . لذا نُثبّت القاعدة

(assert! (rule (married ?x ?y)
               (married ?y ?x)))

ونستعلم مرّةً أخرى

(married Mickey ?who)

وللأسف، فهذا سيدفع النظام إلى حلقةٍ لا نهائيّة، على النحو الآتي:

  • يجد النظام أنّ القاعدة married قابلةٌ للتطبيق؛ أي أنّ استنتاج القاعدة (married ?x ?y) يتوحّد بنجاحٍ مع نمط الاستعلام (married Mickey ?who) لإنتاج إطارٍ يُربط فيه ?x بـMickey و?y بـ?who. فيمضي المفسّر في تقييم جسم القاعدة (married ?y ?x) في هذا الإطار - أي في معالجة الاستعلام (married ?who Mickey) في الواقع.
  • ويظهر جوابٌ واحدٌ مباشرةً بوصفه تأكيدًا في قاعدة البيانات: (married Minnie Mickey).
  • والقاعدة married قابلةٌ للتطبيق أيضًا، فيُقيّم المفسّر جسم القاعدة مرّةً أخرى، وهو المكافئ هذه المرّة لـ(married Mickey ?who).

وإنّ النظام الآن في حلقةٍ لا نهائيّة. والحقيقة أنّ عثور النظام على الجواب البسيط (married Minnie Mickey) قبل سقوطه في هذه الحالة أم لا، يتوقّف على تفاصيل التنفيذ المتعلّقة بالترتيب الذي يفحص به النظام العناصر في قاعدة البيانات. وهذا مثالٌ في غاية البساطة على أنواع الحلقات التي يمكن أن تقع. ومجموعاتٌ من القواعد المترابطة قد تؤدّي إلى حلقاتٍ يصعب استباقها بدرجةٍ أكبر بكثير، وقد يتوقّف ظهور حلقةٍ على ترتيب البنود في and (انظر التمرين 4.64) أو على تفاصيل منخفضة المستوى تتعلّق بالترتيب الذي يعالج به النظام الاستعلامات.[77]

مشاكل not

وهناك مثيلةٌ أخرى في نظام الاستعلامات تتعلّق بـnot. فبالنظر إلى قاعدة البيانات الواردة في 4.4.1، تأمّل الاستعلامَين الآتيين:

(and (supervisor ?x ?y)
     (not (job ?x (computer programmer))))

(and (not (job ?x (computer programmer)))
     (supervisor ?x ?y))

وهذان الاستعلامان لا يُنتجان النتيجة ذاتها. فالاستعلام الأوّل يبدأ بإيجاد جميع المُدخلات في قاعدة البيانات التي تُطابق (supervisor ?x ?y)، ثم يُرشّح الإطارات الناتجة بإزالة تلك التي تُشبع فيها قيمة ?x الشرط (job ?x (computer programmer)). والاستعلام الثاني يبدأ بترشيح الإطارات الداخلة لإزالة تلك التي تستطيع إشباع (job ?x (computer programmer)). وحين الإطار الداخل الوحيد فارغ، يفحص قاعدة البيانات ليرى ما إذا كانت هناك أنماطٌ تُشبع (job ?x (computer programmer)). وبما أنّ هناك عادةً مُدخلاتٍ من هذا الشكل، فإنّ البند not يُزيل الإطار الفارغ ويُعيد مجرى إطارات فارغ. ونتيجةً لذلك، فإنّ الاستعلام المركّب كلّه يُعيد مجرى فارغ.

والمشكلة أنّ تنفيذنا لـnot إنّما يُقصد به حقًّا أن يكون مرشّحًا على قيم المتغيّرات. فإذا عُومل بند not بإطارٍ تبقى فيه بعض المتغيّرات غير مربوطة (كما هو الحال بالنسبة إلى ?x في المثال أعلاه)، فإنّ النظام سيُنتج نتائج غير متوقّعة. ومشاكلٌ مماثلة تقع مع استخدام lisp-value - إذ إنّ المُسَيِّم Lisp لا يستطيع العمل إذا كانت بعض معطيته غير مربوطة. انظر التمرين 4.77.

وهناك أيضًا طريقةٌ أخطرُ كثيرًا يختلف بها not لغة الاستعلامات عن not المنطق الرياضيّ. ففي المنطق، نُفسّر العبارة «not PP » على أنّها تعني أنّ PP ليست صحيحة. غير أنّ «not PP » في نظام الاستعلامات تعني أنّ PP غير قابلةٍ للاستنباط من المعرفة في قاعدة البيانات. فمثلًا، بالنظر إلى قاعدة البيانات الخاصّة بالموظفين الواردة في 4.4.1، فإنّ النظام سيستنبط بسرورٍ جميع أنواع عبارات not، كأنّ بن بِتدل ليس من محبّي كرة القاعدة، وأنّ المطر لا يسقط في الخارج، وأنّ 2 + 2 ليست 4.[78] وبعبارةٍ أخرى، فإنّ not لغات البرمجة المنطقيّة يعبّر عمّا يُسمّى افتراض العالم المغلق، وهو أنّ جميع المعلومات ذات الصلة قد أُدرجت في قاعدة البيانات.[79]

التمرين 4.64: يحذف لويس ريزونر قاعدة outranked-by (4.4.1) من قاعدة البيانات سهوًا. وحين يُدرك ذلك، يُعيد تثبيتها سريعًا. ولكنّ من سوء حظّه أنّه يُدخل تغييرًا طفيفًا على القاعدة، فيكتبها على النحو الآتي

(rule (outranked-by ?staff-person ?boss)
  (or (supervisor ?staff-person ?boss)
      (and (outranked-by ?middle-manager
                         ?boss)
           (supervisor ?staff-person 
                       ?middle-manager))))

وبعد أن يكتب لويس هذه المعلومة في النظام مباشرةً، يأتي ديوت أول ليسأل عمّن يتفوّق على بن بِتدل. فيُصدر الاستعلام

(outranked-by (Bitdiddle Ben) ?who)

وبعد الإجابة، يسقط النظام في حلقةٍ لا نهائيّة. اشرح السبب.

التمرين 4.65: يُعطي ساي د. فكت، وهو يتطلّع إلى اليوم الذي سيرتفع فيه في المؤسّسة، استعلامًا لإيجاد جميع العجلات (باستخدام قاعدة wheel الواردة في 4.4.1):

(wheel ?who)

ولدهشته، يستجيب النظام

;;; Query results:
(wheel (Warbucks Oliver))
(wheel (Bitdiddle Ben))
(wheel (Warbucks Oliver))
(wheel (Warbucks Oliver))
(wheel (Warbucks Oliver))

لماذا يُدرَج أوليفر واربكس أربع مرّات؟

التمرين 4.66: عمّم بن نظام الاستعلامات لتوفير إحصائيّات عن الشركة. فمثلًا، لإيجاد مجموع رواتب جميع مبرمجي الحاسوب، سيصير بإمكاننا أن نقول

(sum ?amount
     (and (job ?x (computer programmer))
          (salary ?x ?amount)))

وبصورةٍ عامّة، يتيح نظام بن الجديد تعبيراتٍ على الصيغة

(accumulation-function ⟨variable⟩
                       ⟨query pattern⟩)

حيث يمكن أن يكون accumulation-function أشياءً مثل sum أو average أو maximum. ويستنتج بن أنّ هذا ينبغي أن يكون سهل التنفيذ. فسيُمرّر نمط الاستعلام ببساطةٍ إلى qeval. وهذا سيُنتج مجرى إطارات. ثم سيُمرّر هذا المجرى عبر دالّة تحويلٍ تستخرج قيمة المتغيّر المعيَّن من كلّ إطارٍ في المجرى، ويُغذّي مجرى القيم الناتج إلى دالّة التجميع. وفي اللحظة التي يُكمل فيها بن التنفيذ ويكاد أن يجرّبه، يمرّ ساي، وهو ما زال يحيّره نتيجة استعلام wheel الواردة في التمرين 4.65. وحين يُظهر ساي لبن ردّ النظام، يئنّ بن: «لا، لا، لن تعمل خطّتي البسيطة للتجميع!»

ما الذي أدركه بن للتوّ؟ اذكر خطّةً يمكنه استخدامها لإنقاذ الموقف.

التمرين 4.67: ابتكر طريقةً لتثبيت كاشف حلقاتٍ في نظام الاستعلامات لتجنّب أنواع الحلقات البسيطة الموضّحة في النصّ وفي التمرين 4.64. والفكرة العامّة هي أنّ النظام ينبغي أن يُحتفظ فيه بنوعٍ من تاريخ سلسلة الاستنباطات الحاليّة، وألّا يبدأ معالجة استعلامٍ يعمل عليه أصلًا. صِف أيّ نوعٍ من المعلومات (الأنماط والإطارات) يُدرَج في هذا التاريخ، وكيف ينبغي إجراء الفحص. (بعد أن تدرس تفاصيل تنفيذ نظام الاستعلامات الواردة في 4.4.4، قد ترغب في تعديل النظام ليضمّ كاشف الحلقات الذي ابتكرته.)

التمرين 4.68: عرّف قواعد تُنفّذ عمليّة reverse الواردة في التمرين 2.18، والتي تُعيد قائمةً تحتوي العناصر ذاتها الموجودة في قائمةٍ معطاةً بترتيبٍ معكوس. (تلميح: استخدم append-to-form.) فهل تستطيع قواعدك الإجابة عن كلٍّ من (reverse (1 2 3) ?x) و(reverse ?x (1 2 3))؟

التمرين 4.69: بدءًا من قاعدة البيانات والقواعد التي صُغتها في التمرين 4.63، ابتكر قاعدةً لإضافة «greats» إلى علاقة الحفد. وينبغي لهذا أن يمكّن النظام من استنتاج أنّ إيراد هو حفيد آدم الأكبر، أو أنّ جابل وجوبال هما حفيدا آدم الأكابر بخمس درجات. (تلميح: مثّل الحقيقة حول إيراد، مثلًا، بـ((great grandson) Adam Irad). اكتب قواعدًا تُحدّد ما إذا كانت قائمة تنتهي بالكلمة grandson. استخدم هذا للتعبير عن قاعدةٍ تسمح باستنتاج العلاقة ((great . ?rel) ?x ?y)، حيث ?rel قائمة تنتهي بـgrandson.) افحص قواعدك على استعلامات مثل ((great grandson) ?g ?ggs) و(?relationship Adam Irad).

4.4.4 تنفيذ نظام الاستعلامات

وصف القسم 4.4.2 كيف يعمل نظام الاستعلامات. والآن نستكمل التفاصيل بتقديم تنفيذٍ كاملٍ للنظام.

4.4.4.1 حلقة المُشغّل والإنشاء

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

(define input-prompt  ";;; Query input:")
(define output-prompt ";;; Query results:")

(define (query-driver-loop)
  (prompt-for-input input-prompt)
  (let ((q (query-syntax-process (read))))
    (cond ((assertion-to-be-added? q)
           (add-rule-or-assertion! 
            (add-assertion-body q))
           (newline)
           (display 
            "Assertion added to data base.")
           (query-driver-loop))
          (else
           (newline)
           (display output-prompt)
           (display-stream
            (stream-map
             (lambda (frame)
               (instantiate
                q
                frame
                (lambda (v f)
                  (contract-question-mark v))))
             (qeval q (singleton-stream '()))))
           (query-driver-loop)))))

وهنا، كما في بقيّة المُقيّمات في هذا الفصل، نستخدم صياغةً مجرّدةً لتعبيرات لغة الاستعلامات. وتنفيذ صياغة التعبير، متضمّنًا المُسَيِّم assertion-to-be-added? والمحدِّد add-assertion-body، مُعطى في 4.4.4.7. وAdd-rule-or-assertion! معرَّفٌ في 4.4.4.5.

وقبل إجراء أيّ معالجةٍ على تعبير دخلٍ، تُحوّله حلقة المُشغّل تحويلًا صياغيًّا إلى شكلٍ يجعل المعالجة أكفأ. وهذا يستلزم تغيير تمثيل متغيّرات الأنماط. وحين يُنشأ الاستعلام، فإنّ أيّة متغيّراتٍ تبقى غير مربوطة تُحوَّل رجوعًا إلى تمثيل الدخل قبل طباعتها. وهذه التحويلات تُجرى بإجراءَين هما query-syntax-process وcontract-question-mark (4.4.4.7).

ولإنشاء تعبيرٍ، ننسخه، مستبدلين أيّ متغيّراتٍ في التعبير بقيمها في إطارٍ معطًى. والقيم نفسها تُنشأ، إذ قد تحتوي متغيّراتٍ (فمثلًا، إذا كان ?x في exp مربوطًا بـ?y نتيجةً للتوحيد وكان ?y مربوطًا بدوره بـ5). والإجراء الذي يجب اتّخاذه إذا تعذّر إنشاء متغيّرٍ يُعطى بوسيطٍ إجرائيٍّ لـinstantiate.

(define (instantiate 
         exp frame unbound-var-handler)
  (define (copy exp)
    (cond ((var? exp)
           (let ((binding 
                  (binding-in-frame 
                   exp frame)))
             (if binding
                 (copy 
                  (binding-value binding))
                 (unbound-var-handler 
                  exp frame))))
          ((pair? exp)
           (cons (copy (car exp)) 
                 (copy (cdr exp))))
          (else exp)))
  (copy exp))

والإجراءات التي تتلاعب بالروابط معرَّفةٌ في 4.4.4.8.

4.4.4.2 المُقيّم

وإجراء qeval، الذي تستدعيه query-driver-loop، هو المُقيّم الأساسيّ لنظام الاستعلامات. وهو يأخذ كمدخلاتٍ استعلامًا ومجرى إطارات، ويُعيد مجرى إطاراتٍ مُوسَّعة. وهو يُحدّد الصيغ الخاصّة بإرسالٍ موجَّه بالبيانات باستخدام get وput، تمامًا كما فعلنا عند تنفيذ العمليّات العامّة في الفصل 2. وأيّ استعلامٍ لا يُحدَّد على أنّه صيغةٌ خاصّة يُفترض أنّه استعلامٌ بسيط، تُعالجه simple-query.

(define (qeval query frame-stream)
  (let ((qproc (get (type query) 'qeval)))
    (if qproc
        (qproc (contents query) frame-stream)
        (simple-query query frame-stream))))

والإجراءان type وcontents، المعرَّفان في 4.4.4.7، يُنفّذان الصياغة المجرّدة للصيغ الخاصّة.

الاستعلامات البسيطة

ويتعامل إجراء simple-query مع الاستعلامات البسيطة. فهو يأخذ كمعطياتٍ استعلامًا بسيطًا (نمطًا) مع مجرى إطاراتٍ، ويُعيد المجرى المشكّل بتوسيع كلّ إطارٍ بجميع مطابقات قاعدة البيانات للاستعلام.

(define (simple-query query-pattern 
                      frame-stream)
  (stream-flatmap
   (lambda (frame)
     (stream-append-delayed
      (find-assertions query-pattern frame)
      (delay 
        (apply-rules query-pattern frame))))
   frame-stream))

بالنسبة إلى كلّ إطارٍ في مجرى الدخل، نستخدم find-assertions (4.4.4.3) لمطابقة النمط ضدّ جميع التأكيدات في قاعدة البيانات، مما يُنتج مجرى إطاراتٍ مُوسَّعة، ونستخدم apply-rules (4.4.4.4) لتطبيق جميع القواعد الممكنة، مما يُنتج مجرى إطاراتٍ مُوسَّعةً آخر. وهذان المجريان يُدمَجان (باستخدام stream-append-delayed، 4.4.4.6) لتكوين مجرى بجميع الطرق التي يمكن بها إشباع النمط المعطى اتّساقًا مع الإطار الأصليّ (انظر التمرين 4.71). والمجاري الخاصّة بإطارات الدخل الفرديّة تُدمَج باستخدام stream-flatmap (4.4.4.6) لتشكيل مجرى واحدٍ هائلٍ بجميع الطرق التي يمكن بها توسيع أيّ إطارٍ من مجرى الدخل الأصليّ لإنتاج مطابقةٍ مع النمط المعطى.

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

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

(define (conjoin conjuncts frame-stream)
  (if (empty-conjunction? conjuncts)
      frame-stream
      (conjoin (rest-conjuncts conjuncts)
               (qeval 
                (first-conjunct conjuncts)
                frame-stream))))

والتعبير

(put 'and 'qeval conjoin)

يُهيّئ qeval للإرسال إلى conjoin حين يصادف شكل and.

وتُعالَج استعلامات or على نحوٍ مماثل، كما هو موضّح في الشكل 4.6. فمجاري المخرج الخاصّة بالفصول المختلفة لـor تُحتسب كلٌّ على حدةٍ ويُدمَج بعضها ببعض باستخدام إجراء interleave-delayed الوارد في 4.4.4.6. (انظر التمرين 4.71 والتمرين 4.72.)

(define (disjoin disjuncts frame-stream)
  (if (empty-disjunction? disjuncts)
      the-empty-stream
      (interleave-delayed
       (qeval (first-disjunct disjuncts) 
              frame-stream)
       (delay (disjoin 
               (rest-disjuncts disjuncts)
               frame-stream)))))
(put 'or 'qeval disjoin)

والمُسَيِّمات والمحدِّدات الخاصّة بصياغة القيود والفصول مُعطاةٌ في 4.4.4.7.

المرشّحات

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

(define (negate operands frame-stream)
  (stream-flatmap
   (lambda (frame)
     (if (stream-null? 
          (qeval (negated-query operands)
                 (singleton-stream frame)))
         (singleton-stream frame)
         the-empty-stream))
   frame-stream))
(put 'not 'qeval negate)

وlisp-value مرشّحٌ مماثل لـnot. فكلّ إطارٍ في المجرى يُستخدم لإنشاء المتغيّرات في النمط، ويُطبَّق المُسَيِّم المُشار إليه، ثم تُرشَّح الإطارات التي يُعيد فيها المُسَيِّم القيمة خاطئة خارج مجرى الدخل. وينتج خطأٌ إذا كانت هناك متغيّرات نمطٍ غير مربوطة.

(define (lisp-value call frame-stream)
  (stream-flatmap
   (lambda (frame)
     (if (execute
          (instantiate
           call
           frame
           (lambda (v f)
             (error 
              "Unknown pat var: LISP-VALUE" 
              v))))
         (singleton-stream frame)
         the-empty-stream))
   frame-stream))
(put 'lisp-value 'qeval lisp-value)

وexecute، الذي يُطبّق المُسَيِّم على المعطيات، يلزمه أن يُقيّم تعبير المُسَيِّم بـeval للحصول على الإجراء الذي سيُطبَّق. غير أنّه يلزمه ألّا يُقيّم المعطيات، إذ إنّها هي المعطيات الفعليّة أصلًا، لا تعبيراتٌ سيُنتج تقييمها (في Lisp) المعطيات. ولاحِظ أنّ execute مُنفَّذٌ باستخدام eval وapply من نظام Lisp الكامن.

(define (execute exp)
  (apply (eval (predicate exp) 
               user-initial-environment)
         (args exp)))

والصيغة الخاصّة always-true تتيح استعلامًا يُشبع دائمًا. فهي تتجاهل محتواها (الذي يكون فارغًا عادةً) وتُمرّر ببساطةٍ جميع الإطارات في مجرى الدخل. وalways-true يستخدمه المحدِّد rule-body (4.4.4.7) لتوفير أجسامٍ للقواعد التي عُرِّفت دون أجسام (أي القواعد التي تُشبع استنتاجاتها دائمًا).

(define (always-true ignore frame-stream) 
  frame-stream)
(put 'always-true 'qeval always-true)

والمحدِّدات التي تُحدّد صياغة not وlisp-value مُعطاةٌ في 4.4.4.7.

4.4.4.3 إيجاد التأكيدات بمطابقة الأنماط

وfind-assertions، الذي تستدعيه simple-query (4.4.4.2)، يأخذ كمدخلٍ نمطًا وإطارًا. وهو يُعيد مجرى إطارات، كلٌّ منها يُوسّع الإطار المعطى بمطابقةٍ من قاعدة البيانات للنمط المعطى. وهو يستخدم fetch-assertions (4.4.4.5) للحصول على مجرى جميع التأكيدات في قاعدة البيانات التي ينبغي فحصها لمطابقة النمط والإطار. والسبب في استخدام fetch-assertions هنا هو أنّنا نستطيع غالبًا تطبيق اختباراتٍ بسيطةٍ ستُقصي العديد من مُدخلات قاعدة البيانات من مجموعة المرشّحين لمطابقةٍ ناجحة. وسيظلّ النظام يعمل لو أقصينا fetch-assertions وفحصنا ببساطةٍ مجرى جميع التأكيدات في قاعدة البيانات، لكنّ الاحتساب سيكون أقلّ كفاءةً لأنّنا سنحتاج إلى إجراء نداءاتٍ أكثر بكثيرٍ للمُطابِق.

(define (find-assertions pattern frame)
  (stream-flatmap 
    (lambda (datum) 
      (check-an-assertion datum pattern frame))
    (fetch-assertions pattern frame)))

ويأخذ check-an-assertion كمعطياتٍ نمطًا، وكائن بيانات (تأكيدًا)، وإطارًا، ويُعيد إمّا مجرىٌ ذو عنصرٍ واحدٍ يحتوي الإطار المُوسَّع وإمّا the-empty-stream إذا فشلت المطابقة.

(define (check-an-assertion 
         assertion query-pat query-frame)
  (let ((match-result
         (pattern-match 
          query-pat assertion query-frame)))
    (if (eq? match-result 'failed)
        the-empty-stream
        (singleton-stream match-result))))

ويُعيد مُطابِق الأنماط الأساسيّ إمّا الرمز failed وإمّا امتدادًا للإطار المعطى. والفكرة الأساسيّة في المُطابِق هي فحص النمط مقابل البيانات، عنصرًا عنصر، مُتراكمًا روابط لمتغيّرات النمط. فإن كان النمط وكائن البيانات متطابقين، تنجح المطابقة ونُعيد إطار الروابط المتراكم حتّى تلك اللحظة. وإلّا، فإن كان النمط متغيّرًا فإنّنا نُوسّع الإطار الحاليّ بربط المتغيّر بالبيانات، ما دام هذا متّسقًا مع الروابط الموجودة في الإطار أصلًا. وإن كان النمط والبيانات كليهما زوجين، فإنّنا نُطابق (تعاوديًّا) car النمط مقابل car البيانات لإنتاج إطار؛ وفي هذا الإطار نُطابق بعد ذلك cdr النمط مقابل cdr البيانات. فإن لم يكن أيٌّ من هذه الحالات منطبقًا، تفشل المطابقة ونُعيد الرمز failed.

(define (pattern-match pat dat frame)
  (cond ((eq? frame 'failed) 'failed)
        ((equal? pat dat) frame)
        ((var? pat) 
         (extend-if-consistent 
          pat dat frame))
        ((and (pair? pat) (pair? dat))
         (pattern-match 
          (cdr pat) 
          (cdr dat)
          (pattern-match
           (car pat) (car dat) frame)))
        (else 'failed)))

وها هو الإجراء الذي يُوسّع إطارًا بإضافة ربطٍ جديد، إذا كان هذا متّسقًا مع الروابط الموجودة في الإطار أصلًا:

(define (extend-if-consistent var dat frame)
  (let ((binding (binding-in-frame var frame)))
    (if binding
        (pattern-match 
         (binding-value binding) dat frame)
        (extend var dat frame))))

فإذا لم يكن هناك ربطٌ للمتغيّر في الإطار، فإنّنا نُضيف ببساطةٍ ربط المتغيّر بالبيانات. وإلّا، فإنّنا نُطابق، في الإطار، البيانات مقابل قيمة المتغيّر في الإطار. فإن كانت القيمة المخزّونة لا تحتوي إلّا ثوابت، كما يلزم أن يكون الأمر إذا خُزّنت أثناء مطابقة الأنماط بـextend-if-consistent، فإنّ المطابقة تختبر ببساطةٍ ما إذا كانت القيمتان المخزّونة والجديدة متطابقتين. فإن كانتا كذلك، أعادت الإطار غير المعدّل؛ وإلّا، أعادت دلالةً على الفشل. غير أنّ القيمة المخزّونة قد تحتوي متغيّرات نمطٍ إذا خُزّنت أثناء التوحيد (انظر 4.4.4.4). والمطابقة التعاوديّة للنمط المخزّون مقابل البيانات الجديدة ستُضيف روابط متغيّرات هذا النمط أو تفحصها. فمثلًا، لنفترض أنّ لدينا إطارًا يكون ?x فيه مربوطًا بـ(f ?y) و?y غير مربوط، ونرغب في تعزيز هذا الإطار بربط ?x بـ(f b). فنبحث عن ?x ونجد أنّه مربوط بـ(f ?y). وهذا يدفعنا إلى مطابقة (f ?y) مقابل القيمة الجديدة المقترحة (f b) في الإطار ذاته. وفي نهاية المطاف، تُوسّع هذه المطابقة الإطار بإضافة ربط ?y بـb. ويظلّ ?X مرتبطًا بـ(f ?y). ونحن لا نُعدّل ربطًا مخزّونًا قطّ، ولا نخزّن أكثر من ربطٍ واحدٍ لمتغيّرٍ معطًى.

والإجراءات التي يستخدمها extend-if-consistent للتلاعب بالروابط معرَّفةٌ في 4.4.4.8.

الأنماط ذات الذيول المنقّطة

فإذا احتوى نمطٌ على نقطةٍ متبوعةٍ بمتغيّر نمطٍ، فإنّ متغيّر النمط يُطابق باقي قائمة البيانات (لا العنصر التالي من قائمة البيانات)، تمامًا كما يُتوقّع مع ترميز الذيل المنقّط الموصوف في التمرين 2.20. وعلى الرغم من أنّ مُطابِق الأنماط الذي نفّذناه للتوّ لا يبحث عن النقاط، إلّا أنّه يسلك على النحو الذي نريده. وهذا لأنّ الأوّليّة read في Lisp، التي تستخدمها query-driver-loop لقراءة الاستعلام وتمثيله بوصفه بنية قائمة، تتعامل مع النقاط بطريقةٍ خاصّة.

فحين ترى read نقطةً، فإنّها - بدلًا من أن تجعل العنصر التالي هو العنصر التالي من قائمة (أي car لـcons يكون cdr فيه هو باقي القائمة) - تجعله cdr بنية القائمة. فمثلًا، يمكن بناء بنية القائمة التي تُنتجها read للنمط (computer ?type) بتقييم التعبير (cons 'computer (cons '?type '()))، وتلك الخاصّة بالنمط (computer . ?type) بتقييم التعبير (cons 'computer '?type).

وهكذا، فبينما تُقارن pattern-match تعاوديًّا بين cars قائمة بياناتٍ ونمطٍ كانت فيه نقطة وcdrsها، فإنّها في نهاية المطاف تُطابق المتغيّر الذي بعد النقطة (وهو cdr النمط) مقابل قائمةٍ جزئيّةٍ من قائمة البيانات، تربط المتغيّر بتلك القائمة. فمثلًا، مطابقة النمط (computer . ?type) مقابل (computer programmer trainee) ستُطابق ?type مقابل القائمة (programmer trainee).

4.4.4.4 القواعد والتوحيد

وapply-rules هو نظير find-assertions (4.4.4.3) في القواعد. فهو يأخذ كمدخلٍ نمطًا وإطارًا، ويُشكّل مجرى إطارات امتدادٍ بتطبيق قواعدٍ من قاعدة البيانات. وstream-flatmap يُسقط apply-a-rule بمجرى القواعد القابلة للتطبيق المحتملة (المختارة بواسطة fetch-rules، 4.4.4.5) ويُدمج مجاري الإطارات الناتجة.

(define (apply-rules pattern frame)
  (stream-flatmap 
   (lambda (rule)
     (apply-a-rule rule pattern frame))
   (fetch-rules pattern frame)))

ويُطبّق apply-a-rule القواعد باستخدام الطريقة الموصوفة إجمالًا في 4.4.2. فهو يُعزّز أوّلًا إطار وسيطه بتوحيد استنتاج القاعدة مع النمط في الإطار المعطى. فإن نجح ذلك، قيّم جسم القاعدة في هذا الإطار الجديد.

غير أنّ البرنامج - قبل حدوث أيّ من هذا - يُعيد تسمية جميع متغيّرات القاعدة بأسماءٍ جديدةٍ فريدة. والسبب في ذلك هو منع متغيّرات تطبيقات القواعد المختلفة من أن تختلط ببعضها. فمثلًا، إذا كانت قاعدتان تستخدمان كِلتاهما متغيّرًا باسم ?x، فإنّ كلًّا منهما قد يُضيف ربطًا لـ?x إلى الإطار عند تطبيقه. وهذان الـ?x ليس بينهما أيّ صلة، ولا ينبغي أن ننخدع ونظنّ أنّ الربطين يلزم أن يكونا متّسقين. وبدلًا من إعادة تسمية المتغيّرات، كان بإمكاننا ابتكار بنية بيئةٍ أكثر ذكاءً؛ غير أنّ نهج إعادة التسمية الذي اخترناه هنا هو الأكثر مباشرةً، حتّى وإن لم يكن الأكفأ. (انظر التمرين 4.79.) وها هو الإجراء apply-a-rule:

(define (apply-a-rule rule
                      query-pattern
                      query-frame)
  (let ((clean-rule 
         (rename-variables-in rule)))
    (let ((unify-result
           (unify-match query-pattern
                        (conclusion clean-rule)
                        query-frame)))
      (if (eq? unify-result 'failed)
          the-empty-stream
          (qeval (rule-body clean-rule)
                 (singleton-stream 
                  unify-result))))))

والمحدِّدان rule-body وconclusion اللذان يستخرجان أجزاءً من القاعدة معرَّفان في 4.4.4.7.

ونُولّد أسماء متغيّراتٍ فريدةً بربط مُعرّفٍ فريد (كالعدد مثلًا) بكلّ تطبيق قاعدةٍ ودمج هذا المُعرّف مع أسماء المتغيّرات الأصليّة. فمثلًا، إذا كان مُعرّف تطبيق القاعدة هو 7، فقد نُغيِّر كلّ ?x في القاعدة إلى ?x-7 وكلّ ?y في القاعدة إلى ?y-7. (وMake-new-variable وnew-rule-application-id مُدرَجان مع إجراءات الصياغة في 4.4.4.7.)

(define (rename-variables-in rule)
  (let ((rule-application-id 
         (new-rule-application-id)))
    (define (tree-walk exp)
      (cond ((var? exp)
             (make-new-variable 
              exp 
              rule-application-id))
            ((pair? exp)
             (cons (tree-walk (car exp))
                   (tree-walk (cdr exp))))
            (else exp)))
    (tree-walk rule)))

وتُنفَّذ خوارزميّة التوحيد بوصفها إجراءً يأخذ كمدخلاتٍ نمطين وإطارًا ويُعيد إمّا الإطار المُوسَّع وإمّا الرمز failed. والموحِّد يشبه مُطابِق الأنماط إلّا في أنّه متناظر - إذ يُسمح بالمتغيّرات على جانبي المطابقة. وunify-match هو في أساسه ذاته pattern-match، إلّا في أنّه توجد شيفرة إضافيّة (مُعلَّمة بـ«***» أدناه) للتعامل مع الحالة التي يكون فيها الكائن على الجانب الأيمن من المطابقة متغيّرًا.

(define (unify-match p1 p2 frame)
  (cond ((eq? frame 'failed) 'failed)
        ((equal? p1 p2) frame)
        ((var? p1)
         (extend-if-possible p1 p2 frame))
        ((var? p2)
         (extend-if-possible 
          p2 
          p1 
          frame))        ; ***
        ((and (pair? p1) 
              (pair? p2))
         (unify-match 
          (cdr p1) 
          (cdr p2)
          (unify-match 
           (car p1)
           (car p2)
           frame)))
        (else 'failed)))

في التوحيد (unification)، كما في مطابقة الأنماط أحاديّة الجانب، نريد قبول توسيعٍ مقترحٍ للإطار فقط إذا كان متّسقًا مع الروابط القائمة. والإجراء extend-if-possible المستخدم في التوحيد هو ذاته الإجراء extend-if-consistent المستخدم في مطابقة الأنماط، فيما عدا فحصين خاصّين موسومَين بـ«***» في البرنامج أدناه. في الحالة الأولى، إذا كان المتغيّر الذي نحاول مطابقته غير مقيّد، لكنّ القيمة التي نحاول مطابقته بها هي بدورها متغيّرٌ (مختلف)، فلا بدّ من التحقّق ممّا إذا كانت القيمة مقيّدة، وإذا كانت كذلك، فمطابقة قيمتها. أمّا إذا كان طرفا المطابقة كلاهما غير مقيّد، فيمكننا ربط أيّهما بالآخر.

يتناول الفحص الثاني محاولات ربط متغيّرٍ بنمطٍ يتضمّن ذلك المتغيّر. ومثل هذا الموقف يمكن أن يقع كلّما تكرّر متغيّرٌ في النمطين كليهما. فتأمّل، مثلًا، توحيد النمطين (?x ?x) و(?y ⟨expression involving ?y⟩) في إطارٍ يكون فيه ?x و?y كلاهما غير مقيّد. أوّلًا تُطابَق ?x مقابل ?y، فينشأ رابطٌ يربط ?x بـ?y. ثم تُطابَق ?x ذاتها مقابل التعبير المعطى المتعلّق بـ?y. وبما أنّ ?x مقيّدةٌ سلفًا بـ?y، فإنّ ذلك يؤدّي إلى مطابقة ?y مقابل التعبير. فإن نظرنا إلى المُوحِّد بوصفه يجد مجموعةً من القيم لمتغيّرات النمط تجعل النمطين متطابقين، فإنّ هذين النمطين يعنيان تعليماتٍ بإيجاد ?y بحيث تساوي ?y التعبير المتعلّق بـ?y. ولا توجد طريقةٌ عامّةٌ لحلّ مثل هذه المعادلات، لذلك نرفض هذه الروابط؛ وتُتعرَّف هذه الحالات بالمسيّم depends-on?.[80] ومن جهةٍ أخرى، فإنّنا لا نريد رفض محاولات ربط متغيّرٍ بذاته. فمثلًا، تأمّل توحيد (?x ?x) و(?y ?y). إنّ المحاولة الثانية لربط ?x بـ?y تُطابِق ?y (القيمة المخزّنة لـ?x) مقابل ?y (القيمة الجديدة لـ?x). وهذا ما تعالجه بند equal? من unify-match.

(define (extend-if-possible var val frame)
  (let ((binding (binding-in-frame var frame)))
    (cond (binding
           (unify-match
            (binding-value binding) val frame))
          ((var? val)                   ; ***
           (let ((binding 
                  (binding-in-frame 
                   val
                   frame)))
             (if binding
                 (unify-match
                  var 
                  (binding-value binding) 
                  frame)
                 (extend var val frame))))
          ((depends-on? val var frame)  ; ***
           'failed)
          (else (extend var val frame)))))

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

(define (depends-on? exp var frame)
  (define (tree-walk e)
    (cond ((var? e)
           (if (equal? var e)
               true
               (let
                 ((b (binding-in-frame 
                      e 
                      frame)))
                  (if b
                      (tree-walk 
                       (binding-value b))
                      false))))
          ((pair? e)
           (or (tree-walk (car e))
               (tree-walk (cdr e))))
          (else false)))
  (tree-walk exp))

4.4.4.5 صيانة قاعدة البيانات

إنّ إحدى المسائل المهمّة في تصميم لغات البرمجة المنطقيّة هي ترتيب الأمور بحيث يُفحَص أقلّ عددٍ ممكنٍ من مدخلات قاعدة البيانات (data base) غير المتعلّقة بالموضوع عند فحص نمطٍ معطى. وفي نظامنا، فبالإضافة إلى تخزين جميع التأكيدات في مجرى واحد كبير، نخزّن جميع التأكيدات التي تكون cars الخاصة بها رموزًا ثابتةً في مجاري منفصلة، في جدولٍ مفهرسٍ بالرمز. فحتى نحضر تأكيدًا قد يطابق نمطًا، نتحقّق أوّلًا ممّا إذا كان car النمط رمزًا ثابتًا. فإن كان كذلك، نُعيد (لكي يُختبر باستخدام المُطابِق) جميع التأكيدات المخزّنة التي لها car المطابق. وأمّا إذا لم يكن car النمط رمزًا ثابتًا، فنُعيد جميع التأكيدات المخزّنة. ويمكن لطرائقَ أذكى أن تستثمر المعلومات الواردة في الإطار أيضًا، أو أن تحاول تحسين الحالة التي لا يكون فيها car النمط رمزًا ثابتًا. ونحن نتجنّب بناء معايير الفهرسة (باستخدام car، ومعالجة حالة الرموز الثابتة وحدها) في البرنامج؛ وبدلًا من ذلك نستدعي مسيّماتٍ ومحدِّداتٍ تجسّد معاييرنا.

(define THE-ASSERTIONS the-empty-stream)

(define (fetch-assertions pattern frame)
  (if (use-index? pattern)
      (get-indexed-assertions pattern)
      (get-all-assertions)))

(define (get-all-assertions) THE-ASSERTIONS)

(define (get-indexed-assertions pattern)
  (get-stream (index-key-of pattern)
              'assertion-stream))

يبحث الإجراء get-stream عن مجرى في الجدول ويُعيد مجرى فارغًا إن لم يُخزَّن هناك شيء.

(define (get-stream key1 key2)
  (let ((s (get key1 key2)))
    (if s s the-empty-stream)))

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

(define THE-RULES the-empty-stream)

(define (fetch-rules pattern frame)
  (if (use-index? pattern)
      (get-indexed-rules pattern)
      (get-all-rules)))

(define (get-all-rules) THE-RULES)

(define (get-indexed-rules pattern)
  (stream-append
   (get-stream (index-key-of pattern)
               'rule-stream)
   (get-stream '? 'rule-stream)))

يُستخدم الإجراء add-rule-or-assertion! من قِبل query-driver-loop لإضافة التأكيدات والقواعد إلى قاعدة البيانات. ويُخزَّن كلّ عنصر في الفهرس، إن كان مناسبًا، وفي مجرى جميع التأكيدات أو القواعد في قاعدة البيانات.

(define (add-rule-or-assertion! assertion)
  (if (rule? assertion)
      (add-rule! assertion)
      (add-assertion! assertion)))

(define (add-assertion! assertion)
  (store-assertion-in-index assertion)
  (let ((old-assertions THE-ASSERTIONS))
    (set! THE-ASSERTIONS
          (cons-stream assertion 
                       old-assertions))
    'ok))

(define (add-rule! rule)
  (store-rule-in-index rule)
  (let ((old-rules THE-RULES))
    (set! THE-RULES
          (cons-stream rule old-rules))
    'ok))

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

(define (store-assertion-in-index assertion)
  (if (indexable? assertion)
      (let ((key (index-key-of assertion)))
        (let ((current-assertion-stream
               (get-stream 
                key 'assertion-stream)))
          (put key
               'assertion-stream
               (cons-stream 
                assertion
                current-assertion-stream))))))

(define (store-rule-in-index rule)
  (let ((pattern (conclusion rule)))
    (if (indexable? pattern)
        (let ((key (index-key-of pattern)))
          (let ((current-rule-stream
                 (get-stream 
                  key 'rule-stream)))
            (put key
                 'rule-stream
                 (cons-stream 
                  rule
                  current-rule-stream)))))))

تُعرِّف الإجراءات الآتية كيفيّة استخدام فهرس قاعدة البيانات. وسيُخزَّن النمط (تأكيدٌ أو استنتاج قاعدة) في الجدول إذا بدأ بمتغيّرٍ أو برمزٍ ثابت.

(define (indexable? pat)
  (or (constant-symbol? (car pat))
      (var? (car pat))))

إنّ المفتاح الذي يُخزَّن النمط تحته في الجدول هو إمّا ? (إذا بدأ بمتغيّر) وإمّا الرمز الثابت الذي يبدأ به.

(define (index-key-of pat)
  (let ((key (car pat)))
    (if (var? key) '? key)))

وسيُستخدم الفهرس لاسترجاع العناصر التي قد تطابق نمطًا إذا بدأ النمط برمزٍ ثابت.

(define (use-index? pat)
  (constant-symbol? (car pat)))

التمرين 4.70: ما الغرض من روابط let في الإجراءين add-assertion! وadd-rule!؟ وما الخطأ في التنفيذ الآتي لـadd-assertion!؟ تلميح: تذكّر تعريف مجرى الآحاد اللانهائي الوارد في 3.5.2: (define ones (cons-stream 1 ones)).

(define (add-assertion! assertion)
  (store-assertion-in-index assertion)
  (set! THE-ASSERTIONS
        (cons-stream assertion 
                     THE-ASSERTIONS))
  'ok)

4.4.4.6 عمليّات المجاري

يستخدم نظام الاستعلام بضعة عمليّاتٍ على المجاري لم تُعرض في الفصل 3.

الإجراءان stream-append-delayed وinterleave-delayed هما بمنزلة stream-append وinterleave (3.5.3)، إلّا أنّهما يأخذان معطىً مؤجّلًا (كمثل الإجراء integral في 3.5.4). وهذا يؤخّر الدوران في بعض الحالات (انظر التمرين 4.71).

(define (stream-append-delayed s1 delayed-s2)
  (if (stream-null? s1)
      (force delayed-s2)
      (cons-stream
       (stream-car s1)
       (stream-append-delayed (stream-cdr s1)
                              delayed-s2))))

(define (interleave-delayed s1 delayed-s2)
  (if (stream-null? s1)
      (force delayed-s2)
      (cons-stream
       (stream-car s1)
       (interleave-delayed 
        (force delayed-s2)
        (delay (stream-cdr s1))))))

Stream-flatmap، الذي يُستخدم في كلّ مواضع مُقيِّم الاستعلام لإسقاط إجراءٍ على مجرى إطاراتٍ ودمج مجاري الإطارات الناتجة، هو نظير الإجراء flatmap بين المجاري، وهو الإجراء الذي استُحدث للقوائم الاعتياديّة في 2.2.3. ولكن بخلاف flatmap الاعتياديّ، فإنّنا نُراكم المجاري بعمليّة تشابكٍ، بدلًا من إلحاقها إلحاقًا بسيطًا (انظر التمرين 4.72 والتمرين 4.73).

(define (stream-flatmap proc s)
  (flatten-stream (stream-map proc s)))

(define (flatten-stream stream)
  (if (stream-null? stream)
      the-empty-stream
      (interleave-delayed
       (stream-car stream)
       (delay (flatten-stream
               (stream-cdr stream))))))

ويستخدم المُقيِّم أيضًا الإجراء البسيط الآتي لتوليد مجرىً يتألّف من عنصرٍ وحيد:

(define (singleton-stream x)
  (cons-stream x the-empty-stream))

4.4.4.7 إجراءات نحو الاستعلام

الإجراءان type وcontents، اللذان يستخدمهما qeval (4.4.4.2)، يُحدّدان أنّ الصيغة الخاصّة تُعرَف بالرمز الواقع في car الخاصّ بها. وهما مثلهما مثل الإجراءين type-tag وcontents في 2.4.2، فيما عدا رسالة الخطأ.

(define (type exp)
  (if (pair? exp)
      (car exp)
      (error "Unknown expression TYPE"
             exp)))

(define (contents exp)
  (if (pair? exp)
      (cdr exp)
      (error "Unknown expression CONTENTS"
             exp)))

والإجراءات الآتية، التي يستخدمها query-driver-loop (في 4.4.4.1)، تُحدّد أنّ القواعد والتأكيدات تُضاف إلى قاعدة البيانات بتعابيرٍ على الصورة (assert! ⟨rule-or-assertion⟩):

(define (assertion-to-be-added? exp)
  (eq? (type exp) 'assert!))

(define (add-assertion-body exp)
  (car (contents exp)))

وها هي تعريفات النحو للصيغ الخاصّة and وor وnot وlisp-value (4.4.4.2):

(define (empty-conjunction? exps) (null? exps))
(define (first-conjunct exps) (car exps))
(define (rest-conjuncts exps) (cdr exps))
(define (empty-disjunction? exps) (null? exps))
(define (first-disjunct exps) (car exps))
(define (rest-disjuncts exps) (cdr exps))
(define (negated-query exps) (car exps))
(define (predicate exps) (car exps))
(define (args exps) (cdr exps))

والإجراءات الثلاثة الآتية تُعرّف نحو القواعد:

(define (rule? statement)
  (tagged-list? statement 'rule))

(define (conclusion rule) (cadr rule))

(define (rule-body rule)
  (if (null? (cddr rule))
      '(always-true)
      (caddr rule)))

يستدعي query-driver-loop (4.4.4.1) الإجراء query-syntax-process ليحوّل متغيّرات الأنماط في التعبير، التي على الصورة ?symbol، إلى الصيغة الداخليّة (? symbol). أي أنّ نمطًا مثل (job ?x ?y) يمثّله النظام داخليًّا في الحقيقة على الصورة (job (? x) (? y)). وهذا يزيد من كفاءة معالجة الاستعلامات، إذ يعني أنّ النظام يستطيع التحقّق ممّا إذا كان التعبير متغيّر نمطٍ بفحص ما إذا كان car التعبير هو الرمز ?، بدلًا من اضطراره إلى استخراج المحارف من الرمز. ويُنجَز تحويل النحو بالإجراء الآتي:[81]

(define (query-syntax-process exp)
  (map-over-symbols expand-question-mark exp))

(define (map-over-symbols proc exp)
  (cond ((pair? exp)
         (cons (map-over-symbols 
                proc (car exp))
               (map-over-symbols 
                proc (cdr exp))))
        ((symbol? exp) (proc exp))
        (else exp)))

(define (expand-question-mark symbol)
  (let ((chars (symbol->string symbol)))
    (if (string=? (substring chars 0 1) "?")
        (list '? (string->symbol
                  (substring
                   chars 
                   1 
                   (string-length chars))))
        symbol)))

وبمجرّد تحويل المتغيّرات على هذا النحو، تصير متغيّرات النمط قوائم تبدأ بـ?، وتكون الرموز الثابتة (التي يلزم التعرّف عليها لفهرسة قاعدة البيانات، 4.4.4.5) هي الرموز فحسب.

(define (var? exp) (tagged-list? exp '?))
(define (constant-symbol? exp) (symbol? exp))

وتُبنى المتغيّرات الفريدة أثناء تطبيق القواعد (في 4.4.4.4) بوساطة الإجراءات الآتية. والمعرّف الفريد لتطبيق قاعدةٍ هو عددٌ يُزاد كلّما طُبِّقت قاعدة.

(define rule-counter 0)

(define (new-rule-application-id)
  (set! rule-counter (+ 1 rule-counter))
  rule-counter)

(define (make-new-variable 
         var rule-application-id)
  (cons '? (cons rule-application-id
                 (cdr var))))

وحين يُثبِّت query-driver-loop الاستعلام لطباعة الجواب، فإنّه يحوّل أيّ متغيّرات أنماطٍ غير مربوطةٍ عودةً إلى الصيغة الملائمة للطبع، مستخدمًا

(define (contract-question-mark variable)
  (string->symbol
   (string-append "?"
     (if (number? (cadr variable))
         (string-append
          (symbol->string (caddr variable))
          "-"
          (number->string (cadr variable)))
         (symbol->string (cadr variable))))))

4.4.4.8 الإطارات والروابط

تُُمثَّل الإطارات كقوائمٍ من الروابط، والرابط زوجٌ متغيّر-قيمة:

(define (make-binding variable value)
  (cons variable value))

(define (binding-variable binding)
  (car binding))

(define (binding-value binding)
  (cdr binding))

(define (binding-in-frame variable frame)
  (assoc variable frame))

(define (extend variable value frame)
  (cons (make-binding variable value) frame))

التمرين 4.71: يتساءل لويس ريزنر عن السبب في أنّ الإجراءين simple-query وdisjoin (4.4.4.2) نُفِّذا باستخدام عمليّات delay صريحة، بدلًا من تعريفهما على النحو الآتي:

(define (simple-query 
         query-pattern frame-stream)
  (stream-flatmap
   (lambda (frame)
     (stream-append
      (find-assertions query-pattern frame)
      (apply-rules query-pattern frame)))
   frame-stream))

(define (disjoin disjuncts frame-stream)
  (if (empty-disjunction? disjuncts)
      the-empty-stream
      (interleave
       (qeval (first-disjunct disjuncts)
              frame-stream)
       (disjoin (rest-disjuncts disjuncts)
                frame-stream))))

هل تستطيع إعطاء أمثلةً على استعلاماتٍ تؤدّي فيها هذه التعريفات الأبسط إلى سلوكٍ غير مرغوبٍ فيه؟

التمرين 4.72: لماذا يشابك disjoin وstream-flatmap المجاري بدلًا من إلحاقها إلحاقًا بسيطًا؟ أعطِ أمثلةً تُبيّن لماذا يعمل التشابك على نحوٍ أفضل. (تلميح: لماذا استخدمنا interleave في 3.5.3؟)

التمرين 4.73: لماذا يستخدم flatten-stream الإجراء delay صراحةً؟ وما الخطأ في تعريفه على النحو الآتي:

(define (flatten-stream stream)
  (if (stream-null? stream)
      the-empty-stream
      (interleave (stream-car stream)
                  (flatten-stream 
                   (stream-cdr stream)))))

التمرين 4.74: تقترح أليسا ب. هاكر استخدام نسخةٍ أبسط من stream-flatmap في negate وlisp-value وfind-assertions. فهي تلاحظ أنّ الإجراء الذي يُسقَط على مجرى الإطارات في هذه الحالات يُنتج دائمًا إمّا المجرى الفارغ وإمّا مجرى ذي عنصرٍ وحيد، فلا حاجة إلى التشابك عند دمج هذه المجاري.

أكمِل التعابير الناقصة في برنامج أليسا.

(define (simple-stream-flatmap proc s)
  (simple-flatten (stream-map proc s)))

(define (simple-flatten stream)
  (stream-map ⟨??⟩
              (stream-filter ⟨??⟩ 
                             stream)))

هل يتغيّر سلوك نظام الاستعلام إذا غيّرناه على هذا النحو؟

التمرين 4.75: نفّذ للغة الاستعلام صيغةً خاصّةً جديدةً تُسمّى unique. وينبغي لـunique أن تنجح إذا وُجد عنصرٌ واحدٌ بالضبط في قاعدة البيانات يُرضي استعلامًا معيَّنًا. فمثلًا،

(unique (job ?x (computer wizard)))

ينبغي أن يطبع مجرى ذي عنصرٍ واحد

(unique (job (Bitdiddle Ben)
             (computer wizard)))

إذ إنّ بن هو ساحر الحاسوب الوحيد، و

(unique (job ?x (computer programmer)))

ينبغي أن يطبع المجرى الفارغ، إذ يوجد أكثر من مبرمج حاسوبٍ واحد. فضلًا عن ذلك،

(and (job ?x ?j) 
     (unique (job ?anyone ?j)))

ينبغي أن يسرد جميع الوظائف التي يشغلها شخصٌ واحدٌ فقط، والأشخاص الذين يشغلونها.

ثمّة جزءان لتنفيذ unique. الأوّل هو كتابة إجراءٍ يتعامل مع هذه الصيغة الخاصّة، والثاني هو جعل qeval يُوجّه نداءاته إلى ذلك الإجراء. والجزء الثاني تافه، إذ إنّ qeval يؤدّي توجيهه بطريقةٍ موجَّهةٍ بالبيانات. فإذا سُمِّي إجراءك uniquely-asserted، فكلّ ما عليك فعله هو

(put 'unique 'qeval uniquely-asserted)

وسيُوجِّهُ qeval إلى هذا الإجراء لكلّ استعلامٍ type (car) الخاصّ به هو الرمز unique.

المسألة الحقيقيّة هي كتابة الإجراء uniquely-asserted. وينبغي له أن يأخذ كمعطى contents (cdr) استعلام unique، مع مجرى إطارات. فلكلّ إطارٍ في المجرى، ينبغي له أن يستخدم qeval لإيجاد مجرى جميع الامتدادات للإطار التي تُرضي الاستعلام المعطى. وأيّ مجرىً لا يحوي عنصرًا واحدًا بالضبط ينبغي حذفه. وينبغي أن تُعاد المجاري الباقية لتُراكم في مجرىً كبيرٍ واحدٍ هو نتيجة استعلام unique. وهذا شبيهٌ بتنفيذ الصيغة الخاصّة not.

اختبر تنفيذك بتشكيل استعلامٍ يسرد جميع الأشخاص الذين يشرفون على شخصٍ واحدٍ بالضبط.

التمرين 4.76: إنّ تنفيذنا لـand بوصفه تركيبًا متسلسلًا للاستعلامات (الشكل 4.5) أنيق، لكنّه غير كفؤ لأنّنا في معالجة الاستعلام الثاني من and يتعيّن علينا أن نمسح قاعدة البيانات لكلّ إطارٍ يُنتجه الاستعلام الأوّل. فإذا كانت قاعدة البيانات تحوي nn عنصرًا، وأنتج استعلامٌ نموذجيّ عددًا من الإطارات الناتجة متناسبًا مع nn (ليكن n/kn / k )، فإنّ مسح قاعدة البيانات لكلّ إطارٍ يُنتجه الاستعلام الأوّل سيتطلّب n2/kn^{2} / k نداءً إلى مطابق الأنماط. وكان من شأن منهجٍ آخر أن يعالج بندَي and كلًّا على حدة، ثم يبحث عن جميع أزواج الإطارات الناتجة المتوافقة. فإذا أنتج كلّ استعلامٍ n/kn / k إطارًا ناتجًا، فإنّ ذلك يعني أنّ علينا أداء n2/k2n^{2} / k^{2} فحصًا للتوافق—أي أقلّ بمقدار عامل kk من عدد المطابقات التي تتطلّبها طريقتنا الحاليّة. ابتكر تنفيذًا لـand يستخدم هذه الاستراتيجيّة. ويجب عليك أن تنفّذ إجراءً يأخذ إطارين كمعطيين، ويفحص ما إذا كانت الروابط في الإطارين متوافقة، وإذا كان الأمر كذلك يُنتج إطارًا يدمج مجموعتَي الروابط. وهذه العمليّة شبيهةٌ بالتوحيد.

التمرين 4.77: رأينا في 4.4.3 أنّ not وlisp-value قد يجعلان لغة الاستعلام تُعطي أجوبةً «خاطئة» إذا طُبِّقت هاتان عمليّتا الترشيح على إطاراتٍ متغيّراتها غير مربوطة. فكّر في طريقةٍ لعلاج هذا القصور. وإحدى الأفكار هي أداء الترشيح بطريقةٍ «مؤجّلة» بإلحاق «وعدٍ» بالترشيح بالإطار لا يُوفى به إلّا حين تُربط متغيّراتٌ بقدرٍ يكفي لجعل العمليّة ممكنة. وكان في وسعنا أن ننتظر أداء الترشيح حتّى تُؤدّى جميع العمليّات الأخرى. غير أنّنا، حرصًا على الكفاءة، نودّ أن نُؤدّي الترشيح في أسرع وقتٍ ممكنٍ حتّى نقلّل عدد الإطارات الوسيطة المتولّدة.

التمرين 4.78: أعد تصميم لغة الاستعلام بوصفها برنامجًا غير حتميّ ليُنفَّذ باستخدام مُقيِّم 4.3، بدلًا من جعلها عمليّة مجاري. وفي هذا المنهج، سيُنتج كلّ استعلامٍ جوابًا وحيدًا (بدلًا من مجرى جميع الأجوبة) ويستطيع المستخدم أن يكتب try-again ليرى المزيد من الأجوبة. وينبغي أن تجد أنّ كثيرًا من الآليّة التي بنيناها في هذا القسم يستوعبه البحث غير الحتميّ والتتبّع الرجعيّ. غير أنّك ستجد على الأرجح أيضًا أنّ لغة الاستعلام الجديدة لديك لها فروقًا دقيقةً في السلوك عن تلك المنفَّذة هنا. فهل تستطيع إيجاد أمثلةٍ توضّح هذا الفرق؟

التمرين 4.79: حين نفّذنا مُقيّم Lisp في 4.1، رأينا كيف نستخدم البيئات المحلّيّة لتجنّب تعارض الأسماء بين وسائط الإجراءات. فمثلًا، في تقييم

(define (square x) 
  (* x x))

(define (sum-of-squares x y)
  (+ (square x) (square y)))

(sum-of-squares 3 4)

لا يقع أيّ التباس بين x في square وx في sum-of-squares، لأنّنا نقيّم جسم كلّ إجراءٍ في بيئةٍ مبنيّةٍ خصيصًا لتحتوي روابط المتغيّرات المحلّيّة. أمّا في نظام الاستعلام، فقد استخدمنا استراتيجيّة مختلفةً لتجنّب تعارض الأسماء عند تطبيق القواعد. ففي كلّ مرّةٍ نطبّق فيها قاعدةً نُعيد تسمية المتغيّرات بأسماءٍ جديدةٍ مضمونٌ أنّها فريدة. وكانت الاستراتيجيّة المماثلة لمُقيّم Lisp هي الاستغناء عن البيئات المحلّيّة وإعادة تسمية المتغيّرات في جسم الإجراء ببساطةٍ في كلّ مرّةٍ نطبّق فيها الإجراء.

نفّذ للغة الاستعلام طريقةَ تطبيق قواعدٍ تستخدم البيئات بدلًا من إعادة التسمية. وانظر إن كنت تستطيع البناء على بنية بيئتك لإنشاء تراكيبَ في لغة الاستعلام للتعامل مع النظم الكبيرة، مثل نظير القاعدة للإجراءات المبنيّة ببنية الكتل. فهل تستطيع ربط أيّ من هذا بمسألة إجراء الاستنتاجات في سياقٍ (مثلًا، «لو افترضت أنّ PP صحيحة، لكنت أستطيع استنتاج AA وBB .») بوصفها طريقةً لحلّ المسائل؟ (وهذه المسألة مفتوحة النهايات. والجواب الجيّد يستحقّ على الأرجح درجة دكتوراه.)


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

  2. وأهمّ الخصائص التي يُغفلها مُقيّمنا هي آليّات معالجة الأخطاء ودعم التنقيح. ولمناقشةٍ أوسع للمُقيّمين، انظر فريدمان وآخرون 1992، الذي يعرض لغات البرمجة عبر تسلسلٍ من مُقيّماتٍ مكتوبةٍ بلغة Scheme. ↩︎

  3. وحتى مع ذلك، فستبقى جوانب مهمّة من عمليّة التقييم لا يجلّيها مُقيّمنا. وأهمّ هذه الجوانب هي الآليّات التفصيليّة التي تنادي بها الإجراءاتُ بعضُها بعضًا وتُعيد القيم إلى مناداتها. وسنعالج هذه القضايا في الفصل 5، حيث نُمعن النظر في عمليّة التقييم بتنفيذ المُقيّم بوصفه آلة مسجّلاتٍ بسيطة. ↩︎

  4. فإذا منحنا أنفسنا القدرة على تطبيق الأوّليّات، فما الذي يبقى علينا تنفيذه في المُقيّم؟ فمهمّة المُقيّم ليست تحديد أوّليّات اللغة، بل توفير النسيج الضّام—وسائل التركيب ووسائل التجريد—الذي يربط مجموعةً من الأوّليّات ليشكّل لغة. وعلى وجه الخصوص: - يتيح لنا المُقيّم التعامل مع التعابير المتداخلة. فمثلًا، على الرغم من أنّ تطبيق الأوّليّات ببساطةٍ يكفي لتقييم التعبير (+ 1 6)، فإنّه غير كافٍ للتعامل مع (+ 1 (* 2 3)). فمن وجهة نظر الإجراء الأوّليّ +، ينبغي أن تكون معطياته أعدادًا، وكان ليختنق لو مرّرنا إليه التعبير (* 2 3) كمعطى. وأحد الأدوار المهمّة للمُقيّم هو تنسيق تركيب الإجراءات بحيث يُختزل (* 2 3) إلى 6 قبل أن يُمرَّر كمعطىً إلى +. - ويتيح لنا المُقيّم استخدام المتغيّرات. فمثلًا، ليس للإجراء الأوّليّ للجمع أيّة طريقةٍ للتعامل مع تعابيرَ مثل (+ x 1). فنحن نحتاج إلى مُقيّمٍ يتعقّب المتغيّرات ويحصل على قيمها قبل استدعاء الإجراءات الأوّليّة. - ويتيح لنا المُقيّم تعريف الإجراءات المركّبة. وهذا يشمل تعقّب تعريفات الإجراءات، ومعرفة كيفيّة استخدام هذه التعريفات في تقييم التعابير، وتوفير آليّةٍ تُمكّن الإجراءات من قبول المعطيات. - ويوفّر المُقيّم الصيغ الخاصّة، التي ينبغي تقييمها بطرائق مختلفة عن نداءات الإجراءات. ↩︎

  5. كان في وسعنا تبسيط بند application? في eval باستخدام map (وبالنصّ على أنّ operands تُعيد قائمةً) بدلًا من كتابة إجراء list-of-values صريح. وقد اخترنا ألّا نستخدم map هنا للتأكيد على حقيقة أنّ المُقيّم يمكن تنفيذه دون أيّ استخدامٍ للإجراءات ذات الرتبة العليا (وبالتالي يمكن كتابته بلغةٍ لا تمتلك إجراءاتٍ ذات رتبةٍ عاليا)، حتّى وإن كانت اللغة التي يدعمها ستضمّ إجراءاتٍ ذات رتبةٍ عاليا. ↩︎

  6. وفي هذه الحالة، اللغة المنفَّذة ولغة التنفيذ هما اللغّة ذاتها. والتأمّل في معنى true? هنا يُفضي إلى اتّساع الوعي دون إساءة استخدام الموادّ. ↩︎

  7. إنّ هذا التنفيذ لـdefine يُغفل مسألةً دقيقةً في التعامل مع التعريفات الداخليّة، وإن كان يعمل بصورةٍ صحيحةٍ في معظم الحالات. وسنرى ما المسألة وكيفيّة حلّها في 4.1.6. ↩︎

  8. كما قلنا حين عرّفنا define وset!، فإنّ هذه القيم تابعةٌ للتنفيذ في Scheme—أي أنّ المنفّذ يستطيع اختيار القيمة التي تُعاد. ↩︎

  9. كما ورد في 2.3.1، فإنّ المُقيّم يرى التعبير المُقتبَس كقائمةٍ تبدأ بـquote، حتّى لو كُتب التعبير بعلامة الاقتباس. فمثلًا، فإنّ التعبير 'a سيراه المُقيّم على الصورة (quote a). انظر التمرين 2.55. ↩︎

  10. قيمةُ تعبير if حين يكون المسيّم خاطئًا ولا يوجد بديلٌ غير محدّدةٍ في Scheme؛ وقد اخترنا ها هنا جعلها خاطئة. وسندعم استخدام المتغيّرين true وfalse في التعابير التي تُقيَّم بربطهما في البيئة العامّة. انظر 4.1.4. ↩︎

  11. إنّ هذه المحدِّدات لقائمة التعابير—وتلك المقابلة لها لقائمة العوامل—ليست مقصودةً بوصفها تجريد بيانات. وهي مُستحدَثةٌ بوصفها أسماءً تذكيريّةً لعمليّات القوائم الأساسيّة، لتسهيل فهم مُقيّم التحكّم الصريح الوارد في 5.4. ↩︎

  12. قيمةُ تعبير cond حين تكون جميع المسيّمات خاطئة ولا يوجد بند else غير محدّدةٍ في Scheme؛ وقد اخترنا ها هنا جعلها خاطئة. ↩︎

  13. تُوفّر منظومات لisp العمليّة آليّةً تسمح للمُستخدِم بإضافة تعابير مشتقّة جديدة وتحديد تنفيذها بوصفه تحويلات صياغيّة دون تعديل المقيّم. ويُسمّى مثل هذا التحويل المعرَّف من المُستخدِم ماكرو. وعلى الرغم من أنّ إضافة آليّة أوّليّة لتعريف الماكروهات سهلة، إلّا أنّ اللغة الناتجة لها مشكلات دقيقة في تعارض الأسماء. وقد كان هناك بحثٌ كثير في آليّات تعريف الماكرو التي لا تُسبّب هذه الصعوبات. انظر، مثلًا، Kohlbecker 1986، Clinger and Rees 1991، و Hanson 1991. ↩︎

  14. ليست الإطارات تجريد بيانات حقيقيًّا في الشيفرة التالية: فـ set-variable-value! و define-variable! تستخدمان set-car! لتعديل القيم في إطار مباشرةً. والغرض من إجراءات الإطار هو تسهيل قراءة إجراءات التلاعب بالبيئة. ↩︎

  15. عيب هذا التمثيل (وكذلك التنويعة الواردة في التمرين 4.11) هو أنّ المقيّم قد يضطرّ إلى البحث في إطارات كثيرة لإيجاد الربط الخاصّ بمتغيّر مُعطى. (ويُشار إلى مثل هذا الأسلوب بـ الربط العميق.) وإحدى طرق تجنّب هذا العجز في الكفاءة هي الاستعانة باستراتيجيّة تُسمّى العنونة المعجميّة، التي سنناقشها في 5.5.6. ↩︎

  16. يُمكن استخدام أيّ إجراء معرَّف في الليسب الأساسيّ كأوّليّ للمقيّم الحلقيّ الوسيط. ولا يلزم أن يكون اسم الأوّليّة المثبَّتة في المقيّم هو نفس اسم تنفيذها في الليسب الأساسيّ؛ والأسماء هي نفسها هنا لأنّ المقيّم الحلقيّ الوسيط يُنفّذ Scheme نفسه. بالتالي، مثلًا، كان في وسعنا أن نضع (list 'first car) أو (list 'square (lambda (x) (* x x))) في قائمة primitive-procedures. ↩︎

  17. إنّ apply-in-underlying-scheme هو إجراء apply الذي استخدمناه في الفصول السابقة. أمّا إجراء apply الخاصّ بالمقيّم الحلقيّ الوسيط (4.1.1) فينمذج عمل هذه الأوّليّة. وإنّ وجود شيئين مختلفين يُسمّيان apply يؤدّي إلى مشكلة تقنيّة في تشغيل المقيّم الحلقيّ الوسيط، لأنّ تعريف apply الخاصّ بالمقيّم الحلقيّ الوسيط سيُخفي تعريف الأوّليّة. وإحدى سُبل تجاوز ذلك هي إعادة تسمية apply الحلقيّ الوسيط لتجنّب التعارض مع اسم الإجراء الأوّليّ. وقد افترضنا بدلًا من ذلك أنّنا حفظنا مرجعًا إلى apply الأساسيّ بفعل scheme (define apply-in-underlying-scheme apply) ↩︎

  18. ينتظر الإجراء الأوّليّ read الدخل من المُستخدِم، ويُعيد أوّل تعبير كامل يُطبَع. فعلى سبيل المثال، إذا طبع المُستخدِم (+ 23 x)، فإنّ read يُعيد قائمةً من ثلاثة عناصر تحتوي الرمز + والعدد 23 والرمز x. وإذا طبع المُستخدِم 'x، فإنّ read يُعيد قائمةً من عنصرين تحتوي الرمز quote والرمز x. ↩︎

  19. حقيقة أنّ الآلات موصوفة بلغة لisp غير جوهرية. فإذا أعطينا مقيّمنا برنامج لisp يتصرّف بصفته مقيّمًا لغة أخرى، لنقل C، فإنّ مقيّم لisp سيُحاكي مقيّم C، الذي يستطيع بدوره محاكاة أيّ آلة موصوفة كبرنامج C. وبالمثل، فإنّ كتابة مقيّم لisp بلغة C تُنتج برنامج C قادرًا على تنفيذ أيّ برنامج لisp. والفكرة العميقة هنا هي أنّ أيّ مقيّم يُمكنه محاكاة أيّ مقيّم آخر. وبالتالي، فإنّ مفهوم «ماذا يُمكن حسابه مبدئيًا» (متجاهلين اعتبارات الوقت والذاكرة العمليّة المطلوبة) مستقلّ عن اللغة أو الحاسوب، بل يعبّر بدلًا من ذلك عن مفهوم أساسيّ يُسمّى القابليّة للحساب. وقد أُثبِثَ هذا أوّل مرّة بطريقة واضحة على يد آلان م. تورينغ (1912-1954)، الذي وضعت ورقتهResearch عام 1936 أسس علم الحاسوب النظريّ. وفي الورقة، قدّم تورينغ نموذجًا حسابيًّا بسيطًا — يُعرَف الآن بـ آلة تورينغ — واحتجّ بأنّ أيّ «عمليّة فعّالة» يُمكن صياغتها كبرنامج لمثل هذه الآلة. (وهذه الاحتجاجة تُعرَف بـ أطروحة تشرتش-تورينغ.) ثمّ نفّذ تورينغ آلة عامّة، أي آلة تورينغ تتصرف بصفتها مقيّمًا لبرامج آلات تورينغ. واستخدم هذا الإطار ليُظهر وجود مسائل مُصوَّغة صياغةً سليمة لا يُمكن حسابها بآلات تورينغ (انظر التمرين 4.15)، وبالتالي — بالاستلزام — لا يُمكن صياغتها بوصفها «عمليّات فعّالة». وواصل تورينغ ليُقدّم إسهامات أساسيّة في علم الحاسوب العمليّ أيضًا. فعلى سبيل المثال، اخترع فكرة تنظيم البرامج باستخدام إجراءات فرعيّة عامّة الأغراض. انظر Hodges 1983 من أجل سيرة تورينغ. ↩︎

  20. يجد بعض الناس أنّ من غير البديهيّ أنّ مقيّمًا، يُنفَّذ بإجراء بسيط إلى حدٍّ ما، يُمكنه محاكاة برامج أكثر تعقيدًا من المقيّم ذاته. ووجود آلة مقيّم عامّة خاصّيّة عميقة ورائعة في الحساب. أمّا نظريّة التعاود، فرعٌ من المنطق الرياضيّ، فتتعلّق بالحدود المنطقيّة للحساب. ويستكشف كتاب دوغلاس هوفشتادتر الجميل غودل وإيشر وباخ بعض هذه الأفكار (Hofstadter 1979). ↩︎

  21. تحذير: هذه الأوّليّة eval ليست هي إجراء eval الذي نفّذناه في 4.1.1، لأنّها تستخدم بيئات Scheme فعليّة بدلًا من بُنى البيئة النموذجيّة التي بنيناها في 4.1.3. وهذه البيئات الفعليّة لا يُمكن التلاعب بها من المُستخدِم كقوائم اعتياديّة؛ بل لا بدّ من النفاذ إليها عبر eval أو عمليّات خاصّة أخرى. وبالمثل، فإنّ الأوّليّة apply التي رأيناها سابقًا ليست هي apply الحلقيّ الوسيط، لأنّها تستخدم إجراءات Scheme فعليّة بدلًا من كائنات الإجراءات التي بنيناها في 4.1.3 و4.1.4. ↩︎

  22. يتضمّن تنفيذ MIT للغة Scheme eval، وكذلك رمزًا user-initial-environment مربوطًا بالبيئة الأوّليّة التي تُقيَّم فيها تعابير دخل المُستخدِم. ↩︎

  23. وعلى الرغم من أنّنا اشترطنا أنّ halts? يُعطى كائن إجراء، لاحِظ أنّ هذا الاستدلال يسري حتى لو كان في وسع halts? أن ينفذ إلى نصّ الإجراء وبيئته. وهذا هو مبرهنة التوقّف الشهيرة لتورينغ، التي أعطت أوّل مثال واضح على مسألة غير قابلة للحساب، أي مهمّة مُصوَّغة صياغةً سليمة لا يُمكن أداؤها كإجراء حسابيّ. ↩︎

  24. إنّ رغبتنا في ألّا تعتمد البرامج على آليّة التقييم هذه هي سبب الملاحظة «الإدارة غير مسؤولة» الواردة في الحاشية 28 من الفصل 1. فبإصرارها على أن تأتي التعريفات الداخليّة أوّلًا ولا تستخدم بعضها بعضًا أثناء تقييم التعريفات، يترك معيار IEEE للغة Scheme بعض الخيار للمُنفّذين في الآليّة المستخدمة لتقييم هذه التعريفات. وقد يبدو اختيار قاعدة تقييم واحدة بدلًا من أخرى هنا مسألةً صغيرة، لا تؤثّر إلّا في تفسير البرامج «المصوغة صياغةً سيّئة». لكنّنا سنرى في 5.5.6 أنّ الانتقال إلى نموذج نطاقٍ آنيّ للتعريفات الداخليّة يتجنّب بعض الصعوبات الشائكة التي كان من شأنها أن تنشأ وإلّا في تنفيذ مصرِّف. ↩︎

  25. يسمح معيار IEEE للغة Scheme باستراتيجيّات تنفيذ مختلفة بتحديد أنّ الأمر متروك للمبرمج في الالتزام بهذا القيد، لا للمُنفّذ في فرضه. وبعض المنفّذات للغة Scheme، بما فيها MIT Scheme، تستخدم التحويل المُبيَّن أعلاه. بالتالي، فإنّ بعض البرامج التي لا تلتزم بهذا القيد ستعمل فعلًا في مثل هذه المنفّذات. ↩︎

  26. يُؤيّد منظّمو MIT للغة Scheme أليسا على الأسس التالية: إيفا محقّة من حيث المبدأ — إذ ينبغي اعتبار التعريفات آنيّة. لكن يبدو أنّ تنفيذ آليّة عامّة فعّالة تفعل ما تتطلّبه إيفا صعب. وفي غياب مثل هذه الآليّة، فمن الأفضل توليد خطأ في الحالات الصعبة للتعريفات الآنيّة (مفهوم أليسا) من إنتاج جواب غير صحيح (كما يريده بن). ↩︎

  27. يُوضيح هذا المثال خدعة برمجيّة لصياغة إجراءات تعاوديّة دون استخدام define. وأعمّ خدعة من هذا القبيل هي undefinedundefined operator (المُعامِل)، التي يُمكن استخدامها لإعطاء تنفيذ «حساب لامدا خالص» للتعاود. (انظر Stoy 1977 للتفاصيل حول حساب لامدا، و Gabriel 1988 لعرض المُعامِل undefinedundefined في Scheme.) ↩︎

  28. هذه التقنيّة جزء لا يتجزّأ من عمليّة التصريف، التي سنناقشها في الفصل 5. وقد كتب جوناثان ريس مُفسّر Scheme على هذا النحو نحو عام 1982 لمشروع T (Rees and Adams 1982). واخترع مارك Feeley (1986) (انظر أيضًا Feeley and Lapalme 1987) هذه التقنيّة باستقلال في رسالة ماجستيره. ↩︎

  29. هناك، مع ذلك، جزء مهمّ من بحث المتغيّر يُمكن أداؤه كجزء من التحليل الصياغيّ. فكما سنُظهر في 5.5.6، يُمكن للمرء تحديد الموضع في بُنية البيئة الذي ستوجد فيه قيمة المتغيّر، مما يُغني ضرورة مسح البيئة للبحث عن المدخلة المطابقة للمتغيّر. ↩︎

  30. انظر التمرين 4.23 لبعض الإفهام عن معالجة التسلسلات. ↩︎

  31. Snarf: «أن تَخْطِف، لا سيّما مستندًا أو ملفًا كبيرًا بغرض استخدامه مع إذن مالكه أو دونه.» Snarf down: «أن تَخْطِف، أحيانًا مع دلالة الامتصاص أو المعالجة أو الفهم.» (هذان التعريفان مَخطوفان من Steele et al. 1983. انظر أيضًا Raymond 1993.) ↩︎

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

  33. مصطلح «الصارم» مقابل مصطلح «غير الصارم» يعني الشيء عينه في الجوهر الذي يعنيه «الترتيب التطبيقيّ» مقابل «الترتيب الاعتياديّ»، إلّا أنّه يشير إلى الإجراءات والوسطاء فرادى لا إلى اللغة ككلّ. وفي مؤتمر عن لغات البرمجة، قد تسمع أحدهم يقول: «لغة الترتيب الاعتياديّ Hassle لها أوّليّات صارمة مُعيَّنة. أمّا الإجراءات الأخرى فتأخذ وسطاءها بالتقييم المتأخّر.» ↩︎

  34. كلمة thunk اختُرِعَت من قبل مجموعة عمل غير رسميّة كانت تناقش تنفيذ الاستدعاء بالاسم في Algol 60. فقد لاحظوا أنّ معظم التحليل («التفكير في») التعبير يُمكن أداؤه في وقت التصريف؛ وبالتالي، في وقت التشغيل، يكون التعبير قد خُطِّف فيه بالفعل (Ingerman et al. 1960). ↩︎

  35. هذا مُشابه لاستخدام force على الكائنات المؤجّلة التي طُرِحَت في الفصل 3 لتمثيل المجاري. أمّا الفرق الجوهريّ بين ما نفعله هنا وما فعلناه في الفصل 3 فهو أنّنا نُبني التأخير والإجبار في المقيّم، وبالتالي نجعل ذلك موحّدًا وآليًّا في اللغة بأكملها. ↩︎

  36. يُشار إلى التقييم المتأخّر المُرافق لحفظ النتائج أحيانًا بـ تمرير الوسطاء بالحاجة، مُقابِل تمرير الوسطاء بالاسم. (والاستدعاء بالاسم، المُدخَل في Algol 60، مُشابه للتقييم المتأخّر غير المُحفَظ.) وبوصفنا مُصمّمي لغات، يُمكننا بناء مقيّمنا ليُحفِظ، أو لا يُحفِظ، أو جعل هذا خيارًا للمبرمجين (التمرين 4.31). وكما قد تتوقّع من الفصل 3، فإنّ هذه الخيارات تُثير مسائل تصبح دقيقة ومُربِكة معًا في وجود الإحالات. (انظر التمرين 4.27 والتمرين 4.29.) وتحاول مقالة رائعة لـ Clinger (1982) تَوضيح الأبعاد المتعدّدة للالتباس الذي ينشأ هنا. ↩︎

  37. لاحِظ أنّنا نُمسي env من الـ thunk أيضًا بعد أن تُحسَب قيمة التعبير. وهذا لا يُحدث فرقًا في القيم التي يُعيدها المفسّر. لكنّه يُساعد فعلًا في توفير المساحة، لأنّ إزالة المرجع من الـ thunk إلى env بعد أن يعود غير ضروريّ يسمح لهذه البُنية بأن تُجمَّع بوصفها قمامة ويُعاد تدوير مساحتها، كما سنناقش في 5.3. وبالمثل، كان في وسعنا أن نسمح للبيئات غير الضروريّة في الكائنات المؤجّلة المُحفَظة الواردة في 3.5.1 بأن تُجمَّع قمامةً، بأن نجعل memo-proc يفعل شيئًا مثل (set! proc '()) لِتَطريف الإجراء proc (الذي يتضمّن البيئة التي قُيّم فيها delay) بعد تخزين قيمته. ↩︎

  38. يُظهر هذا التمرين أنّ التفاعل بين التقييم المتأخّر والآثار الجانبيّة يُمكن أن يكون مُربِكًا للغاية. وهذا هو تمامًا ما قد تتوقّعه من المناقشة الواردة في الفصل 3. ↩︎

  39. هذه هي عين المسألة المتعلّقة بإجراء unless، كما في التمرين 4.26. ↩︎

  40. هذا هو التمثيل الإيّجاديّ الموصوف في التمرين 2.4. وأيّ تمثيل إيّجاديّ في الجوهر (مثلًا، تنفيذٌ بتمرير الرسائل) يكفي كذلك. ولاحِظ أنّنا نستطيع تثبيت هذه التعريفات في المقيّم المتأخّر بمُجرّد طبعها في حلقة المُشغِّل. فلو كنّا قد أدرجنا cons و car و cdr أصلاً كعمليّات أوّليّة في البيئة العالميّة، فسيُعاد تعريفها. (انظر أيضًا التمرين 4.33 والتمرين 4.34.) ↩︎

  41. هذا يسمح لنا بإنشاء إصدارات مؤجّلة من أنواع أعمّ من بُنى القوائم، لا التسلسلات وحدها. ويناقش Hughes 1990 بعض تطبيقات «الأشجار المتأخّرة.» ↩︎

  42. نفترض أنّنا عرّفنا سابقًا إجراءً prime? يختبر whether تكون الأعداد أوّليّة. وحتّى مع تعريف prime?، قد يبدو إجراء prime-sum-pair مشبوه الشبه بمحاولة «الليسب الكاذب» غير المُفيدة لتعريف دالّة الجذر التربيعيّ، التي وصفناها في بداية 1.1.7. وفي الواقع، يُمكن فعلًا صياغة إجراء جذر تربيعيّ على ذلك المنوال كبرنامجٍ غير حتميّ. فبإدخالنا آليّة بحث في المقيّم، فإنّنا نُضعف التمييز بين الأوصاف التصريحيّة المحضة والمواصفات الأمريّة لكيفيّة حساب الأجوبة. وسنمضي أبعد من ذلك في هذا الاتّجاه في 4.4. ↩︎

  43. وُصِفَت فكرة amb للبرمجة غير الحتميّة أوّل مرّة في عام 1961 على يد جون مكارثي (انظر McCarthy 1963). ↩︎

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

  45. قد يُعترض أحدهم بأنّ هذه آليّة يائسة عدم كفاءة. فقد تستلزم ملايين المعالجات لحلّ بعض المسائل سيّئة الصياغة بهذه الطريقة، وأكثر الوقت تكون معظم تلك المعالجات عاطلة. وينبغي أخذ هذا الاعتراض في سياق التاريخ. فقد كانت الذاكرة تُعتبر يومًا سلعةً باهظة من هذا القبيل. ففي عام 1964، كانت تكلفة ميغابايت من الذاكرة العشوائيّة نحو $400,000. والآن كل حاسوب شخصيّ لديه ميغابايتات عديدة من الذاكرة العشوائيّة، وأكثر الوقت معظم تلك الذاكرة غير مستخدمة. ومن الصعب الإفراط في تقدير انخفاض تكلفة الإلكترونيّات المُنتَجة بكمّيّات كبيرة. ↩︎

  46. آليًّا (Automagically): «آليًّا، ولكن بطريقةٍ، لسببٍ ما (عادةً لأنّها بالغة التعقيد، أو بالغة القبح، أو ربّما حتّى بالغة التبسيط)، لا يشعر المتحدّث بالرغبة في شرحها.» (Steele et al. 1983، Raymond 1993) ↩︎

  47. لقد كان دمج استراتيجيّات البحث الآليّ في لغات البرمجة تاريخًا طويلًا متقلّب الأحداث. وجاءت الاقتراحات الأولى بأنّ الخوارزميّات غير الحتميّة قد تُرمَّم بأناقة في لغة برمجة تتضمّن بحثًا وتتبّعًا رجعيًّا آليًّا من روبرت Floyd (1967). واخترع كارل Hewitt (1969) لغة برمجة تُسمّى Planner دَعَمَت صراحةً التتبّع الرجعيّ الزمنيّ الآليّ، مُزوِّدةً استراتيجيّة بحث بالأولويّة إلى العمق مدمجة. ونفّذ Sussman et al. (1971) مجموعةً جزئيّة من هذه اللغة، تُسمّى MicroPlanner، استُخدِمَت لدعم الأعمال في حلّ المشكلات وتخطيط الروبوتات. وأدّت أفكار مشابهة، ناشئة عن المنطق وإثبات النظريّات، إلى نشأة لغة Prolog الأنيقة في إدنبرة ومرسيليا (والتي سنناقشها في 4.4). وبعد إحباط كافٍ من البحث الآليّ، طوّر McDermott and Sussman (1972) لغةً تُسمّى Conniver، تضمّنت آليّات لوضع استراتيجيّة البحث تحت تحكّم المبرمج. غير أنّ هذا أثبت عدم عمليّته، ومع ذلك، وجد Sussman and Stallman 1975 أسلوبًا أكثر قابلية للتطبيق أثناء فحص طرائق التحليل الرمزيّ للدوائر الكهربائيّة. وقد طوّرا مخطّط تتبّع رجعيّ غير زمنيّ يقوم على تعقّب التبعيّات المنطقيّة التي تربط الحقائق، وهي تقنيّة عُرفت فيما بعد بـ dependency-directed backtracking (التتبّع الرجعيّ الموجَّه بالتبعيّات). وعلى الرغم من تعقيد طريقتهما، فقد أنتجت برامج فعّالة إلى حدّ معقول لأنّها أجرت بحثًا زائدًا قليلًا. وعمّم Doyle (1979) وMcAllester (1978; 1980) طرائق ستالمان وسوسمان ووضّحاها، مطوّرين نموذجًا جديدًا لصياغة البحث يُسمّى الآن truth maintenance (صيانة الحقيقة). وجميع منظومات حلّ المشكلات الحديثة تستخدم شكلًا ما من منظومات صيانة الحقيقة كطبقة أساسيّة. انظر Forbus and deKleer 1993 لمناقشة طرق أنيقة لبناء منظومات صيانة الحقيقة والتطبيقات التي تستخدم صيانة الحقيقة. ويصف Zabih et al. 1987 امتدادًا غير حتميّ لـ Scheme قائمًا على amb؛ وهو مشابه للمفسّر الموصوف في هذا القسم، لكنه أكثر تطورًا، لأنّه يستخدم التتبّع الرجعيّ الموجَّه بالتبعيّات بدلًا من التتبّع الرجعيّ الزمنيّ. ويُعطي Winston 1992 مقدّمة إلى نوعَي التتبّع الرجعيّ. ↩︎

  48. يستخدم برنامجنا الإجراء التاليّ لتحديد ما إذا كانت عناصر قائمة متمايزة: scheme (define (distinct? items) (cond ((null? items) true) ((null? (cdr items)) true) ((member (car items) (cdr items)) false) (else (distinct? (cdr items))))) ↩︎

  49. أُخِذَت هذه المسألة من كتيّبٍ يُسمّى «مسائل ترفيهيّة مثيرة للمشكلات»، نُشر في الستينيّات من القرن الماضي على يد شركة Litton Industries، وتُنسب فيه إلى مهندس ولاية كانساس. ↩︎

  50. نعتمد هنا العُرف الذي يكون فيه أوّل عنصر من كل قائمة هو ما يُعيّن القسم اللغويّ (جزء الكلام) لبقية الكلمات في القائمة. ↩︎

  51. لاحِظ أنّ parse-word يستخدم set! لتعديل قائمة الدخل غير المُحلَّلة. ولكي يعمل هذا، يجب على مقيّم amb أن يُلغي آثار عمليّات set! عندما يقوم بالتتبّع الرجعيّ. ↩︎

  52. لاحِظ أنّ هذا التعريف تعاوديّ — فقد يتبع الفعل أيّ عدد من العبارات الجارّة. ↩︎

  53. يُمكن أن يصبح هذا النوع من القواعد بالغ التعقيد اعتباطيًّا، لكنّه ليس إلّا لُعبةً من منظور فهم اللغة الطبيعيّ الحقيقيّ. فالفهم الحقيقيّ للغة الطبيعيّة بالحاسوب يستلزم مزيجًا مُحكمًا من التحليل النحويّ وتفسير المعنى. من ناحية أخرى، حتّى المُحلِّلات اللعبيّة يُمكن أن تكون مفيدة في دعم لغات الأوامر المرنة للبرامج مثل منظومات استرجاع المعلومات. ويناقش Winston 1992 المقاربات الحسابيّة لفهم اللغة الحقيقيّ وكذلك تطبيقات القواعد البسيطة في لغات الأوامر. ↩︎

  54. على الرغم من أنّ فكرة أليسا تعمل عملًا حسنًا (وهي بسيطة على نحوٍ يُثير الدهشة)، فإنّ الجُمل التي تُولّدها مملّة بعض الشيء — فهي لا تأخذ عيّنات من الجُمل الممكنة لهذه اللغة بطريقة مثيرة للاهتمام. وفي الواقع، فإنّ القاعدة تعاوديّة إلى حدٍّ كبير في مواضع كثيرة، وتقنيّة أليسا «تقع في» إحدى هذه التعاوديّات وتعلق. انظر التمرين 4.50 لطريقة في التصرّف مع هذا. ↩︎

  55. اخترنا تنفيذ المقيّم المتأخّر في 4.2 بوصفه تعديلًا على المقيّم الحلقيّ الوسيط الاعتياديّ الوارد في 4.1.1. وعلى النقيض، سنُأسيس مقيّم amb على المقيّم المُحلِّل الوارد في 4.1.7، لأنّ إجراءات التنفيذ في ذلك المقيّم تُوفّر إطارًا مريحًا لتنفيذ التتبّع الرجعيّ. ↩︎

  56. نفترض أنّ المقيّم يدعم let (انظر التمرين 4.22)، التي استخدمناها في برامجنا غير الحتميّة. ↩︎

  57. لم نقلق بشأن إلغاء التعريفات، إذ يُمكننا الافتراض بأنّ التعريفات الداخليّة مُستخرَجة بالمسح (4.1.6). ↩︎

  58. لقد نما البرمجة المنطقيّة انطلاقًا من تاريخ طويل من البحث في الإثبات الآليّ للنظريّات. فبرامج إثبات النظريّات المبكّرة لم تكن تقدر على تحقيق شيء يُذكر تقريبًا، لأنّها كانت تبحث بحثًا شاملًا في فضاء البراهين الممكنة. وكان الاختراق الكبير الذي جعل مثل هذا البحث احتمالًا هو اكتشاف خوارزميّة التوحيد ومبدأ الاستنتاج في أوائل الستينيّات (Robinson 1965). وقد استُخدِم الاستنتاج، على سبيل المثال، على يد Green and Raphael (1968) (انظر أيضًا Green 1969) كأساس لنظام استنتاجيّ للإجابة عن الأسئلة. وخلال معظم هذه الفترة، ركّز الباحثون على خوارزميّات يَضمَنون إيجاد برهان إن وُجد. وكان من الصعب التحكّم في مثل هذه الخوارزميّات وتوجيهها نحو برهان. وأدرك Hewitt (1969) إمكانيّة دمج بُنية التحكّم في لغة برمجة مع عمليّات نظامٍ للتلاعب المنطقيّ، وهو ما أدّى إلى العمل في البحث الآليّ المذكور في 4.3.1 (الحاشية 250). وفي الوقت نفسه الذي كان يُنفَّذ فيه ذلك، كان كولميرو، في مرسيليا، يُطوّر منظومات قائمة على القواعد للتلاعب باللغة الطبيعيّة (انظر Colmerauer et al. 1973). وقد اخترع لغة برمجة تُسمّى Prolog لتمثيل تلك القواعد. وأدرك Kowalski (1973; 1979)، في إدنبرة، أنّ تنفيذ برنامج Prolog يُمكن تفسيره بأنّه إثبات للنظريّات (باستخدام تقنيّة إثبات تُسمّى الاستنتاج بجُمل هورن الخطيّة). وأدّى دمج الخيطين الأخيرين إلى حركة البرمجة المنطقيّة. وبالتالي، في إسناد الفضل في تطوير البرمجة المنطقيّة، يُمكن للفرنسيّين الإشارة إلى نشأة Prolog في جامعة مرسيليا، في حين يُمكن للبريطانيّين تسليط الضوء على العمل في جامعة إدنبرة. ووفقًا لأهل MIT، فقد طُوِّرَت البرمجة المنطقيّة على يد هذه المجموعات في محاولة لاستيعاب ما كان هيويت يتحدّث عنه في أطروحة دكتوراه بليغة لكنها غامضة. وللتاريخ البرمجة المنطقيّة، انظر Robinson 1983. ↩︎

  59. لرؤية التقابل بين القواعد والإجراء، دع x في الإجراء (حيث يكون x غير فارغ) يُقابِل (cons u v) في القاعدة. ثمّ يكون z في القاعدة هو append كُلًّا من (cdr x) و y. ↩︎

  60. هذا لا يُخفّف بالتأكيد عن المُستخدِم مشكلة كيفيّة حساب الجواب بأكملها. فهناك العديد من مجموعات القواعد المتكافئة رياضيًّا المختلفة لصياغة علاقة append، لا يُمكن تحويل بعضها فقط إلى أجهزة فعّالة للحساب في أيّ اتّجاه. وبالإضافة إلى ذلك، فإنّ معلومات «ما هو» لا تُعطي أحيانًا أيّ دلالة «كيف» يُحسَب جواب. فعلى سبيل المثال، تأمّل مشكلة حساب undefinedundefined بحيث undefinedundefined . ↩︎

  61. بلغ الاهتمام بالبرمجة المنطقيّة ذروته في أوائل الثمانينيّات عندما بدأت الحكومة اليابانيّة مشروعًا طموحًا يهدف إلى بناء حواسيب فائقة السرعة مُحسَّنة لتشغيل لغات البرمجة المنطقيّة. وكانت سرعة هذه الحواسيب ستُقاس بـ LIPS (الاستنتاجات المنطقيّة في الثانية) بدلًا من FLOPS (عمليّات الفاصلة العائمة في الثانية) المعتادة. وعلى الرغم من نجاح المشروع في تطوير عتادٍ وبرمجيّات كما كان مُخطَّطًا له أصلاً، فإنّ صناعة الحاسوب الدوليّة سلكت اتجاهًا مختلفًا. انظر Feigenbaum and Shrobe 1993 من أجل تقييم عامّ للمشروع اليابانيّ. وقد انتقل مُجتمَع البرمجة المنطقيّة أيضًا إلى النظر في البرمجة العلائقيّة القائمة على تقنيّات غير مطابقة الأنماط البسيطة، مثل القدرة على التصرّف مع القيود العدديّة مثل تلك المُوضيَّحة في منظومة انتشار القيود الواردة في 3.3.5. ↩︎

  62. هذا يستخدم صياغة الذيل المنقّط التي طُرِحَت في التمرين 2.20. ↩︎

  63. في الواقع، هذا الوصف لـ not صالح للحالات البسيطة وحدها. أمّا السلوك الحقيقيّ لـ not فأكثر تعقيدًا. وسنفحص خصائص not المميّزة في 4.4.2 و4.4.3. ↩︎

  64. يجب استخدام lisp-value فقط لتنفيذ عمليّة غير مُوفَّرة في لغة الاستعلام. وعلى وجه الخصوص، لا يجب استخدامه لاختبار المساواة (لأنّ ذلك هو ما صُمِّمَت المطابقة في لغة الاستعلام لفعله) أو عدم المساواة (لأنّ ذلك يُمكن فعله بقاعدة same المُبيَّنة أدناه). ↩︎

  65. لاحِظ أنّنا لا نحتاج إلى same لكي نجعل شيئَيْن متساويين: فنحن نستخدم مُتغيّر النمط نفسه لكلٍّ منهما — في الواقع، لدينا شيء واحد بدلًا من شيئين في المكان الأوّل. فعلى سبيل المثال، انظر ?town في قاعدة lives-near و ?middle-manager في قاعدة wheel أدناه. أمّا same فهي مفيدة عندما نريد إرغام شيئين على الاختلاف، مثل ?person-1 و ?person-2 في قاعدة lives-near. فمع أنّ استخدام مُتغيّر النمط نفسه في جزأين من استعلام يُرغم القيمة عينها على الظهور في كلا الموضعين، فإنّ استخدام مُتغيّرات نمط مختلفة لا يُرغم قيمًا مختلفة على الظهور. (وقد تكون القيم المُسنَدة إلى مُتغيّرات نمط مختلفة هي نفسها أو مختلفة.) ↩︎

  66. سنسمح أيضًا بقواعد بلا أجساد، كما في same، وسنفسّر مثل هذه القاعدة بأنّها تعني أنّ استنتاج القاعدة تُرضيه أيّ قيم للمتغيّرات. ↩︎

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

  68. ولكنّ هذا النوع من الانفجار الأُسّيّ غير شائع في استعلامات and لأنّ الشروط المُضافة تميل إلى تقليل عدد الإطارات المُنتَجة لا إلى توسيعه. ↩︎

  69. هناك أدبيّات واسعة على منظومات إدارة قواعد البيانات تتعلّق بكيفيّة التصرّف الفعّال مع الاستعلامات المعقّدة. ↩︎

  70. هناك فرق دقيق بين هذا التنفيذ المُرشِّح لـ not وبين المعنى المعتاد لـ not في المنطق الرياضيّ. انظر 4.4.3. ↩︎

  71. في مطابقة الأنماط أحاديّة الجانب، تكون جميع المعادلات التي تحتوي مُتغيّرات نمط صريحة ومحلولّة سابقًا بالنسبة للمجهول (مُتغيّر النمط). ↩︎

  72. هناك طريقة أخرى للتفكير في التوحيد، وهي أنّه يُولّد النمط الأكثر عموميّة الذي يُخصِّص نمطَي الدخل. أي أنّه توحيد (?x a) و ((b ?y) ?z) هو ((b ?y) a)، وتوحيد (?x a ?y) و (?y ?z a)، المناقَش أعلاه، هو (a a a). وفي تنفيذنا، من الأنسب التفكير في نتيجة التوحيد بوصفها إطارًا لا نمطًا. ↩︎

  73. بما أنّ التوحيد تعميم للمطابقة، فقد نُبسّط المنظومة باستخدام المُوحِّد لإنتاج المجريَيْن. غير أنّ معالجة الحالة السهلة بالمُطابِق البسيط تُوضيح كيف يُمكن أن تكون المطابقة (بخلاف التوحيد الكامل) نافعة بذاتها. ↩︎

  74. السبب في استخدامنا مجاري (لا قوائم) من الإطارات هو أنّ التطبيق التعاوديّ للقواعد يُمكن أن يُولّد أعدادًا لا نهائيّة من القيم التي تُرضي استعلامًا. والتقييم المؤجَّل المتجسّد في المجاري حاسم هنا: إذ سيطبع النظام الاستجابات واحدةً تلو الأخرى أثناء توليدها، سواء كان عدد الاستجابات منتهيًا أو غير منتهٍ. ↩︎

  75. أنّ طريقة استنتاج مُعيَّنة مشروعة ليس تأكيدًا تافهًا. فيجب على المرء أن يُبرهن على أنّه إذا انطلق المرء من مُقدّمات صحيحة، فلا يُمكن استنتاج إلّا نتائج صحيحة. وطريقة الاستنتاج التي تمثّلها تطبيقات القواعد هي modus ponens، طريقة الاستنتاج المألوفة التي تقول بأنّه إذا كان undefinedundefined صحيحًا وكانت A يستتبع B صحيحة، فيجوز لنا استنتاج أنّ undefinedundefined صحيح. ↩︎

  76. يجب أن نُقيّد هذا القبول باتّفاقنا على أنّنا، عند الحديث عن «الاستنتاج» التي يُنجزه برنامج منطقيّ، نفترض أنّ الحساب ينتهي. وللأسف، هذا القبول المُقيَّد باطل حتّى في تنفيذنا للغة الاستعلام (وباطل أيضًا في برامج Prolog ومعظم لغات البرمجة المنطقيّة الراهنة) بسبب استخدامنا not و lisp-value. فكما سنصف أدناه، فإنّ not المُنفَّذة في لغة الاستعلام غير متّسقة دائمًا مع not المنطق الرياضيّ، وlisp-value يُدخل تعقيدات إضافيّة. وقد كنّا لنُنفّذ لغةً متّسقة مع المنطق الرياضيّ بإزالة not و lisp-value ببساطة من اللغة والاتّفاق على كتابة البرامج باستخدام الاستعلامات البسيطة و and و or وحدها. لكنّ هذا من شأنه تقييد القوّة التعبيريّة للغة تقييدًا كبيرًا. وإنّ أحد الاهتمامات الكبرى للبحث في البرمجة المنطقيّة هو إيجاد سُبل لتحقيق اتّساقٍ أكبر مع المنطق الرياضيّ دون التضحية بالقوّة التعبيريّة تضحيةً غير مبرّرة. ↩︎

  77. هذه ليست مشكلة في المنطق بل في التفسير الإجرائيّ للمنطق الذي يُوفّره مُفسّرنا. فقد كنّا لنكتب مُفسّرًا لا يقع في حَلْقة هنا. فعلى سبيل المثال، كنّا لنُعدّد جميع البراهين القابلة للاستنتاج من تأكيداتنا وقواعدنا بترتيب الأوسع أوّلًا بدلًا من الأعمق أوّلًا. لكنّ مثل هذه المنظومة يجعل الأمر أكثر صعوبة في الانتفاع من ترتيب الاستنتاجات في برامجنا. وهناك محاولة لبناء تحكّم متطوّر في مثل هذا البرنامج موصوفة في deKleer et al. 1977. وهناك تقنيّة أخرى، لا تؤدّي إلى مشكلات تحكّم بهذه الخطورة، وهي إدخال معرفة خاصّة، مثل كواشف أنواع مُعيَّنة من الحلقات (التمرين 4.67). لكن لا يُمكن أن يكون هناك مخطّط عامّ لمنع منظومة بموثوقيّة من النزول في مسارات لا نهائيّة أثناء أداء الاستنتاجات. فتخيّل قاعدة شيطانيّة من صيغة «لإظهار أنّ undefinedundefined صحيح، أظهر أنّ undefinedundefined صحيح»، لدالّة ما مُختارة بشكلٍ مناسب undefinedundefined . ↩︎

  78. تأمّل الاستعلام (not (baseball-fan (Bitdiddle Ben))). فتجد المنظومة أنّ (baseball-fan (Bitdiddle Ben)) غير موجود في قاعدة البيانات، فلا يُرضي الإطار الفارغ النمط ولا يُرشَّح خارج مجرى الإطارات الأوّليّ. ونتيجة الاستعلام هي بالتالي الإطار الفارغ، الذي يُستخدم لإسكان الاستعلام المُدخَل لإنتاج (not (baseball-fan (Bitdiddle Ben))). ↩︎

  79. يُمكن العثور على مناقشة وتبرير لهذه المعاملة لـ not في المقالة التي كتبها Clark (1978). ↩︎

  80. بشكل عام، فإنّ توحيد ?y مع تعبيرٍ يَشتمل ?y يستلزم منّا القدرة على إيجاد نقطة ثابتة للمعادلة ?y = ⟨تعبير يَشتمل ?y``⟩. ويُمكن أحيانًا تكوين تعبير يبدو حلًّا للمسألة صياغيًّا. فعلى سبيل المثال، يبدو أنّ ?y = (f ?y) له النقطة الثابتة (f (f (f … )))، التي نستطيع إنتاجها بالبدء بالتعبير (f ?y) واستبدال (f ?y) بـ ?y تكرارًا. وللأسف، ليست كل معادلة من هذا القبيل لها نقطة ثابتة ذات مغزى. والمسائل التي تظهر هنا تشبه مسائل التلاعب بالمتسلسلات اللانهائيّة في الرياضيّات. فعلى سبيل المثال، نعلم أنّ 2 هو الحلّ للمعادلة undefinedundefined . وبالبدء بالتعبير undefinedundefined واستبدال undefinedundefined بـ undefinedundefined تكرارًا، نحصل على undefinedundefined وهو ما يؤدّي إلى undefinedundefined ولكن، إذا جرّبنا التلاعب نفسه بدءًا بملاحظة أنّ undefinedundefined هو الحلّ للمعادلة undefinedundefined ، فنحن نحصل على undefinedundefined وهو ما يؤدّي إلى undefinedundefined وعلى الرغم من أنّ التلاعبات الصياغيّة المُستخدمة في استنتاج هاتين المعادلتين متطابقة، فإنّ النتيجة الأولى تأكيد صحيح عن المتسلسلات اللانهائيّة أمّا الثانية فليست كذلك. وبالمثل، بالنسبة لنتائج توحيدنا، فإنّ الاستنتاج بتعبير مبنيّ صياغيًّا اعتباطيًّا قد يؤدّي إلى أخطاء. ↩︎

  81. تُعطي معظم منظومات لisp للمُستخدِم القدرة على تعديل إجراء read الاعتياديّ لأداء مثل هذه التحويلات بتعريف أحرف ماكرو القارئ. فالتعبيرات بين علامتَي اقتباس مُعالَجة سابقًا بهذه الطريقة: إذ يُترجم القارئ 'expression آليًّا إلى (quote expression) قبل أن يراه المقيّم. ويُمكننا ترتيب تحويل ?expression إلى (? expression) بالطريقة نفسها؛ غير أنّنا، من أجل الوضوح، أدرجنا إجراء التحويل هنا صراحةً. ويستخدم expand-question-mark و contract-question-mark عدّة إجراءات تحتوي string في أسمائها. وهذه هي أوّليّات Scheme. ↩︎