مقدمة في علوم الحاسوب النظرية › نماذج مكافئة للاحتساب 10 / 26 السابق التالي

نماذج حوسبة مكافئة { #chapequivalentmodels }

نماذج حوسبة مكافئة

  • تعرَّف على آلات RAM وحساب لامدا.
  • التكافؤ بين هذين النموذجين ونماذج أخرى وبين آلات تورينج.
  • الآليات الخلوية وتكوينات آلات تورينج.
  • افهم أطروحة Church-Turing.

"كل المشكلات في علم الحاسوب يمكن حلها بمستوى آخر من التوجيه غير المباشر", منسوب إلى ديفيد وي Wheeler.

"لأننا سنحسب لاحقًا بتعبيرات عن الدوال، نحتاج إلى تمييز بين الدوال والصيغ، وإلى رمز للتعبير عن هذا التمييز. هذا التمييز، والرموز التي تصفه، والتي نحيد عنها بشكل تافه، هو ما قدّمه Church.", جون ماكانتي، 1960 (في ورقة تصف لغة البرمجة LISP)

حتى الآن، عرّفنا مفهوم حساب دالة باستخدام آلات تورينج، وهي لا تطابق الطرق التي يجري بها الحساب في الممارسة العملية عن قرب. في هذا الفصل نبرر هذا الاختيار بإظهار أن تعريف الدوال القابلة للحساب سيبقى كما هو تحت مجموعة واسعة من نماذج الحوسبة. يُعرف هذا المفهوم بـ_اكتمال تورينج_ (Turing completeness) أو تكافؤ تورينج (Turing equivalence)، وهو إحدى أكثر حقائق علم الحاسوب جوهرية. في الواقع، هناك دعوى منتشرة الانتشار تُعرف بـ_أطروحة Church-Turing_ تقول إن كل تعريف "معقول" للدالة القابلة للحساب هو مكافئ لقابلية حسابها بآلة تورينج. نناقش أطروحة Church-Turing والتعريفات المحتملة لكلمة "معقول" في أطروحة Church-Turing (مناقشة).

تشمل بعض نماذج الحوسبة الرئيسية التي سنناقشها في هذا الفصل:

  • آلات RAM: لا تقابل آلات تورينج البنى المعمارية المعيارية للحاسوب التي تحتوي على ذاكرة عشوائية الوصول (Random Access Memory, RAM). فالنموذج الرياضي لآلات RAM أقرب بكثير إلى الحاسوب الفعلي، لكننا سنرى أنه مكافئ في القوة لآلات تورينج. كما سنناقش أيضًا متغيّرًا من لغات البرمجة لآلات RAM، نسميه NAND-RAM. والتكافؤ بين آلات تورينج وآلات RAM يتيح لنا إثبات تكافؤ تورينج للعديد من لغات البرمجة الشائعة، بما فيها جميع اللغات العامة المستخدمة عمليًا مثل C وPython وJavaScript وغيرها.

  • الآليات الخلوية (cellular automata): يمكن نمذجة كثير من الأنظمة الطبيعية والاصطناعية كمجموعات من مكوّنات بسيطة، كل منها يتطور وفق قواعد بسيطة تستند إلى حالته وحالة جيرانه (neighbors) المباشرين. ومن الأمثلة الشهيرة على ذلك لعبة حياة كونواي (Conway's Game of Life). ولإثبات أن الآليات الخلوية مكافئة لآلات تورينج، نقدّم أداة التكوينات (configurations) الخاصة بآلات تورينج. ولكل هذه الأدوات تطبيقات أخرى، وعلى الأخص تُستخدم في هل كل مبرهنة قابلة للإثبات؟ لإثبات نظرية عدم اكتمال غودل: نتيجة مركزية في الرياضيات.

  • حساب لامدا (lambda calculus): حساب لامدا هو نموذج للتعبير عن الحساب يعود أصله إلى ثلاثينيات القرن الماضي، وإن كان مرتبطًا عن قرب بلغات البرمجة الوظيفية المستخدمة على نطاق واسع اليوم. وإظهار تكافؤ حساب لامدا مع آلات تورينج يتطلب تقنية جميلة للتخلص من التهاكر تُعرف بـ"المُجمِّع Y" (Y Combinator).

في هذا الفصل ندرس التكافؤ بين النماذج. نقول إن نموذجَي حوسبة متكافئان (ويُعرفان أيضًا بـ_متكافئَي تورينج_) إذا كان بإمكانهما حساب المجموعة نفسها من الدوال. على سبيل المثال، رأينا أن آلات تورينج وبرامج NAND-TM متكافئة، لأننا نستطيع تحويل كل آلة تورينج إلى برنامج NAND-TM يحسب الدالة نفسها، وكذلك نستطيع تحويل كل برنامج NAND-TM إلى آلة تورينج تحسب الدالة نفسها.

في هذا الفصل نبيّن أن هذا يمتد إلى ما هو أبعد بكثير من آلات تورينج. والتقنيات التي سنطوّرها تتيح لنا أن نبيّن أن جميع لغات البرمجة العامة (أي Python وC وJava وغيرها) كاملة بحسب تورينج (Turing Complete)، بالمعنى أنها تستطيع محاكاة آلات تورينج، وبالتالي حساب جميع الدوال التي يمكن حسابها بآلة TM. وسنبيّن أيضًا الاتجاه الآخر - إذ يمكن استخدام آلات تورينج لمحاكاة برنامج بأي من هذه اللغات، وبالتالي حساب أي دالة يمكن حسابها بها. وهذا يعني أن جميع لغات البرمجة هذه متكافئة مع تورينج: فهي متكافئة في القوة مع آلات تورينج ومع بعضها البعض. وهذا مبدأ قوي يستند إليه المدى الواسع الذي بلغه علم الحاسوب. فضلا عن ذلك، فهو يتيح لنا أن «نأكل الكعكة ونحتفظ بها أيضًا» - فبما أن جميع هذه النماذج متكافئة، يمكننا اختيار النموذج الذي يناسبنا للمهمة قيد اليد. ولتحقيق هذا التكافؤ، نعرّف نموذج حوسبة جديدًا يُعرف بـ_آلات RAM_. تلتقط آلات RAM بنية الحاسوب الحديث عن قرب أكبر مما تفعل آلات تورينج، لكنها تظل مكافئة حسابيًا لآلات تورينج.

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

انظر الشكل التوضيحي لمحة عامة عن نتائج هذا الفصل.

/images/introtcs/lec_07_other_models-1.webp

آلات RAM وNAND-RAM

أحد قيود آلات تورينج (وبرامج NAND-TM) أننا لا نستطيع الوصول إلا إلى موقع واحد من مصفوفاتنا/شريطنا في كل مرة. إذا كانت الرأس عند الموضع 2222 في الشريط وأردنا الوصول إلى الموضع 957957، فإن ذلك سيستغرق منّا 923 خطوة على الأقل. في المقابل، تمتلك كل لغة برمجة تقريبًا صيغة رسمية (formalism) للوصول المباشر إلى مواقع الذاكرة. كما توفّر الحواسيب الفيزيائية الفعلية ما يُعرف بـ_الذاكرة العشوائية الوصول (RAM)_، ويمكن التفكير فيها على أنها مصفوفة كبيرة اسمها Memory، بحيث إن أعطينا فهرسًا pp (أي عنوان ذاكرة، أو مؤشرًا pointer) استطعنا القراءة من الموضع pthp^{th} في Memory والكتابة إليه. ("الذاكرة العشوائية الوصول" تسمية مضللة إلى حد بعيد إذ لا علاقة لها بالاحتمال، لكن بما أنها مصطلح معتمد في نظريّة الحوسبة وممارستها معًا، سنستخدمها نحن أيضًا.)

نموذج الحوسبة الذي يجسّد الوصول إلى ذاكرة من هذا النوع هو آلة RAM (ويُعرف أحيانًا أيضًا بـ_نموذج Word RAM_)، كما هو موضح في الشكل التوضيحي. ذاكرة آلة RAM هي مصفوفة ذات حجم غير محدود، يمكن لكل خلية فيها أن تخزّن كلمةً واحدة، نعتبرها سلسلة في {0,1}w\{0,1\}^w، وكذلك (بشكل مكافئ) عددًا في [2w][2^w]. على سبيل المثال، تستخدم كثير من البنى الحاسوبية الحديثة كلمات بطول 6464 بت، حيث يحمل كل موقع ذاكرة سلسلة في {0,1}64\{0,1\}^{64}، ويمكن أيضًا التفكير فيها على أنها عدد بين 00 و264−1=18,446,744,073,709,551,6152^{64}-1= 18,446,744,073,709,551,615. تُعرف المعامل ww بـ_حجم الكلمة_ (word size). في الممارسة غالبًا ما يكون ww عددًا ثابتًا مثل 6464، لكن عند تقديم النظرية نعتبر ww معاملًا قد يعتمد على طول المدخل أو على عدد الخطوات. (يمكنك التفكير في 2w2^w على أنها تقابل تقريبًا أكبر عنوان ذاكرة نستخدمه أثناء الحساب.) بالإضافة إلى مصفوفة الذاكرة، تحتوي آلة RAM أيضًا على عدد ثابت من السجلات (registers) r0,…,rk−1r_0,\ldots,r_{k-1}، يمكن لكل منها أن يحتوي كلمة واحدة أيضًا.

/images/introtcs/lec_07_other_models-2.webp

تشمل العمليات التي تستطيع آلة RAM تنفيذها:

  • نقل البيانات: تحميل بيانات من خلية معينة في الذاكرة إلى سجل، أو تخزين محتوى سجل في خلية معينة من الذاكرة. تستطيع آلة RAM الوصول إلى أي خلية في الذاكرة مباشرةً دون الحاجة إلى تحريك "الرأس" (كما تفعل آلات تورينج) إلى ذلك الموضع. أي أنه في خطوة واحدة تستطيع آلة RAM أن تحمّل في السجل rir_i محتوى خلية الذاكرة التي يفهرسها السجل rjr_j، أو أن تخزّن في خلية الذاكرة التي يفهرسها السجل rjr_j محتوى السجل rir_i.

  • الحساب: تستطيع آلات RAM إجراء عمليات على السجلات مثل العمليات الحسابية، والعمليات المنطقية، والمقارنات.

  • مسار التنفيذ: كما في حالة آلات تورينج، يمكن أن يعتمد اختيار أمر التعليمات التالي على حالة آلة RAM، وهي محدَّدة بمحتوى سجلها.

/images/introtcs/lec_07_other_models-3.webp

لن نقدّم تعريفًا رسميًا لآلات RAM، وإن كان قسم ملاحظات المراجع (ملاحظات مرجعية) يحتوي على مصادر لمثل هذه التعريفات. تمامًا كما تُجسّد لغة البرمجة NAND-TM آلات تورينج، يمكننا أيضًا تعريف لغة برمجة NAND-RAM تُجسّد آلات RAM. توسّع لغة برمجة NAND-RAM لغة NAND-TM بإضافة الميزات التالية:

  • يُسمح لمتغيرات NAND-RAM بأن تكون ذات قيم صحيحة (غير سالبة) بدلًا من أن تكون بوليانية فقط كما في NAND-TM. أي أن المتغير العددي foo يحمل عددًا صحيحًا غير سالب في N\N (بدلًا من بت واحد في {0,1}\{0,1\})، والمتغير المصفوفي Bar يحمل مصفوفة من الأعداد الصحيحة. وكما في حالة آلات RAM، لن نسمح بأعداد صحيحة ذات حجم غير محدود. بشكل ملموس، يحمل كل متغير عددًا بين 00 وT−1T-1، حيث TT هو عدد الخطوات التي نفّذها البرنامج حتى الآن. (يمكنك تجاهل هذا القيد الآن: إن أردنا حمل أعداد أكبر، يمكننا ببساطة تنفيذ تعليمات وهمية؛ وسيكون ذلك مفيدًا في الفصول اللاحقة.)

  • نسمح بـ_الوصول المفهرس_ (indexed access) إلى المصفوفات. إذا كان foo متغيرًا عدديًا وBar مصفوفة، فإن Bar[foo] تشير إلى الموضع في Bar الذي تفهرسه قيمة foo. (لاحظ أن هذا يعني أننا لم نعد بحاجة إلى متغير فهرس خاص i.)

  • كما هو شائع في لغات البرمجة، سنفترض أنه في العمليات البوليانية مثل NAND، يُعتبر العدد الصحيح ذو القيمة صفرًا خطأً (false)، ويُعتبر العدد الصحيح غير الصفري صوابًا (true).

  • بالإضافة إلى NAND، تشمل NAND-RAM أيضًا جميع العمليات الحسابية الأساسية: الجمع، والطرح، والضرب، والقسمة (على الأعداد الصحيحة)، وكذلك المقارنات (التساوي، والأكبر، والأصغر، إلخ).

  • تتضمن NAND-RAM عبارات شرطية if/then كجزء من اللغة.

  • تحتوي NAND-RAM على بنيات تكرارية مثل while وdo كجزء من اللغة.

الوصف الكامل للغة برمجة NAND-RAM موجود في الملحق. لكن أهم ما تحتاج معرفته عن NAND-RAM هو أنك في الحقيقة لا تحتاج إلى معرفة الكثير عنها إطلاقًا، لأنها مكافئة في القوة لآلات تورينج:

مبرهنة — آلات تورينج (أي برامج NAND-TM) وآلات RAM (أي برامج NAND-RAM) متكافئة

لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون FF قابلة للحساب ببرنامج NAND-TM إذا وفقط إذا كانت FF قابلة للحساب ببرنامج NAND-RAM.

لأن برامج NAND-TM مكافئة لآلات تورينج، وبرامج NAND-RAM مكافئة لآلات RAM، فإن مبرهنة — آلات تورينج (أي برامج NAND-TM) وآلات RAM (أي برامج NAND-RAM) متكافئة تُظهر أن هذه النماذج الأربعة كلها متكافئة فيما بينها.

نظرة عامة على خطوات برهان RAMTMequivalencethm الذي يُحاكي NANDRAM بـNANDTM. نبدأ باستعمال الحلقة الداخلية (inner loop) كسكرٍ نحويٍّ في nandtminnerloopssec لإتاحة تحميل عددٍ صحيحٍ من مصفوفة إلى متغيّر الفهرسة  في NANDTM. ومتى استطعنا ذلك، حاكينا العنونة بالفهرس في NANDTM. ثمّ نستعمل تضمينًا لـ في  لمحاكاة مصفوفات بتٍّ ثنائيّة الأبعاد في NANDTM. وأخيرًا نستعمل التمثيل الثنائي لتشفير مصفوفاتٍ أحادية الأبعاد من الأعداد الصحيحة كمصفوفاتٍ ثنائيّة الأبعاد من البتات، مُتمّنين بذلك محاكاة NANDRAM بـNANDTM.

من الواضح أن NAND-RAM أقوى من NAND-TM فحسب، لذا إذا كانت دالة FF قابلة للحساب ببرنامج NAND-TM، فيمكن حسابها ببرنامج NAND-RAM. الاتجاه الصعب هو تحويل برنامج NAND-RAM PP إلى برنامج NAND-TM QQ مكافئ له. ولوصف البرهان بالكامل، سنحتاج إلى تغطية المواصفة الرسمية الكاملة للغة NAND-RAM، وإظهار كيف يمكننا تنفيذ كل واحدة من ميزاتها كسكّر نحوي فوق NAND-TM.

هذا ممكن، لكن المرور على جميع العمليات بالتفصيل ممل إلى حد ما. لذا سنركّز على وصف الأفكار الرئيسية وراء هذا التحويل. (انظر أيضًا الشكل التوضيحي.) تعمّم NAND-RAM لغة NAND-TM بطريقتين رئيسيتين: (أ) بإضافة الوصول المفهرس إلى المصفوفات (أي صيغة Foo[bar])، و__(ب)__ بالانتقال من متغيرات ذات قيم بوليانية إلى متغيرات ذات قيم صحيحة. يتكوّن التحويل من خطوتين:

  1. الوصول المفهرس إلى مصفوفات البتات: نبدأ بإظهار كيف نتعامل مع (أ). أي أننا نبيّن كيف يمكننا تنفيذ العملية Setindex(Bar) في NAND-TM بحيث إذا كان Bar مصفوفة تُرمِّز لعدد صحيح jj، فإن قيمة i بعد تنفيذ Setindex(Bar) ستكون مساوية لـjj. وسيتيح لنا هذا محاكاة الصيغة Foo[Bar] عبر Setindex(Bar) يتبعها Foo[i].

  2. مصفوفات البتات ثنائية البعد: ثم نبيّن كيف يمكننا استخدام "السكر النحوي" لإثراء NAND-TM بـ_مصفوفات ثنائية البعد_. أي أن لدينا فهرسين هما i وj و_مصفوفات ثنائية البعد_، بحيث يمكننا استخدام الصيغة Foo[i][j] للوصول إلى الموضع (i,j) من Foo.

  3. مصفوفات الأعداد الصحيحة: وأخيرًا سنُرمِّز مصفوفة Arr أحادية البعد من الأعداد الصحيحة عبر مصفوفة Arrbin ثنائية البعد من البتات. الفكرة بسيطة: إذا كانت ai,0,…,ai,ℓa_{i,0},\ldots,a_{i,\ell} تمثيلًا ثنائيًا (خالٍ من البادئة) لـ Arr[ii]، فإن Arrbin[$i$][$j$]` سيكون مساويًا لـ ai,ja_{i,j}.

وبعد أن تتوفر لدينا مصفوفات الأعداد الصحيحة، يمكننا استخدام سكرنا النحوي المعتاد للدوال وGOTO إلخ لتنفيذ عمليات الحساب ومسار التنفيذ في NAND-RAM.

النهج أعلاه ليس الطريقة الوحيدة لإثبات مبرهنة — آلات تورينج (أي برامج NAND-TM) وآلات RAM (أي برامج NAND-RAM) متكافئة، انظر على سبيل المثال تمرين — برهان بديل لتكافؤ TM/RAM

ملاحظة — آلات RAM / NAND-RAM ولغة التجميع (اختياري)

تقابل آلات RAM المعالجات الدقيقة الفعلية عن قرب كبير، مثل معالجات سلسلة Intel x86 التي تحتوي أيضًا على ذاكرة أولية (primary memory) كبيرة وعدد ثابت من السجلات الصغيرة. وهذا ليس مصادفة بالطبع: فهدف آلات RAM هو نمذجة بنية أنظمة الحوسبة الفعلية عن قرب أكبر مما تفعله آلات تورينج، وهي بنى تتبع إلى حد بعيد ما يُعرف بـبنية فون نيومان كما وصفها التقرير [@vonNeumann45]. ونتيجة لذلك، فإن NAND-RAM مشابهة في مخططها العام لغات التجميع مثل x86 أو NIPS. وتحتوي جميع لغات التجميع هذه على تعليمات (1) لنقل البيانات من السجلات إلى الذاكرة، (2) لإجراء عمليات حسابية أو منطقية على السجلات، و__(3)__ للتنفيذ الشرطي والحلقات ("if" و"goto"، وتُعرف عادة بـ"الفروع" و"القفزات" في سياق لغات التجميع).

الفرق الرئيسي بين آلات RAM والمعالجات الدقيقة الفعلية (وبالتالي بين NAND-RAM ولغات التجميع) أن المعالجات الدقيقة الفعلية لها حجم كلمة ثابت ww، بحيث تحمل كل السجلات وخلايا الذاكرة أعدادًا في [2w][2^w] (أو ما يكافئ ذلك سلاسل في {0,1}w\{0,1\}^w). قد يختلف هذا العدد ww بين المعالجات المختلفة، لكن القيم الشائعة هي إما 3232 أو 6464. وكنموذج نظري، لا تفتقر آلات RAM إلى هذا القيد، بل نجعل ww يكون لوغاريتم زمن التنفيذ لدينا (وهو ما يقابل قيمته في الممارسة أيضًا). للمعالجات الدقيقة الفعلية أيضًا عدد ثابت من السجلات (مثلًا 14 سجلًا للأغراض العامة في x86-64)، لكن هذا لا يحدث فرقًا كبيرًا مع آلات RAM. ويمكن إظهار أن آلات RAM التي لا تملك سوى سجلين فقط قوية بقدر آلات RAM الكاملة التي تملك عددًا ثابتًا كبيرًا من السجلات.

بالطبع، للمعالجات الدقيقة الفعلية كذلك كثير من الميزات التي لا تتشاركها مع آلات RAM، بما فيها التوازي، وتسلسلات الذاكرة، والعديد غيرها. لكن آلات RAM تلتقط بالفعل الحواسيب الفعلية تقريبًا، ولذلك (كما سترى) فإن زمن تشغيل خوارزمية على آلة RAM (مثل O(n)O(n) مقابل O(n2)O(n^2)) يرتبط ارتباطًا وثيقًا بكفاءتها العملية.

التفاصيل القبيحة (اختياري)

لن نعرض البرهان الرسمي الكامل لـ مبرهنة — آلات تورينج (أي برامج NAND-TM) وآلات RAM (أي برامج NAND-RAM) متكافئة، بل نركّز على أهم الأجزاء: تنفيذ الوصول المفهرس، ومحاكاة المصفوفات ثنائية البعد بالمصفوفات أحادية البعد. حتى هذين الجزءين مملة في وصفها، ولن يكون ذلك مفاجئًا لأي شخص كتب مترجمًا (compiler) من قبل. لذا يمكنك أن تتصفح هذا القسم قراءة سريعة دون عناء. النقطة المهمة ليست أن تحفظ كل التفاصيل عن ظهر قلب، بل أن تُقنع بأن من الممكن في المبدأ تحويل برنامج NAND-RAM إلى برنامج NAND-TM مكافئ، وأن تُقنع حتى بأنك أنت تستطيع فعل ذلك، لو أردت، مع وقت كافٍ وجهد كافٍ.

الوصول المفهرس في NAND-TM

في NAND-TM لا يمكننا الوصول إلى مصفوفاتنا إلا في موضع متغير الفهرس i، بينما لدى NAND-RAM متغيرات ذات قيم صحيحة يمكن استخدامها لـ_الوصول المفهرس_ إلى المصفوفات، من صيغة Foo[bar]. لتنفيذ الوصول المفهرس في NAND-TM، سنُرمِّز الأعداد الصحيحة في مصفوفاتنا باستخدام تمثيل ما (خالٍ من البادئة) (انظر الترميزُ الخالي من البادئات))، ثم نضع إجراءً Setindex(Bar) يضبط i على القيمة التي يرمّزها Bar. يمكننا محاكاة أثر Foo[Bar] باستخدام Setindex(Bar) يتبعه Foo[i].

يمكن تنفيذ Setindex(Bar) على النحو التالي:

  1. نهيّئ مصفوفة Atzero بحيث يكون Atzero[00]=1=1 وAtzero[jj]=0=0 لكل j>0j>0. (يمكن dilakukan ذلك بسهولة في NAND-TM لأن كل المتغيرات غير المهيّأة تُعتبر صفرًا افتراضيًا.)

  2. نضبط i على صفر، بأن نقصنه حتى نصل إلى النقطة التي يكون فيها Atzero[i]=1=1.

  3. نجعل Temp مصفوفة ترمِّز للعدد 00.

  4. نستخدم GOTO لمحاكاة حلقة داخلية من الشكل: بينما Temp ≠\neq Bar، نزيد Temp.

  5. في نهاية الحلقة، تكون i مساوية للقيمة التي يرمّزها Bar.

في شيفرة NAND-TM (باستخدام بعض السكر النحوي)، يمكننا تنفيذ العمليات أعلاه على النحو التالي:

# assume Atzero is an array such that Atzero[0]=1
# and Atzero[j]=0 for all j>0

# set i to 0.
LABEL("zero_idx")
dir0 = zero
dir1 = one
# corresponds to i <- i-1
GOTO("zero_idx",NOT(Atzero[i]))
...
# zero out temp
#(code below assumes a specific prefix-free encoding in which 10 is the "end marker")
Temp[0] = 1
Temp[1] = 0
# set i to Bar, assume we know how to increment, compare
LABEL("increment_temp")
cond = EQUAL(Temp,Bar)
dir0 = one
dir1 = one
# corresponds to i <- i+1
INC(Temp)
GOTO("increment_temp",cond)
# if we reach this point, i is number encoded by Bar
...
# final instruction of program
MODANDJUMP(dir0,dir1)

المصفوفات ثنائية البعد في NAND-TM

لتنفيذ المصفوفات ثنائية البعد، نريد أن نضمّها (نُدرجها) في مصفوفة أحادية البعد. الفكرة أن نأتي بدالة واحد إلى واحد (one to one) embed:N×N→Nembed:\N \times \N \rightarrow \N، وبذلك نضمّ الموضع (i,j)(i,j) من المصفوفة ثنائية البعد Two في الموضع embed(i,j)embed(i,j) من المصفوفة One.

نظرًا إلى أن المجموعة N×N\N \times \N تبدو "أكبر بكثير" من المجموعة N\N، قد لا يكون من البديهي في البداية أن مثل هذا الوقوف الواحد إلى الواحد موجود. لكن بمجرد التفكير في الأمر أكثر، لا يكون بناؤه صعبًا إلى هذا الحد. على سبيل المثال، يمكنك أن تطلب من طفل أن يستخدم المقصّ واللصق ليحوّل قطعة ورق بمقاس 10" في 10" إلى شريط بمقاس 1" في 100". وهذا في جوهره وقوف واحد إلى واحد من [10]×[10][10]\times [10] إلى [100][100]. ويمكننا تعميم ذلك للحصول على وقوف واحد إلى واحد من [n]×[n][n]\times [n] إلى [n2][n^2]، وعمومًا من N×N\N \times \N إلى N\N. وبالتحديد، الوقوف embedembed التالي يكفي (انظر الشكل التوضيحي):

embed(x,y)=12(x+y)(x+y+1)+x    .embed(x,y) = \tfrac{1}{2}(x+y)(x+y+1)+x\;\;.

تصويرٌ للتطبيق  لـ؛ فيمكن للمرء أن يرى أنّ كلّ زوجين متمايزين  و يُحقّق .

تمرين — الأزواج يطلب إليك أن تُثبت أن embedembed يق فعلًا واحدًا إلى واحد، وكذلك أن حسابه ممكن ببرنامج NAND-TM. (ويمكن للأخير أن يتم ببساطة باتباع خوارزميات المدرسة الابتدائية في الضرب والجمع والقسمة.) وهذا يعني أننا نستطيع استبدال شيفرة من الشكل Two[Foo][Bar] = something (أي الوصول إلى المصفوفة ثنائية البعد Two عند الأعداد الصحيحة التي ترمّزها المصفوفتان أحاديتا البعد Foo وBar) بشيفرة من الشكل:

Blah = embed(Foo,Bar)
Setindex(Blah)
Two[i] = something

كل ما تبقّى

بعد أن تتوفر لدينا المصفوفات ثنائية البعد والوصول المفهرس، لا توجد محاكاة NAND-RAM بواسطة NAND-TM إلا مجرد مسألة تنفيذ الخوارزميات القياسية للعمليات الحسابية والمقارنات في NAND-TM. وإن كان هذا مرهقًا، فهو ليس صعبًا، والنتيجة النهائية هي إظهار أن كل برنامج NAND-RAM PP يمكن محاكاته ببرنامج NAND-TM QQ مكافئ له، وبذلك يكتمل برهان مبرهنة — آلات تورينج (أي برامج NAND-TM) وآلات RAM (أي برامج NAND-RAM) متكافئة.

ملاحظة — التهاكر في NAND-RAM (متقدّم)

مفهوم يظهر في كثير من لغات البرمجة لكننا لم نُدرجه في برامج NAND-RAM هو التهاكر (recursion). غير أن التهاكر (والمكالمات بين الدوال عمومًا) يمكن تنفيذه في NAND-RAM باستخدام بنية بيانات المكدس (stack data structure). المكدس (stack) هو بنية بيانات تحتوي على سلسلة من العناصر، حيث يمكننا "دفع" العناصر إليها و"سحبها" منها بترتيب "الأول داخلًا الأخير خارجًا" (first in last out).

يمكننا تنفيذ مكدس باستخدام مصفوفة من الأعداد الصحيحة اسمها Stack ومتغير عددي stackpointer يمثل عدد العناصر الموجودة في المكدس. وننفذ push(foo) mediante

Stack[stackpointer]=foo
stackpointer += one

وننفذ bar = pop() mediante

bar = Stack[stackpointer]
stackpointer -= one

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

الأمر أن بإمكاننا تنفيذ التهاكر باستخدام لغة غير تهاكرية ليس مثيرًا للدهشة. فعلى سبيل المثال، لا تملك لغات الآلة (machine languages) عادةً تهاكرًا (ولا مكالمات بين الدوال عمومًا)، ومن ثمّ ينفّذ المترجم مكالمات الدوال باستخدام مكدس وGOTO. يمكنك أن تجد على الإنترنت شروحات عن كيفية تنفيذ التهاكر عبر المكدس في لغة برمجة تحبها، سواء كانت Python أو JavaScript أو Lisp/Scheme.

تكافؤ تورينج (مناقشة)

/images/introtcs/lec_07_other_models-4.webp

أي من لغات البرمجة المعيارية مثل C وJava وPython وPascal وFortran لديها عمليات مشابهة جدًا لتلك في NAND-RAM. (وفي الحقيقة، في النهاية يمكن تنفيذها جميعًا بآلات تملك عددًا ثابتًا من السجلات ومصفوفة ذاكرة كبيرة.) لذلك، باستخدام مبرهنة — آلات تورينج (أي برامج NAND-TM) وآلات RAM (أي برامج NAND-RAM) متكافئة، يمكننا محاكاة أي برنامج في أي من لغات البرمجة هذه ببرنامج NAND-TM. وفي الاتجاه المقابل، فإن كتابة مُفسِّر (interpreter) لـ NAND-TM بأي من لغات البرمجة أعلاه هي تمرين برمجي سهل إلى حد كبير. لذلك يمكننا أيضًا محاكاة برامج NAND-TM (وبالتالي آلات تورينج بحسب مبرهنة — آلات تورينج وبرامج NAND-TM متكافئة) باستخدام لغات البرمجة هذه. تُسمى هذه الخاصية، أي التكافؤ في القوة مع آلات تورينج / NAND-TM، بـ_التكافؤ مع تورينج_ (Turing Equivalent) (وتُسمى أحيانًا الاكتمال بحسب تورينج). وعليه، فجميع لغات البرمجة التي نعرفها متكافئة مع تورينج.[1]

نموذج "أفضل الحلين"

التكافؤ بين آلات تورينج وآلات RAM يتيح لنا اختيار أنسب لغة للمهمة قيد اليد:

  • عندما نريد إثبات مبرهنة عن جميع البرامج/الخوارزميات، يمكننا استخدام آلات تورينج (أو NAND-TM) لأنها أبسط وأسهل في التحليل. وبشكل خاص، إذا أردنا إظهار أن دالة معينة لا يمكن حسابها، فإننا سنستخدم آلات تورينج.

  • عندما نريد إظهار أن دالة يمكن حسابها، يمكننا استخدام آلات RAM أو NAND-RAM، لأن البرمجة فيها أسهل، ولأنها تقابل عن قرب لغات البرمجة عالية المستوى التي اعتدنا عليها. وفي الواقع، كثيرًا ما سنصف برامج NAND-RAM بصيغة غير رسمية، واثقين من أن القارئ يستطيع ملء التفاصيل وترجمة الوصف عالي المستوى إلى البرنامج الدقيق. (وهذا مثل تمامًا الطريقة التي يستخدم بها الناس عادةً أوصافًا غير رسمية أو "شيفرة زائفة" (pseudocode) للخوارزميات، واثقين من أن جمهورهم سيعرف ترجمة هذه الأوصاف إلى شيفرة عند الحاجة.)

استخدامنا لآلات تورينج / NAND-TM وآلات RAM / NAND-RAM مشابه جدًا للطريقة التي يستخدم بها الناس في الممارسة لغات البرمجة عالية ومنخفضة المستوى. عندما يريد أحدهم إنتاج جهاز ينفّذ البرامج، يكون من الأنسب فعل ذلك بلغة برمجة بسيطة جدًا و"منخفضة المستوى". وعندما يريد أحدهم وصف خوارزمية، يكون من الأنسب استخدام أعلى صيغة رسمية ممكنة.

/images/introtcs/lec_07_other_models-5.webp

باستخدام نتائج التكافؤ مثل التكافؤ بين آلات تورينج وآلات RAM، يمكننا أن «نأكل الكعكة ونحتفظ بها أيضًا».

يمكننا استخدام نموذج أبسط مثل آلات تورينج عندما نريد إثبات أن شيئًا ما لا يمكن إنجازه، واستخدام نموذج غني بالميزات مثل آلات RAM عندما نريد إثبات أن شيئًا ما يمكن إنجازه.

لنتحدث عن التجريدات

"إن المبرمج في موقف فريد ... إذ عليه أن يكون قادرًا على التفكير وفق تسلسلات هرمية مفاهيمية أعمق بكثير مما واجهه أي عقل منفرد في أي وقت مضى."، إدسغر دايكسترا، "في قسوة تدريس علوم الحاسوب حقًا"، 1988.

في وقت ما من أي مقرر في نظريّة الحوسبة، يحتاج كلٌّ من المدرّس والطلاب إلى إجراء هذا الحديث. أي أننا نحتاج إلى مناقشة مستوى التجريد (level of abstraction) في وصف الخوارزميات. في مقرّرات الخوارزميات، يُكتب عادةً وصف الخوارزميات بالإنجليزية، مع الافتراض بأن القارئ يستطيع "ملء التفاصيل" وأنه قادر على تحويل تلك الخوارزمية إلى تنفيذ ملموس عند الحاجة. على سبيل المثال، خوارزمية — Breadth First Search هو وصف عالي المستوى لخوارزمية البحث بالعرض أولًا (breadth first search).

خوارزمية — Breadth First Search

Input: Graph $G$, vertices $u,v$
Output: "connected" when $u$ is connected to $v$ in $G$, "disconnected"


Initialize empty queue $Q$.
Put $u$ in $Q$
While{$Q$ is not empty}
   Remove top vertex $w$ from $Q$
   If{$w=v$} return "connected" endif
   Mark $w$
   Add all unmarked neighbors of $w$ to $Q$.
Endwhile
Return "disconnected"

لو أردنا إعطاء تفاصيل أكثر عن كيفية تنفيذ البحث بالعرض أولًا في لغة برمجة مثل Python أو C (أو NAND-RAM / NAND-TM على سبيل المثال)، لَوَصَفنا كيف ننفّذ بنية بيانات الطابور باستخدام مصفوفة، وكذلك كيف نستخدم المصفوفات لوضع علامات على الرؤوس. ونسمّي هذا الوصف "ذو المستوى الوسيط" وصفًا على مستوى التنفيذ (implementation level) أو وصفًا بالشيفرة الزائفة (pseudocode). وأخيرًا، إذا أردنا وصف التنفيذ بدقّة، لقَدّمنا الشيفرة الكاملة للبرنامج (أو تمثيلًا دقيقًا آخر تمامًا، مثلًا على شكل قائمة من الثنائيات (tuples)). ونسمّي هذا وصفًا رسميًا أو منخفض المستوى.

/images/introtcs/lec_07_other_models-6.webp

رغم أننا بدأنا بوصف برامج NAND-CIRC وNAND-TM وNAND-RAM على المستوى الرسمي الكامل، فإننا مع تقدّمنا في هذا الكتاب سننتقل إلى التنفيذ والوصف عالي المستوى. فهدفنا في النهاية ليس استخدام هذه النماذج في حساب فعلي، بل تحليل ظاهرة الحساب بصفة عامة. ومع ذلك، إن كنت لا تفهم كيف يُترجَم الوصف عالي المستوى إلى تنفيذ فعلي، فإن النزول "إلى المعدن" ("down to the metal") هو غالبًا تمرين ممتاز. ومن أهم المهارات التي يحتاجها عالم الحاسوب القدرة على الصعود والهبوط في التسلسلات الهرمية للتجريدات.

ينطبق تمييز مشابه على مفهوم التمثيل (representation) للأشياء بوصفها سلاسل. أحيانًا، من أجل الدقّة، نقدّم مواصفة منخفضة المستوى تصف بالضبط كيف يُرمَّز كائن ما في سلسلة ثنائية. على سبيل المثال، يمكننا وصف ترميز لرسوم بيانية ذات nn رأسًا كسلاسل ثنائية طولها n2n^2، بالقول إننا نرمّز رسمًا بيانيًا GG على الرؤوس [n][n] إلى سلسلة x∈{0,1}n2x\in \{0,1\}^{n^2} بحيث يكون الإحداثي رقم n⋅i+jn\cdot i + j في xx يساوي 11 إذا وفقط إذا كان الضلع i  j→\overrightarrow{i \; j} موجودًا في GG. ويمكننا أيضًا استخدام وصف وسيط أو على مستوى التنفيذ، بأن نقول ببساطة إننا نمثّل رسمًا بيانيًا باستخدام تمثيل مصفوفة التجاور (adjacency matrix).

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

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

اكتمال تورينج والتكافؤ، تعريف رسمي (اختياري)

النموذج الحوسبي (computational model) هو أي طريقة نعرّف بها معنى أن برنامج (يمثَّل بسلسلة) يحسب دالة (جزئية). إن النموذج الحوسبي M\mathcal{M} هو كامل بحسب تورينج إذا استطاعنا أن نُسقط كل آلة تورينج (أو بما يكافئها برنامج NAND-TM) NN في برنامج PP لـ M\mathcal{M} يحسب الدالة نفسها التي تحسبها NN. وهو متكافئ مع تورينج إذا كان الاتجاه الآخر قائمًا أيضًا (أي أننا نستطيع إسقاط كل برنامج في M\mathcal{M} في آلة تورينج تحسب الدالة نفسها). ويمكننا تعريف هذا المفهوم رسميًا على النحو التالي. (هذا التعريف الرسمي ليس جوهريًا لبقية هذا الكتاب، فلا تتردد في تخطيه ما دمت تفهم المفهوم العام للتكافؤ مع تورينج؛ ويُشار إلى هذا المفهوم أحيانًا في الأدبيات بـترقيم غودل (Gödel numbering) أو الترقيم المقبول (admissible numbering).)

تعريف — اكتمال تورينج والتكافؤ (اختياري)

لنفترض أن F\mathcal{F} هي مجموعة جميع الدوال الجزئية من {0,1}∗\{0,1\}^* إلى {0,1}∗\{0,1\}^*. النموذج الحوسبي هو دالة M:{0,1}∗→F\mathcal{M}:\{0,1\}^* \rightarrow \mathcal{F}.

نقول إن برنامجًا P∈{0,1}∗P \in \{0,1\}^* يحسب F∈FF\in \mathcal{F} بحسب M\mathcal{M} إذا كان M(P)=F\mathcal{M}(P) = F.

النموذج الحوسبي M\mathcal{M} هو كامل بحسب تورينج إذا وُجدت دالة قابلة للحساب ENCODEM:{0,1}∗→{0,1}∗ENCODE_{\mathcal{M}}:\{0,1\}^* \rightarrow \{0,1\}^* بحيث إن لكل آلة تورينج NN (مُمثَّلة كسلسلة) تكون M(ENCODEM(N))\mathcal{M}(ENCODE_{\mathcal{M}}(N)) مساوية للدالة الجزئية التي تحسبها NN.

النموذج الحوسبي M\mathcal{M} هو متكافئ مع تورينج إذا كان كاملًا بحسب تورينج، ووُجدت دالة قابلة للحساب DECODEM:{0,1}∗→{0,1}∗DECODE_{\mathcal{M}}:\{0,1\}^* \rightarrow \{0,1\}^* بحيث لكل سلسلة P∈{0,1}∗P\in \{0,1\}^*، تكون N=DECODEM(P)N=DECODE_{\mathcal{M}}(P) تمثيلًا سلسلة لآلة تورينج تحسب الدالة M(P)\mathcal{M}(P).

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

  • آلات تورينج
  • برامج NAND-TM
  • برامج NAND-RAM
  • حساب لامدا
  • لعبة الحياة (إسقاط البرامج والمدخلات والمخرجات على تكوينات البداية والنهاية)
  • لغات برمجة مثل Python/C/Javascript/OCaml... (مع السماح بتخزين غير محدود)

الآليات الخلوية

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

قواعد لعبة حياة كونواي (Conway's Game of Life). الصورة من هذه التدوينة.

مثال نموذجي على آلية خلوية هو لعبة حياة كونواي (Conway's Game of Life). في هذه الآلية تُرتَّب الخلايا في شبكة ثنائية البعد لا نهائية. لكل خلية حالتان فقط: "ميتة" (يمكن ترميزها بـ 00 والتعرّف عليها مع ∅\varnothing) أو "حيّة" (يمكن ترميزها بـ 11). تعتمد الحالة التالية للخلية على حالتها السابقة وعلى حالات جيرانها الثمانية الرأسية والأفقية والقطرية (انظر الشكل التوضيحي). تصبح الخلية الميتة حيّة فقط إذا كان ثلاثة من جيرانها بالضبط أحياء. وتبقى الخلية الحيّة على قيد الحياة إذا كان لديها جيران أحياء عددهما اثنان أو ثلاثة. على الرغم من أن عدد الخلايا قد يكون لا نهائيًا، يمكننا ترميز الحالة باستخدام سلسلة ذات طول متناهٍ عبر متابعة الخلايا الحيّة وحدها. فإذا هيّأنا النظام في تكوين يحوي عددًا متناهى من الخلايا الحيّة، فإن عدد الخلايا الحيّة سيبقى متناهى في جميع الخطوات اللاحقة. وتحتوي صفحة ويكيبيديا الخاصة بلعبة الحياة على بعض الأشكال والرسوم المتحركة الجميلة لتكوينات تنتج تطورات شديدة الإثارة.

/images/introtcs/lec_07_other_models-8.webp

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

تعريف — الآليات الخلوية أحادية البعد

لنفترض أن Σ\Sigma مجموعة منتهية تحتوي على الرمز ∅\varnothing. آلية خلوية أحادية البعد (one dimensional cellular automaton) فوق أبجدية Σ\Sigma تُوصَف بـ_قاعدة انتقال_ (transition rule) r:Σ3→Σr:\Sigma^3 \rightarrow \Sigma، تُحقّق r(∅,∅,∅)=∅r(\varnothing,\varnothing,\varnothing) = \varnothing.

تكوين (configuration) الآلية rr هو دالة A:Z→ΣA:\Z \rightarrow \Sigma. إذا كانت آلة ذات قاعدة rr في التكوين AA، فإن تكوينها التالي، الذي نرمز له بـ A′=NEXTr(A)A' = NEXT_r(A)، هو الدالة A′A' بحيث A′(i)=r(A(i−1),A(i),A(i+1))A'(i) = r(A(i-1),A(i),A(i+1)) لكل i∈Zi\in \Z. بعبارة أخرى، تُحصل الحالة التالية للآلة rr عند النقطة ii بتطبيق القاعدة rr على قيم AA عند ii وعند جاريها.

التكوين المتناهي. نقول إن تكوينًا للآلة rr هو متناهٍ إذا كان هناك عدد متناهٍ فقط من الفهارس i0,…,ij−1i_0,\ldots,i_{j-1} في Z\Z بحيث A(ij)≠∅A(i_j) \neq \varnothing. (أي أنه لكل i∉{i0,…,ij−1}i \not\in \{ i_0, \ldots, i_{j-1}\}، يكون A(i)=∅A(i)=\varnothing.) يمكن تمثيل مثل هذا التكوين بسلسلة متناهية تُرمِّز الفهارس i0,…,in−1i_0,\ldots,i_{n-1} والقيم A(i0),…,A(in−1)A(i_0),\ldots,A(i_{n-1}). ولأن R(∅,∅,∅)=∅R(\varnothing,\varnothing,\varnothing)=\varnothing، فإن كان AA تكوينًا متناهيًا فإن NEXTr(A)NEXT_r(A) يكون متناهيًا أيضًا. ولن يهمّنا إلا دراسة الآليات الخلوية التي تُهيَّأ في تكوينات متناهية، وتبقى كذلك في تكوين متناهٍ طوال تطورها. ولن يهمّنا إلا دراسة الآليات الخلوية التي تُهيَّأ في تكوينات متناهية، ومن ثمّ تبقى في تكوين متناهٍ طوال تطورها.

الآليات الخلوية أحادية البعد كاملة بحسب تورينج

يمكننا كتابة برنامج (مثلًا باستخدام NAND-RAM) يحاكي تطوّر أي آلية خلوية انطلاقًا من تكوين متناهٍ أولي، بمجرد تخزين قيم الخلايا التي حالتها لا تساوي ∅\varnothing وتطبيق القاعدة rr تكرارًا. وعليه يمكن محاكاة الآليات الخلوية بآلات تورينج. والأمر الأكثر مدهشة هو أن الاتجاه الآخر قائم أيضًا. فعلى سبيل المثال، مهما بدت قواعد لعبة الحياة بسيطة، يمكننا محاكاة آلة تورينج باستخدامها (انظر الشكل التوضيحي).

تكوينٌ للعبة الحياة يُحاكي آلة تورينج. الشكل بقلم Paul Rendell.

في الواقع، حتى الآليات الخلوية أحادية البعد يمكن أن تكون كاملة بحسب تورينج:

مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج

لكل آلة تورينج MM، هناك آلية خلوية أحادية البعد تستطيع محاكاة MM على كل مدخل xx.

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

الفكرة المفتاحية في برهان مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج هي أن الموضع الوحيد في شريط آلة تورينج MM الذي يمكن أن يتغيّر عند كل نقطة في الحساب هو الموضع الذي توجد فيه الرأس، والقيمة التي تتغيّر إليها هذه الخلية هي دالة لحالتها الحالية والحالة المتناهية لآلة MM. تتيح لنا هذه الملاحظة ترميز تكوين آلة تورينج MM على هيئة تكوين متناهٍ لآلية خلوية rr، وضمان أن تطوّر هذا التكوين المرمَّز خطوة واحدة تحت قواعد rr يقابل خطوة واحدة في تنفيذ آلة تورينج MM.

تكوينات آلات تورينج ودالة الخطوة التالية

لتحويل الأفكار أعلاه إلى برهان صارم (بل حتى إلى صياغة صارمة!) لـ مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج، سنحتاج إلى تعريف دقيق لمفهوم تكوينات آلات تورينج. وسيفيدنا هذا المفهوم أيضًا في الفصول اللاحقة.

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

تعريف — تكوين آلة تورينج.

لنفترض أن MM آلة تورينج أبجديتها الشريطية Σ\Sigma ومجال حالاتها [k][k]. تكوين MM هو سلسلة α∈Σ‾∗\alpha \in \overline{\Sigma}^* حيث Σ‾=Σ×({⋅}∪[k])\overline{\Sigma} = \Sigma \times \left( \{\cdot\} \cup [k] \right)، تُحقِّق أنه يوجد إحداثي واحد بالضبط ii بحيث αi=(σ,s)\alpha_i = (\sigma,s) لبعض σ∈Σ\sigma \in \Sigma وs∈[k]s\in [k]. أما جميع الإحداثيات الأخرى jj فيكون αj=(σ′,⋅)\alpha_j = (\sigma',\cdot) لبعض σ′∈Σ\sigma'\in \Sigma.

يقابل تكوين α∈Σ‾∗\alpha \in \overline{\Sigma}^* لـ MM مع الحالة التالية لتنفيذه:

  • يحتوي شريط MM على αj,0\alpha_{j,0} لكل j<∣α∣j<|\alpha|، وعلى ∅\varnothing لكل المواضع التي تكون مساوية لـ ∣α∣|\alpha| أو أكبر، حيث نترك αj,0\alpha_{j,0} تكون القيمة σ\sigma بحيث αj=(σ,t)\alpha_j = (\sigma,t) مع σ∈Σ\sigma \in \Sigma وt∈{⋅}∪[k]t \in \{\cdot \} \cup [k]. (بعبارة أخرى، بما أن αj\alpha_j زوج من رمز أبجدية σ\sigma وإما حالة في [k][k] أو الرمز ⋅\cdot، فإن αj,0\alpha_{j,0} هو المكوّن الأول σ\sigma من هذا الزوج.)

  • رأس MM موجود في الموضع ii الفريد الذي يكون αi\alpha_i فيه على الشكل (σ,s)(\sigma,s) مع s∈[k]s\in [k]، وحالة MM مساوية لـ ss.

توقّف وتأمّل

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

فكّر في جميع المكوّنات التي تحتاج إلى معرفتها لتتمكّن من متابعة التنفيذ من هذه النقطة فصاعدًا، وما هي طريقة بسيطة لترميزها باستخدام قائمة من رموز متناهية. وبشكل خاص، بالنظر إلى تطبيقاتنا المستقبلية، حاول أن تفكّر في ترميز يجعل إسقاط التكوين عند الخطوة tt على التكوين عند الخطوة t+1t+1 بسيطًا قدر الإمكان.

تعريف — تكوين آلة تورينج. مُعقَّد بعض الشيء، لكن في النهاية التكوين هو مجرد سلسلة تُرمِّز لقطة (snapshot) لآلة تورينج عند نقطة معيّنة في التنفيذ. (بلغة أنظمة التشغيل، هذه "نسخة ذاكرة أساسية" (core dump).) يحتاج مثل هذه اللقطة أن تُرمِّز المكوّنات التالية:

  1. موضع الرأس الحالي.

  2. المحتويات الكاملة للذاكرة واسعة النطاق، أي الشريط.

  3. محتويات "السجلات المحلية"، أي حالة الآلة.

التفاصيل الدقيقة لكيفية ترميز التكوين ليست مهمة، لكننا نودّ أن نُسجّل الحقيقة البسيطة التالية:

لمّة

لنفترض أن MM آلة تورينج، وأن NEXTM:Σ‾∗→Σ‾∗NEXT_M:\overline{\Sigma}^* \rightarrow \overline{\Sigma}^* هي الدالة التي تُسقط تكوينًا لـ MM على التكوين عند الخطوة التالية من التنفيذ. عندها لكل i∈Ni \in \N، لا تعتمد قيمة NEXTM(α)iNEXT_M(\alpha)_i إلا على الإحداثيات αi−1,αi,αi+1\alpha_{i-1},\alpha_i,\alpha_{i+1}.

(لتبسيط الرمز، استخدمنا أعلاه الاصطلاح الذي إذا كان ii "خارج الحدود"، مثل i<0i<0 أو i>∣α∣i>|\alpha|، فإننا نفترض أن αi=(∅,⋅)\alpha_i = (\varnothing,\cdot).) نترك إثبات لمّة كتمرين في تمرين — دالة الخطوة التالية محلية. الفكرة وراء البرهان بسيطة: إذا لم تكن الرأس عند الموضع ii ولا عند المواضع i−1i-1 وi+1i+1، فإن تكوين الخطوة التالية عند ii سيكون كما كان سابقًا. وإلا، يمكننا "قراءة" حالة آلة تورينج وقيمة الشريط عند موضع الرأس من التكوين عند ii أو عند أحد جيرانه، واستخدام ذلك لتحديث ما يجب أن تكون عليه الحالة الجديدة عند ii. إكمال البرهان الكامل ليس صعبًا، لكن فعله طريقة ممتازة للتأكد من أنك مرتاح لتعريف التكوينات.

إكمال برهان مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج. يمكننا الآن إعادة صياغة مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج بصورة أكثر رسمية، وإكمال برهانه:

مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج (الصياغة الرسمية)

لكل آلة تورينج MM، إذا رمزنا بـ Σ‾\overline{\Sigma} أبجدية سلاسل تكويناتها، فإن هناك آلية خلوية أحادية البعد rr فوق الأبجدية Σ‾∗\overline{\Sigma}^* بحيث NEXTM(α)=NEXTr(α) NEXT_M \left( \alpha \right) = NEXT_r \left( \alpha \right) لكل تكوين α∈Σ‾∗\alpha \in \overline{\Sigma}^* لـ MM (مع استخدام العرف نفسه الذي فيه نعتبر αi=∅\alpha_i=\varnothing إذا كان ii "خارج الحدود").

برهان

نعتبر أن العنصر (∅,⋅)(\varnothing,\cdot) في Σ‾\overline{\Sigma} يقابل العنصر ∅\varnothing في الآلية rr. وفي هذه الحالة، وبحسب لمّة، فإن الدالة NEXTMNEXT_M التي تُسقط تكوينًا لـ MM على التالي هي في الواقع قاعدة صحيحة لآلية أحادية البعد.

الآلية الناتجة عن برهان مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج (الصياغة الرسمية) لها أبجدية كبيرة، وفوق ذلك حجمها يعتمد على الآلة MM التي يجري محاكاتها. ويبيّن أنه يمكن الحصول على آلية بأبجدية ذات حجم ثابت مستقل عن البرنامج الذي يُحاكى، بل يمكن أن تكون أبجدية الآلية هي المجموعة الدنيا {0,1}\{0,1\}! انظر الشكل التوضيحي لمثال على مثل هذه الآلة الكاملة بحسب تورينج.

تطوّر آلة ذاتية أحادية البعد. كلّ صفٍّ في الشكل يقابل تكوينًا. ويقابل التكوين الأولي الصفّ العلوي ويحوي خليةً «حيّة» واحدة فحسب. ويقابل هذا الشكل آلة «القاعدة 110» لستيفن وولفروم (Rule 110) وهي مكتملة حسابيًا (Turing Complete). الشكل مأخوذ من Wolfram MathWorld.

ملاحظة — تكوينات برامج NAND-TM

يمكننا استخدام الأسلوب نفسه في تعريف — تكوين آلة تورينج. لتعريف تكوينات برنامج NAND-TM. وسيحتاج مثل هذا التكوين أن يُرمِّز:

  1. القيمة الحالية للمتغير i.

  2. لكل متغير عددي foo، قيمة foo.

  3. لكل متغير مصفوفي Bar، القيمة Bar[jj] لكل j∈{0,…,t−1}j \in \{0,\ldots, t-1\} حيث t−1t-1 هي أكبر قيمة بلغها متغير الفهرس i في الحساب.

حساب لامدا ولغات البرمجة الوظيفية

حساب لامدا (λ calculus) طريقة أخرى لتعريف الدوال القابلة للحساب. وقد اقترحه أوندزو تشُرْش (Alonzo Church) في ثلاثينيات القرن الماضي في وقت قريب جدًا من اقتراح آلان تورينج لآلة تورينج. ومن المثير للاهتمام أنه بينما لا تُستخدم آلات تورينج في الحساب العملي، إلا أن حساب لامدا ألهم لغات برمجة وظيفية مثل LISP وML وHaskell، وبدوره ألهم وغيرها من لغات البرمجة. وسنعرض في هذا القسم حساب لامدا ونبيّن أن قوته مكافئة لبرامج NAND-TM (وبالتالي أيضًا لآلات تورينج). ويحتوي مستودع Github لدينا على دفتر Jupyter فيه تنفيذ بلغة Python لحساب لامدا، يمكنك التجريب فيه لتكتسب إحساسًا أفضل بهذا الموضوع.

عامل λ\lambda. جوهر حساب لامدا هو طريقة لتعريف دوال "مجهولة الاسم". فعلى سبيل المثال، بدلًا من إعطاء اسم ff لدالة وتعريفها بأن f(x)=x×x f(x) = x\times x يمكننا كتابتها على أنها λx.x×x \lambda x. x\times x وبالتالي (λx.x×x)(7)=49(\lambda x.x\times x)(7)=49. أي أنك تستطيع التفكير في λx.exp(x)\lambda x. exp(x)، حيث expexp هو تعبير ما، على أنها طريقة لتحديد الدالة المجهولة x↦exp(x)x \mapsto exp(x). وتظهر الدوال المجهولة، باستخدام λx.f(x)\lambda x.f(x) أو x↦f(x)x \mapsto f(x) أو رموز أخرىمرتبط، في كثير من لغات البرمجة. فعلى سبيل المثال، في Python يمكننا تعريف دالة التربيع باستخدام lambda x: x*x، بينما في JavaScript يمكننا استخدام x => x*x أو (x) => x*x. وفي Scheme سنعرّفها على أنها (lambda (x) (* x x)). ومن الواضح أن اسم مُعامل الدالة لا يهم، ومن ثمّ فإن λy.y×y\lambda y.y\times y هو نفسه λx.x×x\lambda x.x \times x، إذ يقابلان كلاهما دالة التربيع.

حذف الأقواس. لتقليل فوضى الرمز، فإننا عند كتابة تعبيرات حساب لامدا نُسقط غالبًا أقواس تقييم الدوال. فعوضًا عن كتابة f(x)f(x) لنتيجة تطبيق الدالة ff على المدخل xx، يمكننا أيضًا كتابة ذلك ببساطة f  xf\; x. ولذلك يمكننا كتابة (λx.x×x)7=49(\lambda x.x\times x) 7=49. وسنستخدم في هذا الفصل كلَّيْ رمزي f(x)f(x) وf  xf\; x لتطبيق الدوال. تطبيقات الدوال تجَمّعية وترتبط من اليسار إلى اليمين، ومن ثمّ فإن f  g  hf\;g\;h هو نفسه (fg)h(f g) h.

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

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

(((λf.(λy.(f  (f  y))))(λx.x×x))  3)(((\lambda f.(\lambda y.(f \;(f\; y)))) (\lambda x. x\times x))\; 3)

توقّف وتأمّل

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

لنقيّم المعادلة خطوة في كل مرة. ولئن كان السماح بالدوال المجهولة في حساب لامدا أمرًا لطيفًا، فإن إضافة الأسماء مفيدة جدًا لفهم التعبيرات المعقّدة. ولذلك، لنكتب F=λf.(λy.(f(fy)))F = \lambda f.(\lambda y.(f (f y))) و g=λx.x×xg = \lambda x.x\times x.

وعليه يصبح المعادلة ((F  g)  3)  . ((F \; g)\; 3) \;.

على مدخل دالة ff، تُخرج FF الدالة λy.(f(f  y))\lambda y.(f (f\; y))، أو بعبارة أخرى FfF f هي الدالة y↦f(f(y))y \mapsto f(f(y)). ودالتنا gg هي ببساطة g(x)=x2g(x)=x^2، ومن ثمّ فإن (Fg)(F g) هي الدالة التي تُسقط yy على (y2)2=y4(y^2)^2= y^4. وعليه ((Fg)3)=34=81((F g) 3) = 3^4 = 81.

تمرين محلول

إلى أيّ عدد يُقيَّم التعبير التالي؟

((λx.(λy.x))  2)  9  .((\lambda x.(\lambda y.x)) \; 2)\; 9 \;.

الحل

الدالة λy.x\lambda y.x هي الدالة التي على مدخل yy تتجاهل مدخلها وتُخرج xx. وعليه فإن (λx.(λy.x))2(\lambda x.(\lambda y.x)) 2 تُنتج الدالة y↦2y \mapsto 2 (أو، باستخدام رمز λ\lambda، الدالة λy.2\lambda y. 2). وعليه فإن المعادلة هو مكافئ لـ (λy.2)9=2(\lambda y. 2) 9 = 2.

الحصول على دوال متعدّدة الوسائط عبر Curry

في تعبير λ\lambda من الشكل λx.e\lambda x. e، يمكن أن يأتي التعبير ee نفسه فيه معامل λ\lambda. إذن مثلًا الدالة λx.(λy.x+y) \lambda x. (\lambda y. x+y) تُسقط xx على الدالة y↦x+yy \mapsto x+y.

وبشكل خاص، إذا استدعينا الدالة المعادلة على aa لنحصل على دالة ff، ثم استدعينا ff على bb، نحصل على القيمة a+ba+b. ونرى أن الدالة أحادية الوسيط المعادلة المقابلة لـ a↦(b↦a+b)a \mapsto (b \mapsto a+b) يمكن أيضًا التفكير فيها على أنها الدالة ثنائية الوسيط (a,b)↦a+b(a,b) \mapsto a+b. وبشكل عام، يمكننا استخدام تعبير λ\lambda ‏λx.(λy.f(x,y))\lambda x.(\lambda y.f(x,y)) لمحاكاة أثر دالة ذات وسيطين (x,y)↦f(x,y)(x,y) \mapsto f(x,y). وتُعرف هذه التقنية بـCurrying (currying). وسنستخدم الاختصار λx,y.e\lambda x,y. e من أجل λx.(λy.e)\lambda x. (\lambda y. e). إذا كان f=λx.(λy.e)f= \lambda x.(\lambda y.e) عندها (fa)b(f a) b يقابل تطبيق faf a ثم استدعاء الدالة الناتجة على bb، مما ينتج عنه نتيجة استبدال مواضع xx في ee بـ aa. وبقواعد التجميع التي لدينا، فإن هذا هو نفسه (fab)(f a b)، وهو ما سنكتبه أحيانًا أيضًا f(a,b)f(a,b).

في تحوّل Currying، يمكننا إحداث أثر الدالة ذات المعاملين  بتعبير λ  الذي يُخرج على المدخل  دالةً بمعاملٍ واحد  يكون فيها  مُثبَّتًا مسبقًا فيها وتُحقّق . ويمكن تصوير ذلك بمخطّط دائرة؛ انظر موقع Chelsea Voss.

الوصف الرسمي لحساب لامدا

نقدّم الآن وصفًا رسميًا لحساب لامدا. نبدأ بـ"تعبيرات أساسية" تحتوي على متغير واحد مثل xx أو yy، ونبني منها تعبيرات أعقد من الشكل (e  e′)(e \; e') وλx.e\lambda x.e، حيث e,e′e,e' تعبيران وxx مُعرِّف متغير. وتُعرَّف تعبيرات λ\lambda رسميًا على النحو التالي:

تعريف — تعبير λ\lambda.

تعبير λ\lambda هو إما مُعرِّف متغير واحد، أو تعبيرًا ee من أحد الشكلين التاليين:

  • التطبيق: e=(e′  e′′)e = (e' \; e'')، حيث e′e' وe′′e'' تعبيرا λ\lambda.

  • التجريد: e=λx.(e′)e = \lambda x.(e')، حيث e′e' تعبير λ\lambda.

تعريف — تعبير λ. هو تعريف تكراري (recursive definition) لأننا عرّفنا مفهوم تعبيرات λ\lambda في مصطلح نفسه. قد يبدو هذا محيّرًا في البداية، لكن في الحقيقة لقد اطلعت على التعريفات التكرارية منذ كنت تلميذًا في المدرسة الابتدائية. فكّر في كيفية تعريفنا لتعبير حسابي (arithmetic expression): إنه تعبير إما مجرد رقم، أو يأخذ أحد الأشكال (e+e′)(e + e') أو (e−e′)(e - e') أو (e×e′)(e \times e') أو (e÷e′)(e \div e')، حيث ee وe′e' تعبيرات حسابية أخرى.

المتغيرات الحرة والمربوطة. المتغيرات في تعبير λ\lambda تكون إما حرة (free) أو مربوطة (bound) بمعامل λ\lambda (بالمعنى الموضَّح في تحليل الصيغ: المتغيّرات المربوطة والحرّة). في تعبير λ\lambda أحادي المتغير varvar، المتغير varvar حر. ومجموعة المتغيرات الحرة والمربوطة في تعبير تطبيق e=(e′  e′′)e = (e' \; e'') هي نفسها مجموعة المتغيرات في التعبيرين e′e' وe′′e'' الأساسيين. أما في تعبير تجريد e=λvar.(e′)e = \lambda var.(e')، فإن جميع المواضع الحرة لـ varvar في e′e' تكون مربوطة بمعامل λ\lambda في ee. إن وجدتَ مفهوم المتغيرات الحرة والمربوطة محيّرًا، فيمكنك تفادي كل هذه القضايا باستخدام مُعرِّفات فريدة لكل المتغيرات.

الأولوية والأقواس. سنستخدم القواعد التالية التي تتيح لنا إسقاط بعض الأقواس. ترتبط تطبيقات الدوال من اليسار إلى اليمين، ومن ثمّ فإن fghfgh هو نفسه (fg)h(fg)h. ولتطبيق الدوال أولوية أعلى من معامل λ\lambda، ومن ثمّ فإن λx.fgx\lambda x.fgx هو نفسه λx.((fg)x)\lambda x.((fg)x). وهذا مشابه لاستخدامنا قواعد الأولوية في العمليات الحسابية لنتمكن من استخدام أقواس أقل، ومن ثمّ كتابة التعبير (7×3)+2(7 \times 3) + 2 على صورة 7×3+27\times 3 + 2. وكما ذُكر في الحصول على دوال متعدّدة الوسائط عبر Curry، فإننا نستخدم أيضًا الاختصار λx,y.e\lambda x,y.e من أجل λx.(λy.e)\lambda x.(\lambda y.e)، والاختصار f(x,y)f(x,y) من أجل (f  x)  y(f\; x)\; y. وهذا ينسجم جيدًا مع تحويل "Currying" الذي يحاكي الدوال متعددة المداخل بتعبيرات λ\lambda.

تكافؤ تعبيرات λ\lambda. كما رأينا في تمرين محلول، فإن القاعدة التي تقول إن (λx.exp)exp′(\lambda x. exp) exp' مكافئة لـ exp[x→exp′]exp[x \rightarrow exp'] تتيح لنا تعديل تعبيرات λ\lambda للحصول على صيغة مكافئة أبسط لها. ومن القواعد الأخرى التي نستطيع استخدامها أن المعامل لا يهم، ومن ثمّ فإن λy.y\lambda y.y هو نفسه λz.z\lambda z.z. ومعًا تعرّف هاتان القاعدتان مفهوم تكافؤ تعبيرات λ\lambda:

تعريف — تكافؤ تعبيرات λ\lambda

تعبيرا λ\lambda يكونان متكافئين إذا أمكِن تحويلهما إلى التعبير نفسه بتطبيقات متكررة للقواعد التالية:

  1. التقييم (أو اختزال β\beta): التعبير (λx.exp)exp′(\lambda x.exp) exp' مكافئ لـ exp[x→exp′]exp[x \rightarrow exp'].

  2. إعادة تسمية المتغيرات (أو التحويل α\alpha): التعبير λx.exp\lambda x.exp مكافئ لـ λy.exp[x→y]\lambda y.exp[x \rightarrow y].

إذا كان expexp تعبير λ\lambda من الشكل λx.exp′\lambda x.exp'، فإنه يقابل بشكل طبيعي الدالة التي تُسقط أي مدخل zz على exp′[x→z]exp'[x \rightarrow z]. وعليه فإن حساب لامدا يستلزم بشكل طبيعي نموذجًا حوسبيًا. وبما أن المداخل في حساب لامدا يمكن أن تكون بدورها دوالًا، فعلينا أن نقرّر بترتيب تقييم تعبير مثل (λx.f)(λy.g  z)  . (\lambda x.f)(\lambda y.g\; z) \;. وهناك عُرفان طبيعيان لهذا:

  • الاستدعاء بالاسم (أي "التقييم الكسول", lazy evaluation): نقيّم المعادلة بأن نُدخل أولًا التعبير (λy.g  z)(\lambda y.g\; z) في الطرف الأيسر كمدخل، فنحصل على f[x→(λy.g  z)]f[x \rightarrow (\lambda y.g\; z)] ثم نكمل من هناك.

  • الاستدعاء بالقيمة (أي "التقييم النشِط", eager evaluation): نقيّم المعادلة بأن نقيّم أولًا الطرف الأيمن فنحصل على h=g[y→z]h=g[y \rightarrow z]، ثم نُدخل هذا في الطرف الأيسر لنحصل على f[x→h]f[x \rightarrow h].

ولأن حساب لامدا لا يحتوي إلا على دوال نقيّة (pure) لا تملك "آثارًا جانبية" (side effects)، فإن الترتيب لا يهم في كثير من الحالات. وفي الواقع، يمكن إظهار أن إن وصلنا إلى تعبير غير قابل للاختصار وحتم (مثلًا عدد) في كلا الاستراتيجيتين، فسيكون التعبير نفسه. غير أنه، ولأجل الوضوح، سنستخدم دائمًا ترتيب "الاستدعاء بالاسم" (أي التقييم الكسول). (الاختيار نفسه قائم في لغة البرمجة Haskell، وإن كانت كثير من لغات البرمجة الأخرى تستخدم التقييم النشِط.) (formally)، ويُرمَّز تقييم تعبير λ\lambda باستخدام "الاستدعاء بالاسم" بالعملية التالية:

تعريف — تبسيط تعبيرات λ\lambda

لنفترض أن ee تعبير λ\lambda. تبسيط ee هو ناتج العملية التكرارية التالية:

  1. إذا كان ee متغيرًا واحدًا xx فإن تبسيط ee هو ee.

  2. إذا كان ee على الشكل e=λx.e′e= \lambda x.e' فإن تبسيط ee هو λx.f′\lambda x.f' حيث f′f' هو تبسيط e′e'.

  3. (التقييم / اختزال β\beta.) إذا كان ee على الشكل e=(λx.e′  e′′)e=(\lambda x.e' \; e'') فإن تبسيط ee هو تبسيط e′[x→e′′]e'[x \rightarrow e'']، الذي يعني استبدال جميع نسخ xx في e′e' المربوطة بمعامل λ\lambda بـ e′′e''

  4. (إعادة التسمية / التحويل α\alpha.) يُحصل على التبسيط القياسي (canonical simplification) لـ ee بأخذ تبسيط ee وإعادة تسمية المتغيرات بحيث يكون أول متغير مربوط في التعبير هو v0v_0، والثاني هو v1v_1، وهكذا وهكذا.

نقول إن تعبيرَي λ\lambda هما ee وe′e' متكافئان، ويُرمَّز ذلك بـ e≅e′e \cong e'، إذا كان لهما التبسيط القياسي نفسه.

تمرين محلول — تكافؤ تعبيرات λ\lambda

أَثبِت أن التعبيرين ee وff التاليين متكافئان:

e=λx.xe = \lambda x.x f=(λa.(λb.b))(λz.z  z)f = (\lambda a.(\lambda b.b)) (\lambda z.z\; z)

الحل

التبسيط القياسي لـ ee هو ببساطة λv0.v0\lambda v_0.v_0. ولإجراء التبسيط القياسي لـ ff نستخدم أولًا اختزال β\beta لنُدخل λz.zz\lambda z.zz بدلًا من aa في (λb.b)(\lambda b.b)، لكن بما أن aa غير مستخدمة في هذه الدالة إطلاقًا، فإننا نحصل ببساطة على λb.b\lambda b.b التي تختزل أيضًا إلى λv0.v0\lambda v_0.v_0.

الحلقات اللانهائية في حساب لامدا

كما في آلات تورينج وبرامج NAND-TM، يمكن لعملية التبسيط في حساب لامدا أن تدخل أيضًا في حلقة لا نهائية. فعلى سبيل المثال، خذ التعبير λ\lambda التالي

λx.xx  λx.xx \lambda x.xx \; \lambda x.xx

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

حساب لامدا "المُعزَّز"

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

يتضمن حساب لامدا المعزّز مجموعة الكائنات والعمليات التالية:

  • الثوابت البوليانية ودالة IF: توجد تعبيرات λ\lambda هي 00 و11 وIFIF تحقق الشروط التالية: لكل تعبيرَي λ\lambda هما ee وff، لدينا IF  1  e  f=eIF\; 1\;e\;f = e وIF  0  e  f=fIF\;0\;e\;f = f. أي أن IFIF هي الدالة التي تعطى ثلاثة وسائط a,e,fa,e,f فتُخرج ee إذا كان a=1a=1 وff إذا كان a=0a=0.

  • الأزواج: يوجد تعبير λ\lambda هو PAIRPAIR سنفكّر فيه على أنه دالة الأزواج (pairing). لكل تعبيرَي λ\lambda هما e,fe,f، يكون PAIR  e  fPAIR\; e\; f هو الزوج ⟨e,f⟩\langle e,f \rangle الذي يحتوي ee كعضو أول وff كعضو ثاني. ولدينا أيضًا تعبيرا λ\lambda هما HEADHEAD وTAILTAIL يستخرجان على التوالي عضو الأول والثاني من الزوج. وعليه، لكل تعبيرَي λ\lambda هما e,fe,f، لدينا HEAD  (PAIR  e  f)=eHEAD\; (PAIR \; e\; f) = e وTAIL  (PAIR  e  f)=fTAIL \; (PAIR \; e\; f) = f.[2]

  • القوائم والسلاسل النصية: يوجد تعبير λ\lambda هو NILNIL يقابل القائمة الفارغة، الذي نرمز له أيضًا بـ ⟨⊥⟩\langle \bot \rangle. باستخدام PAIRPAIR وNILNIL نبني القوائم. الفكرة أنه إذا كان LL قائمة من kk عناصر على الشكل ⟨e1,e2,…,ek,⊥⟩\langle e_1, e_2, \ldots, e_k, \bot \rangle، إذًا لكل تعبير λ\lambda هو e0e_0 يمكننا الحصول على قائمة من k+1k+1 عناصر ⟨e0,e1,e2,…,ek,⊥⟩\langle e_0,e_1, e_2, \ldots, e_k, \bot \rangle باستخدام التعبير PAIR  e0  LPAIR\; e_0 \; L. على سبيل المثال، لكل ثلاثة تعبيرات λ\lambda هي e,f,ge,f,g، يقابل التالي قائمة من ثلاثة عناصر ⟨e,f,g,⊥⟩\langle e,f,g,\bot \rangle: PAIR  e  (PAIR  f  (PAIR  g  NIL))  . PAIR \; e \; \left(PAIR\; f \; \left( PAIR\; g \; NIL \right) \right) \;.

تعيد الدالة ISEMPTYISEMPTY القيمة 11 على NILNIL وتُعيد 00 على كل قائمة أخرى. و_السلسلة النصية_ (string) هي ببساطة قائمة من البتات.

  • عمليات القوائم: يحتوي حساب لامدا المعزّز أيضًا على دوال معالجة القوائم وهي MAPMAP وREDUCEوREDUCE وFILTER. إذا أعطينا قائمة L=⟨x0,…,xn−1,⊥⟩L= \langle x_0,\ldots,x_{n-1}, \bot \rangle ودالة ff، فإن MAP  L  fMAP\; L \; f تطبّق ff على كل عنصر في القائمة فتحصل على القائمة الجديدة L′=⟨f(x0),…,f(xn−1),⊥⟩L'= \langle f(x_0),\ldots,f(x_{n-1}), \bot \rangle. وإذا أعطينا قائمة LL على النحو أعلاه وتعبيرًا ff يكون ناتجه إما 00 أو 11، فإن FILTER  L  fFILTER\; L\; f تُعيد القائمة ⟨xi⟩fxi=1\langle x_i \rangle_{f x_i = 1} التي تحتوي كل عناصر LL التي تُخرج ff لها 11. وتطبّق الدالة REDUCEعملية"دمج"علىقائمة.فعلىسبيلالمثال،ستُعيدREDUCE عملية "دمج" على قائمة. فعلى سبيل المثال، ستُعيد REDUCE; L ; + ; 0مجموعكلعناصرالقائمة مجموع كل عناصر القائمة L. وبشكل أكثر عمومية، تأخذ REDUCEقائمةREDUCE قائمة L،وعملية، وعملية f(نفكّرفيهاعلىأنهاتأخذوسيطين)،وتعبير (نفكّر فيها على أنها تأخذ وسيطين)، وتعبير \lambdaهو هو z(نفكّرفيهعلىأنه"العنصرالمحايد"للعملية (نفكّر فيه على أنه "العنصر المحايد" للعملية f$، مثل 00 للجمع و11 للضرب). ويُعرَّف الناتج بـ REDUCE  L  f  z={zL=NILf  (HEAD  L)  (REDUCE  (TAIL  L)  f  z)otherwise  .REDUCE\;L\;f\;z = \begin{cases}z & L=NIL \\ f\;(HEAD\; L) \; (REDUCE\;(TAIL\; L)\;f\;z) & \text{otherwise}\end{cases}\;. انظر الشكل التوضيحي لتوضيح عمليات معالجة القوائم الثلاث.

  • التهاكر: وأخيرًا، نريد أن نتمكّن من تنفيذ دوال تهاكرية. ولأن الدوال في حساب لامدا مجهولة الاسم، فلا يمكننا كتابة تعريف من الشكل f(x)=blahf(x) = blah حيث يحتوي blahblah على استدعاءات إلى $f. بدلًا من ذلك نستخدم دوال fتأخذمدخلًاإضافيًاf تأخذ مدخلًا إضافيًا me كوسيط. وسيأخذ المعامل RECURSEمثلهذهالدالةRECURSE مثل هذه الدالة f كمدخل ويُعيد "نسخة تهاكرية" من fحيثتُستبدلجميعالاستدعاءاتإلىf حيث تُستبدل جميع الاستدعاءات إلى me باستدعاءات تهاكرية إلى هذه الدالة. أي أنه إذا كانت لدينا دالة FتأخذوسيطينهماF تأخذ وسيطين هما me وx،فإنx، فإن RECURSE; F ستكون الدالة fالتيتأخذوسيطًاواحدًاf التي تأخذ وسيطًا واحدًا xبحيث بحيث f(x) = F(f,x)لكل لكل x$.

تمرين محلول — حساب NAND باستخدام حساب لامدا

أعطِ تعبير λ\lambda هو NN بحيث N  x  y=NAND(x,y)N\;x\;y = NAND(x,y) لكل x,y∈{0,1}x,y \in \{0,1\}.

الحل

قيمة NANDNAND لـ x,yx,y تساوي 11 إلا إذا كان x=y=1x=y=1. وعليه يمكننا كتابة N=λx,y.IF(x,IF(y,0,1),1) N = \lambda x,y.IF(x,IF(y,0,1),1)

تمرين محلول — حساب XOR باستخدام حساب لامدا

أعطِ تعبير λ\lambda هو XORXOR بحيث لكل قائمة L=⟨x0,…,xn−1,⊥⟩L=\langle x_0, \ldots, x_{n-1}, \bot \rangle حيث xi∈{0,1}x_i \in \{0,1\} لكل i∈[n]i\in [n]، تُقيَّم XORLXOR L إلى ∑ximod  2\sum x_i \mod 2.

الحل

أولًا، نلاحظ أننا نستطيع حساب XOR لبتّين على النحو التالي: NOT=λa.IF(a,0,1) NOT = \lambda a. IF(a,0,1) و XOR2=λa,b.IF(b,NOT(a),a) XOR_2 = \lambda a,b. IF(b,NOT(a),a)

(نستخدم هنا قدرًا من السكر النحوي لوصف الدوال. وللحصول على تعبير λ\lambda الخاص بـ XOR، سنكتفي باستبدال التعبير المعادلة في المعادلة.) والآن يمكننا تعريف XOR لقائمة على النحو التكراري التالي:

XOR(L)={0L is emptyXOR2(HEAD(L),XOR(TAIL(L)))otherwise XOR(L) = \begin{cases} 0 & \text{$L$ is empty} \\ XOR_2(HEAD(L),XOR(TAIL(L))) & \text{otherwise} \end{cases}

وهذا يعني أن XORXOR تساوي RECURSE  (λme,L.IF(ISEMPTY(L),0,XOR2(HEAD  L    ,    me(TAIL  L))))  . RECURSE \; \bigl(\lambda me,L. IF(ISEMPTY(L),0,XOR_2(HEAD\;L\;\;,\;\;me(TAIL \; L)))\bigr) \;.

أي أن XORXOR تُحصل بتطبيق المعامل RECURSEعلىالدالةالتيعلىمدخلَيRECURSE على الدالة التي على مدخلَي meو وL$ تُعيد 00 إذا كان ISEMPTY(L)،وتعيدوإلاISEMPTY(L)، وتعيد وإلا XOR_2مطبَّقةعلى مطبَّقة على HEAD(L)وعلى وعلى me(TAIL(L))$.

كان بإمكاننا أيضًا حساب XORباستخدامالعمليةXOR باستخدام العملية REDUCE، وقد تركنا استخراج ذلك تمرينًا للقارئ.

/images/introtcs/lec_07_other_models-12.webp

/images/introtcs/lec_07_other_models-13.webp

حساب دالة في حساب لامدا المعزّز

يَنحصل على تعبير λ\lambda معزّز (enhanced λ expression) بتركيب الكائنات أعلاه مع قواعد التطبيق و_التجريد_. ونتيجة تبسيط تعبير λ\lambda هي تعبير مكافئ له، ومن ثمّ فإن كان لتعبيرين التبسيط نفسه فإنهما متكافئان.

تعريف — حساب دالة بواسطة حساب لامدا

لنفترض أن F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*

نقول إن expexp يحسب FF إذا كان لكل x∈{0,1}∗x\in \{0,1\}^*، exp⟨x0,…,xn−1,⊥⟩≅⟨y0,…,ym−1,⊥⟩exp \langle x_0,\ldots,x_{n-1},\bot \rangle \cong \langle y_0,\ldots, y_{m-1}, \bot \rangle حيث n=∣x∣n=|x| وy=F(x)y=F(x) وm=∣y∣m=|y|، ومفهوم التكافؤ مُعرَّف بحسب تعريف — تبسيط تعبيرات λ.

حساب لامدا المعزّز كامل بحسب تورينج

العمليات الأساسية في حساب لامدا المعزّز تعادل إلى حد كبير لغتَي البرمجة Lisp أو Scheme. وبناءً على ذلك، ربما لا يكون مفاجئًا أن حساب لامدا المعزّز مكافئ لآلات تورينج:

مبرهنة — حساب لامدا وNAND-TM

لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون FF قابلة للحساب في حساب لامدا المعزّز إذا وفقط إذا كانت قابلة للحساب بآلة تورينج.

لإثبات المبرهنة، نحتاج إلى إظهار أن (1) إذا كانت FF قابلة للحساب بتعبير حساب لامدا فإنها قابلة للحساب بآلة تورينج، و__(2)__ إذا كانت FF قابلة للحساب بآلة تورينج فإنها قابلة للحساب بتعبير حساب لامدا معزّز.

إظهار (1) مباشر إلى حد كبير إلى حد كبير. فتطبيق قواعد التبسيط على تعبير λ\lambda يعادل في الأساس "ابحث واستبدل"، وهو ما يمكننا تنفيذه بسهولة في NAND-RAM، أو في Python على سبيل المثال (كلاهما مكافئ في القوة لآلات تورينج). أما إظهار (2) فيعادل في الأساس محاكاة آلة تورينج (أو كتابة مُفسِّر لـ NAND-TM) في لغة برمجة وظيفية مثل LISP أو Scheme. ونقدّم التفاصيل أدناه، لكن كيفية ذلك تمرين جيد في إتقان بعض تقنيات البرمجة الوظيفية المفيدة بذاتها.

برهان

سنكتفي برسم مخطط البرهان. اتجاه "إذا" بسيط. وكما ذُكر أعلاه، فإن تقييم تعبيرات λ\lambda يعادل في الأساس "ابحث واستبدل". كما أن تنفيذ جميع العمليات الأساسية أعلاه في لغة أمرية مثل Python أو C هو تمرين برمجي مباشر إلى حد كبير، وباستخدام الأفكار نفسها يمكننا فعل ذلك في NAND-RAM أيضًا، ثم نحوّله إلى برنامج NAND-TM.

وبالنسبة لاتجاه "فقط إذا" نحتاج إلى محاكاة آلة تورينج باستخدام تعبير $\lambda`. وسنقوم بذلك أولًا بإظهار أنه لكل آلة تورينج MM يوجد تعبير λ\lambda يحسب دالة الخطوة التالية NEXTM:Σ‾∗→Σ‾∗NEXT_M:\overline{\Sigma}^* \rightarrow \overline{\Sigma}^* التي تُسقط تكوينًا لـ MM على التالي (انظر تكوينات آلات تورينج ودالة الخطوة التالية).

تكوين MM هو سلسلة α∈Σ‾∗\alpha \in \overline{\Sigma}^* لمجموعة متناهية Σ‾\overline{\Sigma}. يمكننا ترميز كل رمز σ∈Σ‾\sigma \in \overline{\Sigma} بسلسلة متناهية {0,1}ℓ\{0,1\}^\ell، وبذلك سنرمّز تكوينًا α\alpha في حساب لامدا على هيئة قائمة ⟨α0,α1,…,αm−1,⊥⟩\langle \alpha_0, \alpha_1, \ldots, \alpha_{m-1}, \bot \rangle حيث αi\alpha_i سلسلة طولها ℓ\ell (أي قائمة طولها ℓ\ell من الآصفار والواحدات) ترمّز رمزًا في Σ‾\overline{\Sigma}.

بحسب لمّة، لكل α∈Σ‾∗\alpha \in \overline{\Sigma}^*، تكون NEXTM(α)iNEXT_M(\alpha)_i مساوية لـ r(αi−1,αi,αi+1)r(\alpha_{i-1},\alpha_i,\alpha_{i+1}) لبعض الدالة المتناهية r:Σ‾3→Σ‾r:\overline{\Sigma}^3 \rightarrow \overline{\Sigma}. وباستخدام ترميزنا لـ Σ‾\overline{\Sigma} على هيئة {0,1}ℓ\{0,1\}^\ell، يمكننا أيضًا التفكير في rr على أنها تُسقط {0,1}3ℓ\{0,1\}^{3\ell} على {0,1}ℓ\{0,1\}^\ell. وبحسب تمرين محلول — حساب NAND باستخدام حساب لامدا، يمكننا حساب الدالة NANDNAND، وبالتالي كل دالة متناهية، بما فيها rr، باستخدام حساب لامدا. وباستخدام هذه الفكرة، يمكننا حساب NEXTMNEXT_M باستخدام حساب لامدا على النحو التالي. إذا أعطينا قائمة LL ترمّز التكوين α0⋯αm−1\alpha_0\cdots \alpha_{m-1}، فإننا نعرّف القائمتين LprevL_{prev} وLnextL_{next} اللتين ترمّزان التكوين α\alpha مُزاحًا خطوة واحدة إلى اليمين وإلى اليسار على التوالي. ويُعرَّف التكوين التالي α′\alpha' على أنه αi′=r(Lprev[i],L[i],Lnext[i])\alpha'_i = r(L_{prev}[i],L[i],L_{next}[i]) حيث نترك L′[i]L'[i] تشير إلى العنصر رقم ii في L′L'. ويمكن حساب هذا بالتهاكر (وبالتالي باستخدام معامل RECURSERECURSE في حساب لامدا المعزّز) على النحو التالي:

خوارزمية — NEXTMNEXT_M using the λ calculus

INPUT: List $L = \langle \alpha_0,\alpha_1,\ldots, \alpha_{m-1}, \bot \rangle$ encoding a configuration $\alpha$.
OUTPUT: List $L'$ encoding $NEXT_M(\alpha)$

PROCEDURE{ComputeNext}{$L_{prev},L,L_{next}$}
If $ISEMPTY \; L_{prev}$
return $NIL$
Endif
$a \leftarrow HEAD \; L_{prev}$
If $ISEMPTY\; L$
$b \leftarrow \varnothing$ # Encoding of $\varnothing$ in $\{0,1\}^\ell$
Else
$b \leftarrow HEAD\;L$
Endif
If $ISEMPTY\; L_{next}$
$c \leftarrow \varnothing$
Else
$c \leftarrow HEAD \; L_{next}$
Endif
Return $PAIR \; r(a,b,c) \; ComputeNext(TAIL\; L_{prev}\;,\;TAIL\; L\;,\;TAIL\; L_{next})$
Endprocedure

$L_{prev} \leftarrow PAIR \; \varnothing \; L$ # $L_{prev} = \langle \varnothing , \alpha_0,\ldots, \alpha_{m-1},\bot \rangle$
$L_{next} \leftarrow  TAIL\; L$ # $L_{next} = \langle \alpha_1,\ldots,\alpha_{m-1}, \bot \}$
Return $ComputeNext(L_{prev},L,L_{next})$

وبعد أن نتمكن من حساب NEXTMNEXT_M، يمكننا محاكاة تنفيذ MM على المدخل xx باستخدام التهاكر التالي. نعرّف FINAL(α)FINAL(\alpha) لتكون التكوين النهائي لـ MM عندما يُهيَّأ عند التكوين α\alpha. ويمكن تعريف الدالة FINALFINAL تكراريًا على النحو التالي: FINAL(α)={αα is halting configurationFINAL(NEXTM(α))otherwise  . FINAL(\alpha) = \begin{cases}\alpha & \text{$\alpha$ is halting configuration} \\ FINAL(NEXT_M(\alpha)) & \text{otherwise}\end{cases}\;.

والفحص هل كان تكوين ما هو تكوين توقف (i.e. هل كان تكوينًا تُخرج فيه دالة الانتقال H\mathsf{H}alt) يمكن تنفيذه بسهولة في حساب λ\lambda، ومن ثمّ يمكننا استخدام RECURSERECURSE لحساب FINALFINAL. وإذا تركنا α0\alpha^0 لتكون التكوين الأولي لـ MM على المدخل xx، فيمكننا الحصول على الناتج M(x)M(x) من FINAL(α0)FINAL(\alpha^0)، وبذلك يكتمل البرهان.

من حساب لامدا المعزّز إلى حساب لامدا النقي

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

ويبيّن أنه يوجد بالفعل مجموعة جزئية فعلية من عمليات حساب لامدا المعزّز يمكن استخدامها لتنفيذ الباقي. تلك المجموعة الجزئية هي المجموعة الخالية. أي يمكننا تنفيذ جميع العمليات أعلاه باستخدام الصيغة λ\lambda وحدها، حتى من دون استخدام 00 و11. إنها λ حتى القاع!

توقّف وتأمّل

هذه نقطة جيدة للتوقف والتفكير في كيفية تنفيذك لهذه العمليات بنفسك. فعلى سبيل المثال، ابدأ بالتفكير في كيفية قدرتك على تنفيذ MAPMAP باستخدام REDUCEREDUCE، ثم REDUCEREDUCE باستخدام RECURSERECURSE مع 0,1,IF,PAIR,HEAD,TAIL,NIL,ISEMPTY0,1,IF,PAIR,HEAD,TAIL,NIL,ISEMPTY. ويمكنك أيضًا تنفيذ PAIRPAIR وHEADHEAD وTAILTAIL بالاعتماد على 0,1,IF0,1,IF. والجزء الأصعب هو تنفيذ RECURSERECURSE باستخدام عمليات حساب لامدا النقي فقط.

مبرهنة — حساب لامدا المعزّز مكافئ لحساب لامدا النقي.

توجد تعبيرات λ\lambda تنفذ الدوال 00 و11 وIFIF وPAIRPAIR وHEADHEAD وTAILTAIL وNILNIL وISEMPTYISEMPTY وMAPMAP وREDUCEREDUCE وRECURSERECURSE.

الفكرة الكامنة وراء مبرهنة — حساب لامدا المعزّز مكافئ لحساب لامدا النقي. هي أننا نرمّز 00 و11 نفسهما على هيئة تعبيرات λ\lambda، ونبني الأشياء انطلاقًا من هناك. ويُعرف هذا بـترميز Church (Church encoding)، لأنه نشأ عن Church في محاولته إظهار أن حساب لامدا يمكن أن يكون أساسًا لكل الحوسبة. ولن نكتب البرهان الرسمي الكامل لـ مبرهنة — حساب لامدا المعزّز مكافئ لحساب لامدا النقي.، بل سنعرض مخطط الأفكار المتضمَّنة فيه:

  • نعرّف 00 لتكون الدالة التي على مدخلين x,yx,y تُخرج yy، و11 لتكون الدالة التي على مدخلين x,yx,y تُخرج xx. ونستخدم Curry لتحقيق أثر دوال الإدخال، ومن ثمّ 0=λx.λy.y0 = \lambda x. \lambda y.y و1=λx.λy.x1 = \lambda x.\lambda y.x. (نظام التمثيل هذا هو العُرف الشائع لتمثيل false وtrue، لكن هناك تمثيلات بديلة كثيرة أخرى لـ 00 و11 لكنها كانت ستؤدي الغرض على الأرجح.)

  • التنفيذ أعلاه يجعل الدالة IFIF تافهة: فـ IF(cond,a,b)IF(cond,a,b) هي ببساطة cond  a  bcond \; a\; b لأن 0ab=b0ab = b و1ab=a1ab = a. يمكننا كتابة IF=λx.xIF = \lambda x.x للحصول على IF(cond,a,b)=(((IFcond)a)b)=cond  a  bIF(cond,a,b) = (((IF cond) a) b) = cond \; a \; b.

  • لترميز زوج (x,y)(x,y) سننتج دالة fx,yf_{x,y} يكون فيها xx وyy "في بطنها" وتُحقِّق fx,yg=gxyf_{x,y}g = g x y لكل دالة gg. أي أن PAIR=λx,y.(λg.gxy)PAIR = \lambda x,y. \left(\lambda g. gxy\right). ويمكننا استخراج العنصر الأول من زوج pp بكتابة p1p1 والعنصر الثاني بكتابة p0p0، ومن ثمّ HEAD=λp.p1HEAD = \lambda p. p1 وTAIL=λp.p0TAIL = \lambda p. p0.

  • نعرّف NILNIL لتكون الدالة التي تتجاهل مدخلها وتُخرج دائمًا 11. أي أن NIL=λx.1NIL = \lambda x.1. ودالة ISEMPTYISEMPTY تفحص، إذا أعطينا مدخلًا pp، هل نحصل على 11 عند تطبيق pp على الدالة zero=λx,y.0zero = \lambda x,y.0 التي تتجاهل كل مداخيلها وتُخرج دائمًا 00. لكل زوج صحيح على الشكل p=PAIRxyp = PAIR x y، لدينا pzero=pxy=0p zero = p x y = 0 بينما NILzero=1NIL zero=1. وبشكل رسمي، ISEMPTY=λp.p(λx,y.0)ISEMPTY = \lambda p. p (\lambda x,y.0).

ملاحظة — أعداد Church (اختياري)

لا شيء خاص في القيم البوليانية. فبإمكانك استخدام حيل مشابهة لتنفيذ الأعداد الطبيعية باستخدام حدوس λ\lambda. والطريقة القياسية لذلك هي تمثيل العدد nn بالدالة ITERnITER_n التي على مدخل دالة ff تُخرج الدالة x↦f(f(⋯f(x)))x \mapsto f(f(\cdots f(x))) (بعدد nn مرة). أي أننا نمثّل العدد الطبيعي 11 على هيئة λf.f\lambda f.f، والعدد 22 على هيئة λf.(λx.f(fx))\lambda f.(\lambda x.f(fx))، والعدد 33 على هيئة λf.(λx.f(f(fx)))\lambda f.(\lambda x.f(f(fx)))، وهكذا وهكذا. (لاحظ أن هذه ليست هي نفس التمثيل الذي استخدمناه للـ 11 في السياق البولياني: لا بأس، فقد علمنا أن الشيء نفسه يمكن تمثيله بأكثر من طريقة واحدة.) ويُمثَّل العدد 00 بالدالة التي تُسقط أي دالة ff على دالة الهوية λx.x\lambda x.x. (أي أن 0=λf.(λx.x)0 = \lambda f.(\lambda x.x).)

في هذا التمثيل، يمكننا حساب PLUS(n,m)PLUS(n,m) على هيئة λf.λx.(nf)((mf)x)\lambda f.\lambda x.(n f)((m f)x) وTIMES(n,m)TIMES(n,m) على هيئة λf.n(mf)\lambda f.n(m f). أما الطرح والقسمة فأكثر تعقيدًا، لكن يمكن تحقيقهما بالتهاكر. (استخراج ذلك تمرين ممتاز.)

معالجة القوائم

نصل الآن إلى عقبة أكبر، وهي كيفية تنفيذ MAPMAP وFILTERFILTER وREDUCEREDUCE وRECURSERECURSE في حساب لامدا النقي. ويبيّن أنه يمكننا بناء MAPMAP وFILTERFILTER من REDUCEREDUCE، وREDUCEREDUCE من RECURSERECURSE. فعلى سبيل المثال، MAP(L,f)MAP(L,f) هو نفسه REDUCE(L,g,NIL)REDUCE(L,g,NIL) حيث gg هي العملية التي على مدخلين xx وyy تُخرج PAIR(f(x),y)PAIR(f(x),y). (أترك لك، أيها القارئ، التحقق من ذلك كتمرين مُستحسن!)

ويمكننا تعريف REDUCE(L,f,z)REDUCE(L,f,z) تكراريًا، بأن نضع REDUCE(NIL,f,z)=zREDUCE(NIL,f,z)=z ونشترط أنه إذا أعطينا قائمة غير فارغة LL، والتي نراها زوجًا (head,rest)(head,rest)، فإن REDUCE(L,f,z)=f(head,REDUCE(rest,f,z)))REDUCE(L,f,z) = f(head, REDUCE(rest,f,z))). وعليه، قد نحاول كتابة تعبير λ\lambda تكراري لـ REDUCEREDUCE على النحو التالي REDUCE=λL,f,z.IF(ISEMPTY(L),z,fHEAD(L)REDUCE(TAIL(L),f,z))  . REDUCE = \lambda L,f,z. IF(ISEMPTY(L),z,f HEAD(L) REDUCE(TAIL(L),f,z)) \;.

العقبة الوحيدة في هذا كله هي أن حساب لامدا لا يملك مفهوم التهاكر، ومن ثمّ فإن هذا تعريف غير صالح. لكننا بالطبع يمكننا استخدام معامل RECURSERECURSE لحل هذه المشكلة. سنستبدل الاستدعاء التكراري إلى "REDUCEREDUCE" باستدعاء دالة meme تُعطى كوسيط إضافي، ثم نطبّق RECURSERECURSE عليها. وعليه REDUCE=RECURSE  myREDUCEREDUCE = RECURSE\;myREDUCE حيث myREDUCE=λme,L,f,z.IF(ISEMPTY(L),z,fHEAD(L)me(TAIL(L),f,z))  . myREDUCE = \lambda me,L,f,z. IF(ISEMPTY(L),z,f HEAD(L) me(TAIL(L),f,z)) \;.

مُجمِّع Y، أي التهاكر بلا تهاكر

المعادلة تعني أن تنفيذ MAPMAP وFILTERFILTER وREDUCEREDUCE ينحصر في تنفيذ معامل RECURSERECURSE في حساب لامدا النقي. وهذا هو ما سنفعله الآن.

كيف يمكننا تنفيذ التهاكر بلا تهاكر؟ سنوضّح ذلك بمثال بسيط - دالة XORXOR. وكما هو مبين في تمرين محلول — حساب XOR باستخدام حساب لامدا، يمكننا كتابة دالة XORXOR لقائمة تكراريًا على النحو التالي: XOR(L)={0L is emptyXOR2(HEAD(L),XOR(TAIL(L)))otherwise XOR(L) = \begin{cases} 0 & L \text{ is empty} \\ XOR_2(HEAD(L),XOR(TAIL(L))) & \text{otherwise} \end{cases}

حيث XOR2:{0,1}2→{0,1}XOR_2:\{0,1\}^2 \rightarrow \{0,1\} هي XOR على بتّين. وفي Python سنكتبها على النحو التالي

def xor2(a,b): return 1-b if a else b
def head(L): return L[0]
def tail(L): return L[1:]

def xor(L): return xor2(head(L),xor(tail(L))) if L else 0

print(xor([0,1,1,0,0,1]))
# 1

والآن، كيف يمكننا التخلص من هذا الاستدعاء التكراري؟ الفكرة الرئيسية هي أن بما أن الدوال تستطيع أن تأخذ دوالًا أخرى كمدخل، فإن إعطاء دالة نفسها كمدخل أمر مشروع تمامًا في Python (وفي حساب لامدا بالطبع). إذن فكرتنا هي أن نحاول أن نجد دالة غير تكرارية هي tempxor تأخذ مدخلين: دالة وقائمة، بحيث تُخرج tempxor(tempxor,L) قيمة XOR لـ L!

توقّف وتأمّل

قد ترغب عند هذه النقطة في التوقف ومحاولة تنفيذ هذا بنفسك في Python أو أي لغة برمجة أخرى من اختيارك (ما دامت تسمح بدوال كمدخلات).

كانت محاولتنا الأولى هي ببساطة استخدام فكرة استبدال الاستدعاء التكراري بـ me. لنعرّف هذه الدالة على اسم myxor

def myxor(me,L): return xor2(head(L),me(tail(L))) if L else 0

لنجرّب هذا:

myxor(myxor,[1,0,1])

إذا فعلت ذلك، ستحصل على الشكوى التالية من المُفسِّر: TypeError: myxor() missing 1 required positional argument

المشكلة أن myxor تتوقع مدخلين - دالة وقائمة - بينما في الاستدعاء إلى me لم نقدّم سوى قائمة. وللتصحيح، نعدّل الاستدعاء لنقدّم الدالة نفسها أيضًا:

def tempxor(me,L): return xor2(head(L),me(me,tail(L))) if L else 0

لاحظ الاستدعاء me(me,..) في تعريف tempxor: فإذا أعطينا tempxor دالة me كمدخل، فستستدعي tempxor فعليًا الدالة me وهي نفسها كأول مدخل. وإذا جرّبنا ذلك الآن، سنرى أننا نحصل فعلًا على النتيجة الصحيحة!

tempxor(tempxor,[1,0,1])
# 0
tempxor(tempxor,[1,0,1,1])
# 1

وعليه يمكننا تعريف xor(L) ببساطة على أنها return tempxor(tempxor,L).

النهج أعلاه ليس خاصًا بـ XOR. فإذا أعطينا دالة تكرارية f تأخذ مدخلًا x، يمكننا الحصول على نسخة غير تكرارية على النحو التالي:

  1. أنشئ الدالة myf التي تأخذ زوج مدخلين me وx، وتستبدل الاستدعاءات التكرارية إلى f باستدعاءات إلى me.

  2. أنشئ الدالة tempf التي تحوّل الاستدعاءات في myf من الشكل me(x) إلى استدعاءات من الشكل me(me,x).

  3. تُعرَّف الدالة f(x) على أنها tempf(tempf,x)

وهذه هي الطريقة التي ننفّذ بها معامل RECURSE في Python. فهو سيأخذ دالة myf كما سبق، ويستبدلها بدالة gg بحيث g(x)=myf(g,x)g(x)=myf(g,x) لكل xx.

def RECURSE(myf):
    def tempf(me,x): return myf(lambda y: me(me,y),x)

    return lambda x: tempf(tempf,x)


xor = RECURSE(myxor)

print(xor([0,1,1,0,0,1]))
# 1

print(xor([1,1,0,0,1,1,1,1]))
# 0

من Python إلى حساب λ\lambda. في حساب لامدا، تُكتب الدالة gg ذات المدخلين التي تأخذ زوج مدخلين me,yme,y على هيئة λme.(λy.g)\lambda me.(\lambda y. g). إذن الدالة y↦me(me,y)y \mapsto me(me,y) تُكتب ببساطة me  meme\;me، وبالمثل الدالة x↦tempf(tempf,x)x \mapsto tempf(tempf,x) تُكتب ببساطة tempf  tempftempf\; tempf. (هل ترى لماذا؟) وعليه يمكن كتابة الدالة tempf المعرَّفة أعلاه على هيئة λ me. myf(me me). وهذا يعني أن إذا رمزنا مدخل RECURSE بـ ff، فإن RECURSE  myf=tempf  tempfRECURSE\; myf = tempf \; tempf حيث tempf=λm.f(m  m)tempf = \lambda m. f(m\; m) أو بعبارة أخرى RECURSE=λf.((λm.f(m  m))    (λm.f(m  m))) RECURSE = \lambda f.\bigl( (\lambda m. f(m\; m))\;\; (\lambda m. f(m \;m)) \bigr)

يحتوي الملحق الإلكتروني على تنفيذ لحساب لامدا باستخدام Python. وها هو تنفيذ لدالة XOR التكرارية من ذلك الملحق:[3]

# XOR of two bits
XOR2 = λ(a,b)(IF(a,IF(b,_0,_1),b))

# Recursive XOR with recursive calls replaced by m parameter
myXOR = λ(m,l)(IF(ISEMPTY(l),_0,XOR2(HEAD(l),m(TAIL(l)))))

# Recurse operator (aka Y combinator)
RECURSE = λf((λm(f(m*m)))(λm(f(m*m))))

# XOR function
XOR = RECURSE(myXOR)

#TESTING:

XOR(PAIR(_1,NIL)) # List [1]
# equals 1

XOR(PAIR(_1,PAIR(_0,PAIR(_1,NIL)))) # List [1,0,1]
# equals 0

ملاحظة — مُجمِّع Y

يُعرف المعامل RECURSERECURSE أعلاه على نحو أفضل بأنه مُجمِّع Y (Y combinator).

وهو واحد من عائلة من معاملات النقطة الثابتة التي، إذا أعطيتها تعبير λ\lambda هو FF، تجد نقطة ثابتة ff لـ FF بحيث f=Fff = F f. وإذا فكّرت في الأمر، فإن XORXOR هي النقطة الثابتة لـ myXORmyXOR أعلاه. فـXORXOR هي الدالة بحيث لكل xx، إن أدخلنا XORXOR كأول وسيط في myXORmyXOR فإننا نحصل على XORXOR من جديد، أو بعبارة أخرى XOR=myXOR  XORXOR = myXOR\; XOR. وعليه فإن إيجاد نقطة ثابتة لـ myXORmyXOR هو نفسه تطبيق RECURSERECURSE عليها.

أطروحة Church-Turing (مناقشة)

"[في عام 1934]، كان تشُرْش قد وضع تخمينات، ثم اقترح على نحو قاطع أن الدوال القابلة للتعريف بـ λ هي جميع الدوال القابلة للحساب فعليًا .... وعندما اقترح تشُرْش هذه الأطروحة، جلست أنفي لإثباتها على بُعد ... لكن، بعد أن أدركت بسرعة أن [منهجي قد أخفق]، أصبحت بين ليلة وضحاها من أنصار الأطروحة."، ستيفن كلين، 1979.

"[الأطروحة ليست] تعريفًا فحسب ولا بديهية، بل ... قانون من قوانين الطبيعة."، إميل بوست، 1936.

لقد عرّفنا الدوال بأنها قابلة للحساب إذا أمكن حسابها ببرنامج NAND-TM، وقد رأينا أن التعريف سيبقى كما هو إذا استبدلنا برامج NAND-TM ببرامج Python، وبآلات تورينج، وبحساب لامدا، وبالآليات الخلوية (cellular automata)، وبنماذج حوسبة أخرى كثيرة. أما أطروحة Church-Turing فهي أن هذا هو التعريف الوحيد المعقول للدوال "القابلة للحساب". وخلافًا لـ"أطروحة Church-Turing الفيزيائية الممتدة" (PECTT) التي رأيناها من قبل، فإن أطروحة Church-Turing لا تقدّم تنبؤًا فيزيائيًا ملموسًا يمكن اختباره تجريبيًا، لكنها بلا شك تحفّز تنبؤات مثل PECTT. ويمكن النظر إلى أطروحة Church-Turing على أنها إما أنها دفاع عن اختيار تعريفي، أو أنها تقدّم تنبؤًا ما عن جميع أجهزة الحوسبة المحتملة، أو أنها تقترح قوانينَ طبيعة تقيّد العالم الطبيعي. وبعبارات سكوت آرونسون (Scott Aaronson)، "أيًّا كان ما هي، فلا يمكن إلا النظر إلى أطروحة Church-Turing على أنها ناجحة إلى حد بعيد". لم يطرح أي جهاز حوسبة مرشَّح (بما في ذلك الحواسيب الكمّية، وكذلك نماذج أقل عقلانية بكثير مثل الحواسيب الافتراضية ذات "منحنيات زمنية مغلقة" التي ذكرناها من قبل) تحديًا جادًّا لأطروحة Church-Turing حتى الآن. قد تجعل هذه الأجهزة بعض الحسابات أكثر كفاءة، لكنها لا تغيّر الفرق بين ما هو قابل للحساب وما ليس كذلك. (فـ_أطروحة Church-Turing الممتدة_، التي سنناقشها في أطروحة تورينج-تشرش الموسَّعة (نقاش)، تنص على أن آلات تورينج تلتقط أيضًا الحد لما يمكن حسابه بكفاءة. وكما في نسختها الفيزيائية، تُمثّل الحوسبة الكمّية التحدي الرئيسي لهذه الأطروحة.)

نماذج حوسبة مختلفة

يمكننا تلخيص النماذج التي رأيناها في الجدول التالي:

المشكلات الحاسوبية نوع النموذج أمثلة
دوال متناهية f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m حوسبة غير منتظمة (الخوارزمية تعتمد على طول المدخل) دوائر بوليانية، دوائر NAND، برامج مستقيمة (مثل NAND-CIRC)
دوال بمداخل غير محدودة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^* وصول تسلسلي إلى الذاكرة آلات تورينج، برامج NAND-TM
-- وصول مفهرس / RAM آلات RAM، NAND-RAM، لغات البرمجة الحديثة
-- أخرى حساب لامدا، الآليات الخلوية

الجدول: نماذج مختلفة لحساب الدوال المتناهية والدوال ذات الطول الاعتباري من المدخل.

لاحقًا، في الحساب المقيّد بالمساحة سندرس الحوسبة المحدودة بالذاكرة (memory bounded). ويبيّن أن برامج NAND-TM ذات كمية ثابتة من الذاكرة مكافئة لنموذج الآلات المتناهية (finite automata) (وتُضاف أحيانًا الصفتان "حتمي" أو "غير حتمي"، كما يُعرف هذا النموذج أيضًا بـ_آلات الحالات المنتهية_ (finite state machines))، وهو بدوره يلتقط مفهوم اللغات المنتظمة (regular languages) (التي يمكن وصفها بـتعبيرات نمطية)، وهو مفهوم سنراه في نماذج الحوسبة المقيَّدة.

خلاصة

  • ورغم أننا عرّفنا الدوال القابلة للحساب باستخدام آلات تورينج، لكنا نستطيع أن نفعل ذلك بنماذج أخرى كثيرة، بما فيها برامج NAND-TM، وكذلك آلات RAM، وNAND-RAM، وحساب لامدا، والآليات الخلوية، ونماذج أخرى كثيرة.
  • تبيّن أن النماذج البسيطة جدًا "كاملة بحسب تورينج" بالمعنى أنها تستطيع محاكاة حوسبة ذات تعقيد اعتباطي.

تمارين

تمرين — برهان بديل لتكافؤ TM/RAM

لنفترض أن SEARCH:{0,1}∗→{0,1}∗SEARCH:\{0,1\}^* \rightarrow \{0,1\}^* هي الدالة التالية. المدخل هو زوج (L,k)(L,k) حيث k∈{0,1}∗k\in \{0,1\}^*، وLL هو ترميز لقائمة من أزواج مفتاح-قيمة (k0,v1),…,(km−1,vm−1)(k_0,v_1),\ldots,(k_{m-1},v_{m-1}) حيث k0,…,km−1k_0,\ldots,k_{m-1} وv0,…,vm−1v_0,\ldots,v_{m-1} سلاسل ثنائية. المخرج هو viv_i لأصغر ii بحيث ki=kk_i=k، إذا وُجد مثل هذا ii، وإلا فالسلسلة الفارغة.

  1. أثبِت أن SEARCHSEARCH قابلة للحساب بآلة تورينج.

  2. لنفترض أن UPDATE(L,k,v)UPDATE(L,k,v) هي الدالة التي مدخلها قائمة LL من الأزواج، ومخرجها هي القائمة L′L' المحصَّلة بإضافة الزوج (k,v)(k,v) في بداية LL. أثبِت أن UPDATEUPDATE قابلة للحساب بآلة تورينج.

  3. لنفترض أننا نرمّز تكوين برنامج NAND-RAM بقائمة LL من أزواج مفتاح/قيمة، حيث المفتاح إما اسم متغير عددي foo أو من الشكل Bar[<num>] وبعض العدد <num>، ويحتوي على جميع قيم المتغيرات غير الصفرية. ولنفترض أن NEXT(L)NEXT(L) هي الدالة التي تُسقط تكوين برنامج NAND-RAM عند خطوة ما على التكوين في الخطوة التالية. أثبِت أن NEXTNEXT قابلة للحساب بآلة تورينج (لست مضطرًا لتنفيذ كل واحدة من العمليات الحسابية: يكفي تنفيذ الجمع والضرب).

  4. أثبِت أن لكل F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^* قابلة للحساب ببرنامج NAND-RAM، تكون FF قابلة للحساب بآلة تورينج.

تمرين — البحث في NAND-TM

يُظهر هذا التمرين جزءًا من برهان أن NAND-TM يستطيع محاكاة NAND-RAM. أنتج شيفرة برنامج NAND-TM يحسب الدالة LOOKUP:{0,1}∗→{0,1}LOOKUP:\{0,1\}^* \rightarrow \{0,1\} المعرَّفة على النحو التالي. على مدخل pf(i)xpf(i)x، حيث pf(i)pf(i) تشير إلى ترميز خالٍ من البادئة للعدد الصحيح ii، تكون LOOKUP(pf(i)x)=xiLOOKUP(pf(i)x)=x_i إذا كان i<∣x∣i<|x| وLOOKUP(pf(i)x)=0LOOKUP(pf(i)x)=0 وإلا. (لا يهمّنا ما تُخرجه LOOKUP على المداخل التي ليست من هذا الشكل.) ويمكنك اختيار أي ترميز خالٍ من البادئة تختاره، ويمكنك أيضًا استخدام لغة برمجة مفضّلة لديك لإنتاج هذه الشيفرة.

تمرين — الأزواج

لنفترض أن embed:N2→Nembed:\N^2 \rightarrow \N هي الدالة المعرَّفة على أنها embed(x0,x1)=12(x0+x1)(x0+x1+1)+x1embed(x_0,x_1)= \tfrac{1}{2}(x_0+x_1)(x_0+x_1+1) + x_1. \

  1. أثبِت أن embed(x0,x1)embed(x^0,x^1) هو بالفعل عدد طبيعي لكل x0,x1∈Nx^0,x^1 \in \N. \

  2. أثبِت أن embedembed واحد إلى واحد \

  3. أنشئ برنامج NAND-TM هو PP بحيث لكل x0,x1∈Nx^0,x^1 \in \N، يكون P(pf(x0)pf(x1))=pf(embed(x0,x1))P(pf(x^0)pf(x^1))=pf(embed(x^0,x^1))، حيث pfpf هي دالة الترميز الخالي من البادئة المعرَّفة أعلاه. ويمكنك استخدام السكر النحوي للحلقات الداخلية، والشروط، وزيادة/نقصان العدّاد. \

  4. أنشئ برامج NAND-TM هي P0,P1P_0,P_1 بحيث لكل x0,x1∈Nx^0,x^1 \in \N وi∈Ni \in N، يكون Pi(pf(embed(x0,x1)))=pf(xi)P_i(pf(embed(x^0,x^1)))=pf(x^i). ويمكنك استخدام السكر النحوي للحلقات الداخلية، والشروط، وزيادة/نقصان العدّاد.

تمرين — أقصر مسار

لنفترض أن SHORTPATH:{0,1}∗→{0,1}∗SHORTPATH:\{0,1\}^* \rightarrow \{0,1\}^* هي الدالة التي على مدخل سلسلة ترمّز ثلاثيًا (G,u,v)(G,u,v) تُخرج سلسلة ترمّز ∞\infty إذا كان uu وvv غير متصلين في GG، أو سلسلة ترمّز الطول kk لأقصر مسار من uu إلى vv. أثبِت أن SHORTPATHSHORTPATH قابلة للحساب بآلة تورينج. انظر الحاشية للتلميح.[4]

تمرين — أطول مسار

لنفترض أن LONGPATH:{0,1}∗→{0,1}∗LONGPATH:\{0,1\}^* \rightarrow \{0,1\}^* هي الدالة التي على مدخل سلسلة ترمّز ثلاثيًا (G,u,v)(G,u,v) تُخرج سلسلة ترمّز ∞\infty إذا كان uu وvv غير متصلين في GG، أو سلسلة ترمّز الطول kk لـ_أطول مسار بسيط_ (longest simple path) من uu إلى vv. أثبِت أن LONGPATHLONGPATH قابلة للحساب بآلة تورينج. انظر الحاشية للتلميح.[5]

تمرين — تعبير λ\lambda لأقصر مسار

لنفترض أن SHORTPATHSHORTPATH كما في تمرين — أقصر مسار. أثبِت أن يوجد تعبير λ\lambda يحسب SHORTPATHSHORTPATH. ويمكنك استخدام تمرين — أقصر مسار

تمرين — دالة الخطوة التالية محلية

أثبِت لمّة واستخدمه لإكمال برهان مبرهنة — الآليات أحادية البعد كاملة بحسب تورينج.

تمرين — حساب لامدا يحتاج إلى ثلاثة متغيرات على الأكثر

أثبِت أن لكل تعبير λ\lambda هو ee ولا يحتوي متغيرات حرة يوجد تعبير λ\lambda مكافئ هو ff لا يستخدم إلا المتغيرات xx وyy وzz.[6]

تمرين — مثال على ترتيب التقييم في حساب لامدا

  1. لنفترض أن e=λx.7((λx.xx)(λx.xx))e = \lambda x.7 \left( (\lambda x.xx) (\lambda x.xx) \right). أثبِت أن عملية تبسيط ee تنتهي إلى عدد متناهٍ إذا استخدمنا ترتيب التقييم "الاستدعاء بالاسم"، بينما لا تنتهي أبدًا إذا استخدمنا ترتيب "الاستدعاء بالقيمة".

  2. (إضافي، متحدٍّ) لنفترض أن ee أي تعبير λ\lambda. أثبِت أن إذا انتهت عملية التبسيط إلى عدد متناهٍ إذا استخدمنا ترتيب "الاستدعاء بالقيمة" فإنها تنتهي أيضًا إلى عدد متناهٍ إذا استخدمنا ترتيب "الاستدعاء بالاسم". انظر الحاشية للتلميح.[7]

تمرين — دالة zip

أعطِ تعبيرًا في حساب لامدا المعزّز لحساب الدالة zip التي على مدخل زوج من القوائم II وLL لهما الطول نفسه nn، تُخرج قائمة من nn زوجًا هي MM بحيث العنصر رقم jj من MM (الذي نرمز له بـ MjM_j) هو الزوج (Ij,Lj)(I_j,L_j). وعليه فإن zip "تضمّ معًا" قائمتي العناصر هاتين في قائمة واحدة من الأزواج.[8]

تمرين — دالة الخطوة التالية بلا RECURSERECURSE

لنفترض أن MM آلة تورينج. أعطِ تعبيرًا في حساب لامدا المعزّز لحساب دالة الخطوة التالية NEXTMNEXT_M الخاصة بـ MM (كما في برهان مبرهنة — حساب لامدا وNAND-TM) دون استخدام RECURSERECURSE. انظر الحاشية للتلميح.[9]

تمرين — مترجم من حساب لامدا إلى NAND-TM (متحدٍّ)

أعطِ برنامجًا بلغة البرمجة التي تختارها يأخذ كمدخل تعبير λ\lambda هو ee ويُخرج برنامج NAND-TM هو PP يحسب الدالة نفسها التي يحسبها ee. وللحصول على جزء من الدرجة يمكنك استخدام GOTO وكل السكر النحوي الخاص بـ NAND-CIRC في برنامج مُخرَجك. ويمكنك استخدام أي ترميز لتعبيرات λ\lambda كسلسلة ثنائية يناسبك. انظر الحاشية للتلميح.[10]

تمرين — اثنان على الأقل في حساب λ\lambda

لنفترض أن 1=λx,y.x1 = \lambda x,y.x و0=λx,y.y0 = \lambda x,y.y كما قبل. عرّف ALT=λa,b,c.(a(b1(c10))(bc0)) ALT = \lambda a,b,c.(a (b 1 (c 1 0)) (b c 0))

أثبِت أن ALTALT هو تعبير λ\lambda يحسب دالة "اثنين على الأقل" (at least two). أي أنه لكل a,b,c∈{0,1}a,b,c \in \{0,1\} (كما رُمِّزت أعلاه) يكون ALTabc=1ALT a b c = 1 إذا وفقط إذا كان عنصران على الأقل من {a,b,c}\{a,b,c\} يساويان 11.

تمرين — محلية دالة الخطوة التالية

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

  • برنامج STRINGS هو QQ له متغير سلسلة واحد str هو في آنٍ واحد مدخل QQ ومخرجها. ويمرّ البرنامج بلا حلقات وبلا متغيرات أخرى، بل يتألف من سلسلة عمليات بحث شرطية واستبدال تُعدِّل str.

  • عمليات برنامج STRINGS هي:

    • REPLACE(pattern1,pattern2) حيث pattern1 وpattern2 سلسلتان ثابتتان. يستبدل هذا أول ظهور لـ pattern1 في str بـ pattern2
    • if search(pattern) { code } ينفّذ code إذا كان pattern سلسلة فرعية من str. ويمكن أن يحتوي code بدوره على if متداخلة. (ويمكن أيضًا إضافة else { ... } للتنفيذ إذا لم يكن pattern سلسلة فرعية من الشرط.)
    • القيمة المُعادة هي str
  • يحسب برنامج STRING هو QQ دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^* إذا كان لكل x∈{0,1}∗x\in \{0,1\}^*، إذا هيّأنا str على xx ثم نفّذنا سلسلة التعليمات في QQ، فإن str في نهاية التنفيذ يساوي F(x)F(x).

على سبيل المثال، فيما يلي برنامج STRINGS يحسب الدالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^* بحيث لكل x∈{0,1}∗x\in \{0,1\}^*، إذا كان xx يحتوي على سلسلة فرعية من الشكل y=11ab11y=11ab11 حيث a,b∈{0,1}a,b \in \{0,1\}، فإن F(x)=x′F(x)=x' حيث x′x' يُحصل عليه باستبدال أول ظهور لـ yy في xx بـ 00.

if search('110011') {
    replace('110011','00')
} else if search('110111') {
    replace('110111','00')
} else if search('111011') {
    replace('111011','00')
} else if search('111111') {
    replace('1111111','00')
}

أثبِت أن لكل برنامج آلة تورينج هو MM، يوجد برنامج STRINGS هو QQ يحسب الدالة NEXTMNEXT_M التي تُسقط كل سلسلة ترمّز تكوينًا صالحًا لـ MM على السلسلة التي ترمّز تكوين الخطوة التالية من حساب MM. (لا يهمّنا ما ستفعله الدالة على السلاسل التي لا ترمّز تكوينًا صالحًا.) ولست مضطرًا إلى كتابة برنامج STRINGS كاملًا، لكن عليك تقديم حجّة مقنعة بأن مثل هذا البرنامج موجود.

ملاحظات مرجعية

الفصل 7 في كتاب مور وميرتنز الرائع [@MooreMertens11] يحتوي على عرض ممتاز لكثير من هذه المادة. .

يمكن لنموذج RAM أن يكون مفيدًا جدًا في دراسة التعقيد الملموس للخوارزميات العملية. وقد بُدئ دراسته النظرية في [@cook1973time]. غير أن مجموعة العمليات المسموح بها تحديدًا في نموذج RAM وتكاليفها تختلف بين الكتب والسياقات. ينبغي توخّي الحذر في وضع مثل هذه التعريفات، خاصة إذا كان حجم الكلمة يكبر، كما أظهر شامير [@shamir1979] بالفعل. يحتوي الفصل 3 في كتاب سافج [@Savage1998models] على وصف أكثر رسمية لآلات RAM، انظر أيضًا الورقة [@hagerup1998]. وقد بُدئت دراسة خوارزميات RAM المستقلة عن حجم المدخل (المعروفة بنموذج "RAM ثنائي القطع" (transdichotomous RAM model)) في [@fredman1993]

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

صُف حساب لامدا في [@church1941]. وكتاب بيرس [@pierce2002types] هو كتاب مرجعي معياري، انظر أيضًا [@barendregt1984]. وتُسمّى "تقنية Curry" باسم المنطقي Haskell Curry (ولغة البرمجة Haskell مسمّاة أيضًا باسم هاسكل كوري). وقد نسب كوري نفسه هذا المفهوم إلى Moses Schönfinkel، وإن كان مصطلح "Schönfinkeling" لم ينتشر لسبب ما.

بخلاف معظم لغات البرمجة، لا يملك حساب لامدا النقي مفهوم الأنواع (types). فكل كائن في حساب لامدا يمكن أيضًا التفكير فيه على أنه تعبير λ\lambda، ومن ثمّ على أنه دالة تأخذ مدخلًا واحدًا وتُخرج ناتجًا واحدًا. وجميع الدوال تأخذ مدخلًا واحدًا وتُخرج ناتجًا واحدًا، وإن أعطيتَ دالةً مدخلًا من شكل لم تتوقعه فإنها تظل تُقيّم تعبير λ\lambda عبر "ابحث واستبدل"، باستبدال جميع مواضع معاملها بنسخ من تعبير المدخل الذي أعطيته لها. وتُعدّ الصيغ المُنمَّطة من حساب لامدا موضوع بحث مكثّف، وهي ترتبط ارتباطًا وثيقًا بأنظمة الأنواع الخاصة بلغات البرمجة وبأنظمة البراهين القابلة للتحقق حاسوبيًا، انظر [@pierce2002types]. وبعض الصيغ المنمَّطة من حساب لامدا لا تملك حلقات لا نهائية، مما يجعلها مفيدة جدًا كطرق لتفعيل التحليل الساكن للبرامج وكذلك للبراهين القابلة للتحقق حاسوبيًا. وسنعود إلى هذه النقطة في نماذج الحوسبة المقيَّدة والبراهين والخوارزميات.

اقترح تاو طريقة تُظهر الاكتمال بحسب تورينج ديناميكا الموائع (حاسوبًا "مائيًا")، وذلك من أجل حسم مسألة سلوك معادلات نافير-ستوكس، انظر هذا المقال الشائع.


  1. بعض لغات البرمجة لها حدود ثابتة (وإن كانت ضخمة جدًا) على حجم الذاكرة التي يمكنها الوصول إليها، وهي حدود تمنعها رسميًا من أن تصلح لحساب دوال لا نهائية، وبالتالي من محاكاة آلات تورينج. نحن نتجاهل مثل هذه القضايا في هذه المناقشة ونفترض إمكانية الوصول إلى جهاز تخزين بلا حد أعلى ثابت لسعته. ↩︎

  2. في Lisp، تُسمّى الدوال PAIRPAIR وHEADHEAD وTAILTAIL تقليديًا cons وcar وcdr. ↩︎

  3. نظرًا لمسائل بعينها في صياغة Python، فإننا في هذا التنفيذ نستخدم f * g لتطبيق f على g بدلًا من fg، ونستخدم λx(exp) بدلًا من λx.exp للتجريد. ونستخدم أيضًا _0 و_1 لحدوس λ\lambda الخاصة بـ 00 و11 حتى لا تختلط مع ثوابت Python. ↩︎

  4. لست مضطرًا إلى إعطاء وصف كامل لآلة تورينج: استخدم نموذج "نأكل الكعكة ونحتفظ بها أيضًا" لإظهار وجود مثل هذه الآلة بالمحاجّة انطلاقًا من نماذج أكفأ ومكافئة. ↩︎

  5. ينطبق نفس التلميح في تمرين — أطول مسار. لاحظ أنك لإظهار أن LONGPATHLONGPATH قابلة للحساب لست مضطرًا إلى إعطاء خوارزمية كفؤة. ↩︎

  6. تلميح: يمكنك تقليل عدد المتغيرات التي تأخذها دالة بأن "تجمعها في أزواج". أي، عرّف تعبير λ\lambda هو PAIRPAIR بحيث لكل x,yx,y تكون PAIRxyPAIR xy هي دالة ff بحيث f0=xf0=x وf1=yf1=y. ثم استخدم PAIRPAIR لتقليل عدد المتغيرات المستخدمة تكراريًا. ↩︎

  7. استخدم الاستدلال البنيوي على التعبير ee. ↩︎

  8. اسم zip هو اسم شائع لهذه العملية، على سبيل المثال في Python. ولا ينبغي خلطه بصيغة ضغط الملفات zip. ↩︎

  9. استخدم MAPMAP وREDUCEREDUCE (وربما FILTERFILTER). وستجد أن دالة zip في تمرين — دالة zip مفيدة أيضًا. ↩︎

  10. حاول أن تُنشئ إجراءً بحيث إذا احتوى المصفوفة Left على ترميز تعبير λ\lambda هو λx.e\lambda x.e واحتوت المصفوفة Right على ترميز تعبير λ\lambda آخر هو e′e'، فإن المصفوفة Result ستحتوي على e[x→e′]e[x \rightarrow e']. ↩︎