مقدمة في علوم الحاسوب النظرية › الحلقات واللانهاية 9 / 26 السابق التالي

الحلقات وما لا نهاية { #chaploops }

الحلقات وما لا نهاية

  • تعلّم نموذج آلات تورينج (Turing machines)، التي تستطيع حساب دوال بمداخل ذات أطوال اعتباطية.
  • اطّلع على وصف بلغة برمجة لآلات تورينج، باستخدام برامج NAND-TM، التي تضيف الحلقات (loops) و_المصفوفات_ (arrays) إلى NAND-CIRC.
  • اطّلع على سكر نحوي (syntactic sugar) أساسي، وعلى تكافؤ صيغ آلات تورينج وبرامج NAND-TM.

"لكن حدود الحساب قد تجاوزت في اللحظة التي خطرت فيها فكرة استعمال البطاقات [المثقوبة]؛ والآلة التحليلية لا تشترك في الأرضية المشتركة مع «آلات الحساب» المجرّدة فحسب... فبإمكان الآلية أن تجمع بين الرموز العامّة، في تعاقببات من تنوّع ومدى غير محدودين، ينشأ رابط يربط بين عمليات المادة والعمليات العقلية المجرّدة في أكثر فروع الرياضيات تجريدًا."، أوغستا آدا، كاونتيسة لوفلايس، 1843

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

  • مجموعة التعليمات المتناهية التي يتعيّن تنفيذها.

  • بعض «المتغيّرات المحلية» (local variables) أو الحالة المتناهية المستخدَمة أثناء التنفيذ.

  • ذاكرة عمل قد تكون غير محدودة الحجم، لتخزين المدخل وأي قيم أخرى قد نحتاج إليها لاحقًا.

  • ورغم أن الذاكرة غير محدودة، إلا أننا في كل خطوة على حدة نستطيع القراءة والكتابة في جزء متناهٍ منها فقط، ونحتاج إلى وسيلة لـ_عنونة_ (address) الأجزاء التي نريد القراءة منها والكتابة إليها.

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

/images/introtcs/lec_06_loops-1.webp

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

  • آلات تورينج (Turing machines)، التي اخترعها آلان تورينج (Alan Turing) سنة 1936، هي أجهزة مجرّدة افتراضية تقدّم أوصافًا متناهية لخوارزميات قادرة على التعامل مع مداخل بطول اعتباطي.

  • توسّع لغة البرمجة NAND-TM (NAND-TM Programming language) لغة NAND-CIRC بمفهوم الحلقات (loops) و_المصفوفات_ (arrays) للحصول على برامج متناهية تستطيع حساب دالة بمدخل بطول اعتباطي.

تبيّن أن هذين النموذجين متكافئان (equivalent). وفي الواقع، إنهما متكافئان مع كثير من نماذج الحوسبة الأخرى، بما فيها لغات البرمجة مثل C وLisp وPython وJavaScript وغيرها. وسنناقش هذه الفكرة، المعروفة بـ_تكافؤ تورينج_ (Turing equivalence) أو اكتمال تورينج (Turing completeness)، في نماذج حوسبة مكافئة. انظر الشكل التوضيحي لنظرة عامّة على النماذج المقدَّمة في هذا الفصل، ونماذج حوسبة مكافئة.

نظرة عامة على نماذجنا للحوسبة المحدودة وغير المحدودة. في الفصول السابقة درسنا حساب الدوال المحدودة، وهي دوالٌ على النمط  لبعض  مُثبّتين، ونمذجنا حساب هذه الدوال باستعمال الدوائر أو البرامج ذات الخطوط المستقيمة. وفي هذا الفصل ندرس حساب الدوال غير المحدودة على النمط  أو . ونُنمذج حساب هذه الدوال باستعمال آلات تورينج (أو ما يكافئها) برامج NAND-TM، التي تُضيف مفهوم الحلقات إلى لغة برمجة NAND-CIRC. وفي chapequivalentmodels سنُظهر أنّ هذه النماذج تكافئ نماذجَ كثيرةً أخرى، منها آلات RAM، وحساب λ، ولغات البرمجة الشائعة كلّها مثل C وPython وJava وJavaScript وغيرها.

آلات تورينج

"يُنجَز الحساب عادةً بكتابة رموز معيّنة على ورق. يمكننا أن نفترض أن هذا الورق مقسَّم إلى مربّعات، كما في كتاب حساب الأطفال... وسلوك الحاسوب البشريالبشري في أي لحظة تحدّده الرموز التي يراها، و«حالته الذهنية» في تلك اللحظة... يمكننا أن نفترض أن في أي عملية بسيطة لا يتغيّر أكثر من رمز واحد."،
"نقارن بين إنسانٍ يجري عملية حسابية ... وبين آلة لا تقبل سوى عدد متناهٍ من الحالات... وتُزوَّد الآلة بـ«شريط» (المقابل للورق) ... مقسَّم إلى مقاطع (تُسمّى «مربّعات») كل منها قادر على حمل «رمز» "، آلان تورينج (Alan Turing)، 1936

"ما الفرق بين آلة تورينج والحاسوب الحديثة؟ إنه نفس الفرق بين صعود هيلاري لقمة إيفرست وبين إنشاء فندق هيلتون على قمّتها."، آلان بيرليس (Alan Perlis)، 1982.

/images/introtcs/lec_06_loops-2.webp

«جدّ» كل نماذج الحوسبة هو آلة تورينج. عُرِّفت آلات تورينج سنة 1936 على يد آلان تورينج، في محاولة لالتقاط رسميّ لكل الدوال التي يمكن أن يحسبها «حاسوبون» (computers) من البشر (انظر الشكل التوضيحي) ويتبعون مجموعة قواعد محدَّدة تمامًا، مثل الخوارزميات القياسية للجمع أو الضرب.

حتّى مجيء الحاسوبات الإلكترونية، كان استخدام كلمة «حاسوب» (computer) لوصف شخصٍ يُجري الحسابات. وكان أغلب هؤلاء «الحواسيب البشرية» نساء، وكانوا عنصرًا بالغ الأهمية في إنجازاتٍ كثيرة، من بينها رسم خرائط النجوم، وكسر تشفير إنغما، ومهمّة ناسا الفضائية؛ انظر أيضًا الملاحظات الببليوغرافية. الصورة من مجموعة شركة الصور الوطنية؛ انظر أيضًا [@sobel2017the].

تصوّر تورينج هذا الشخص بصفته يملك ما يشاء من «ورق مسودّة» (scratch paper). وللبساطة، يمكننا أن نتعامل مع هذه الورقة المسودّة على أنها قطعة ورق مربّعات ببعد واحد (أو شريط (tape) كما يُسمّى عادةً). والورق مقسّم إلى «خلايا»، حيث يمكن لكل «خلية» أن تحمل رمزًا واحدًا (مثل رقم أو حرف، وعمومًا عنصرًا من أبجدية (alphabet) متناهية). في أي لحظة من الزمن، يمكن للشخص أن يقرأ من خليّة واحدة من الورق ويكتب فيها. واستنادًا إلى محتوى هذه الخلية، يمكن للشخص أن يحدّث حالته الذهنية المتناهية، و/أو ينتقل إلى الخلية المجاورة مباشرةً على اليمين أو اليسار.

جدارية لآلة تورينج مُشغَّلة بالبخار، رسمها طلاب الدراسات العليا في قسم هندسة الحاسبات والعلوم بجامعة واشنطن في الليلة السابقة لامتحانات التأهيل الربيعي، سنة 1987. الصورة من https://www.cs.washington.edu/building/art/SPTM.

نمذج تورينج عملية حسابية كهذه بـ«آلة» تحافظ على إحدى kk حالات. في كل لحظة من الزمن، تقرأ الآلة من «شريط عملها» رمزًا واحدًا من أبجدية متناهية Σ\Sigma، وتستخدمه لتحديث حالتها، وللكتابة على الشريط، وللانتقال ربما إلى خليّة مجاورة (انظر الشكل التوضيحي). ولحساب دالة FF باستخدام هذه الآلة، نهيّئ الشريط بالمدخل x∈{0,1}∗x\in \{0,1\}^*، ويكون هدفنا ضمان أن يحتوي الشريط على القيمة F(x)F(x) في نهاية الحساب. وبالتحديد، فإن حساب آلة تورينج MM ذات kk حالة والأبجدية Σ\Sigma على المدخل x∈{0,1}∗x\in \{0,1\}^* يسير كما يلي:

  • في البداية تكون الآلة في الحالة 00 (المعروفة بـ«حالة البدء») ويُهيَّأ الشريط إلى ▹,x0,…,xn−1,∅,∅,…\triangleright,x_0,\ldots,x_{n-1},\varnothing,\varnothing,\ldots. ونستخدم الرمز ▹\triangleright للدلالة على بداية الشريط، والرمز ∅\varnothing للدلالة على خليّة فارغة. وسنفترض دائمًا أن الأبجدية Σ\Sigma مجموعة فوقية (ربما صارمة تمامًا) لـ {▹,∅,0,1}\{ \triangleright, \varnothing , 0 , 1 \}.

  • الموضع ii الذي تشير إليه الآلة يُضبط على 00.

  • في كل خطوة، تقرأ الآلة الرمز σ=T[i]\sigma = T[i] الموجود في الموضع ithi^{th} من الشريط. واستنادًا إلى هذا الرمز وإلى حالتها ss، تقرّر الآلة:

    • أي رمز σ′\sigma' تكتبه على الشريط \
    • هل تتحرك L إلى اليسار (أي، i←i−1i \leftarrow i-1)، أم R إلى اليمين (أي، i←i+1i \leftarrow i+1)، أم تب S مكانها، أم Hوقف الحساب.
    • ما هي الحالة الجديدة s∈[k]s \in [k] التي ستكون عليها
  • تُسمّى مجموعة القواعد التي تتبعها آلة تورينج دالة الانتقال (transition function) لها.

  • عندما تتوقّف الآلة، فإن خرجها هو السلسلة الثنائية التي تُقرأ من الشريط من بدايته حتى أول موضع يحتوي فيه على الرمز ∅\varnothing، ثم إخراج كل رموز 00 و11 بالترتيب، مع إسقاط الرمز ▹\triangleright الأولي إن وُجد، وكذلك الرمز $\varnothing) الأخير.

مكوّنات آلة تورينج. لاحِث كيف تقابل المكوّنات العامة للخوارزميات كما وُصفت في algcomponentfig.

مثال موسَّع: آلة تورينج للكلمات المتناظرة

لتكن PALPAL (أي palindromes، الكلمات المتناظرة) الدالة التي على المدخل x∈{0,1}∗x\in \{0,1\}^* تُخرج 11 إذا وفقط إذا كان xx متناظرًا (palindrome) (أي ذا طول زوجي)، بالمعنى أن x=w0⋯wn−1wn−1wn−2⋯w0x = w_0 \cdots w_{n-1}w_{n-1}w_{n-2}\cdots w_0 لبعض n∈Nn\in \N وw∈{0,1}nw\in \{0,1\}^n.

نعرض الآن آلة تورينج MM تحسب PALPAL. ولتحديد MM نحتاج إلى تحديد (i) أبجدية شريط MM وهي Σ\Sigma، ويجب أن تحتوي على الأقل على الرموز 00،11، ▹\triangleright و∅\varnothing، و__(ii)__ دالة انتقال MM، التي تحدّد الإجراء الذي تتّخذه MM عندما تقرأ رمزًا معيّنًا وهي في حالة معيّنة.

في حالتنا، ستستخدم MM الأبجدية {0,1,▹,∅,×}\{ 0,1,\triangleright, \varnothing, \times \} وستكون لها k=11k=11 حالة. ورغم أن الحالات أرقام بين 00 وk−1k-1 فحسب، فسنعطيها التسميات التالية لتسهيل القراءة:

---
caption: ''
alignment: ''
table-width: ''
id: ''
---
State, Label
0, `START`
1,`RIGHT_0`
2,`RIGHT_1`
3,`LOOK_FOR_0`
4,`LOOK_FOR_1`
5,`RETURN`
6,`OUTPUT_0`
7,`OUTPUT_1`
8,`0_AND_BLANK`
9,`1_AND_BLANK`
10,`BLANK_AND_STOP`

نصف تشغيل آلة تورينج MM بالكلمات:

  • تبدأ MM في الحالة START وتتجه إلى اليمين، باحثة عن أول رمز هو 00 أو 11. فإذا وجدت ∅\varnothing قبل أن تصطدم بمثل هذا الرمز، فإنها تنتقل إلى الحالة OUTPUT_1 الموصوفة أدناه.

  • وبمجرد أن تجد MM رمزًا b∈{0,1}b \in \{0,1\} من هذا النوع، فإنها تحذف bb من الشريط بكتابة الرمز ×\times، وتدخل إما وضع RIGHT_bb وتبدأ بالحركة يمينًا حتى تصطدم بأول رمز ∅\varnothing أو ×\times.

  • وبمجرد أن تجد MM هذا الرمز، فإنها تدخل الحالة LOOK_FOR_0 أو LOOK_FOR_1 بحسب ما إذا كانت في الحالة RIGHT_0 أو RIGHT_1، وتتحرك خطوة واحدة إلى اليسار.

  • في الحالة LOOK_FOR_bb، تتحقق MM ممّا إذا كانت القيمة على الشريط هي bb. فإن كانت كذلك، فإن MM تحذفها بتغيير قيمتها إلى ×\times، وتنتقل إلى الحالة RETURN. وإلا فإنها تنتقل إلى الحالة OUTPUT_0.

  • تعني الحالة RETURN أن MM تعود إلى البداية. وبتحديد، تتحرك MM إلى اليسار حتى تصطدم بأول رمز ليس 00 ولا 11، وعندئذ تغيّر حالتها إلى START.

  • تعني الحالات OUTPUT_bb أن MM ستُخرج في النهاية القيمة bb. وفي كلتا الحالتين OUTPUT_0 وOUTPUT_1 تتحرك MM إلى اليسار حتى تصطدم بـ▹\triangleright. وبمجرد حدوث ذلك، تتخذ خطوة إلى اليمين، وتنتقل إلى الحالتين 1_AND_BLANK أو 0_AND_BLANK على التوالي. وفي هاتين الحالتين تكتب MM القيمة المناظرة، وتتحرك إلى اليمين، وتنتقل إلى الحالة BLANK_AND_STOP، حيث تكتب ∅\varnothing على الشريط ثم تتوقّف.

يمكن تحويل الوصف أعلاه إلى جدول يصف لكل واحدة من توافيق 11⋅511\cdot 5 بين الحالة والرمز ما ستفعله آلة تورينج عندما تكون في تلك الحالة وتقرأ ذلك الرمز. ويُعرف هذا الجدول بـ_دالة الانتقال_ (transition function) لآلة تورينج.

آلات تورينج: تعريف رسمي

/images/introtcs/lec_06_loops-5.webp

التعريف الرسمي لآلات تورينج هو كما يلي:

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

آلة تورينج MM (بشريط واحد) ذات kk حالة وأبجدية Σ⊇{0,1,▹,∅}\Sigma \supseteq \{0,1, \triangleright, \varnothing \} تُمثَّل بـ_دالة انتقال_ (transition function) δM:[k]×Σ→[k]×Σ×{L,R,S,H}\delta_M:[k]\times \Sigma \rightarrow [k] \times \Sigma \times \{\mathsf{L},\mathsf{R}, \mathsf{S}, \mathsf{H} \}.

لكل x∈{0,1}∗x\in \{0,1\}^*، يكون الخرج (output) الخاص بـMM على المدخل xx، ويُرمز له بـM(x)M(x)، هو نتيجة العملية التالية:

  • نهيّئ TT لتكون المتسلسلة ▹,x0,x1,…,xn−1,∅,∅,…\triangleright,x_0,x_1,\ldots,x_{n-1},\varnothing,\varnothing,\ldots، حيث n=∣x∣n=|x|. (أي، T[0]=▹T[0]=\triangleright، وT[i+1]=xiT[i+1]=x_{i} لكل i∈[n]i\in [n]، وT[i]=∅T[i]=\varnothing لكل i>ni>n.)

  • نهيّئ أيضًا i=0i=0 وs=0s=0.

  • ثم نكرّر العملية التالية:

    1. لنضع (s′,σ′,D)=δM(s,T[i])(s',\sigma',D) = \delta_M(s,T[i]).
    2. لنضع s←s′s \leftarrow s'، T[i]←σ′T[i] \leftarrow \sigma'.
    3. إذا كان D=RD=\mathsf{R} فنضع i→i+1i \rightarrow i+1، وإذا كان D=LD=\mathsf{L} فنضع i→max⁡{i−1,0}i \rightarrow \max\{i-1,0\}. (وإذا كان D=SD = \mathsf{S} فنُبقي ii كما هو.)
    4. إذا كان D=HD=\mathsf{H}، فإننا نتوقّف.
  • إذا توقّفت العملية أعلاه، فإن خرج MM، ويُرمز له بـM(x)M(x)، هو السلسلة y∈{0,1}∗y\in \{0,1\}^* التي تُحصل عليها بدمج كل الرموز في {0,1}\{0,1\} في المواضع T[0],…,T[i]T[0],\ldots, T[i]، حيث i+1i+1 هو أول موضع في الشريط يحتوي على ∅\varnothing.

  • وإذا لم تتوقّف آلة تورينج، فإننا نرمز لـ M(x)=⊥M(x)=\bot.

توقّف وتأمّل

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

لا ينبغي الخلط بين دالة الانتقال δM\delta_M لآلة تورينج MM والدالة التي تحسبها الآلة. إن دالة الانتقال δM\delta_M هي دالة متناهية، لها k∣Σ∣k|\Sigma| مدخلًا و4k∣Σ∣4k|\Sigma| خرجًا. (هل ترى لماذا؟) يمكن للآلة أن تحسب دالة لا نهائية FF تأخذ كمدخل سلسلة x∈{0,1}∗x\in \{0,1\}^* ذات طول اعتباطي، وقد تُخرج أيضًا سلسلة ذات طول اعتباطي.

في تعريفنا الرسمي، فرأنا بين الآلة MM ودالة انتقالها δM\delta_M، لأن دالة الانتقال تخبرنا بكل ما نحتاج معرفته عن آلة تورينج. غير أن هذا التمثيل اعتباطي نوعًا ما، ويستند إلى عرفنا بأن فضاء الحالات هو دائمًا الأرقام {0,…,k−1}\{0,\ldots,k-1\} مع 00 كحالة بدء. وتستخدم نصوص أخرى أعرافًا مختلفة، لذا قد يبدو تعريفها الرياضي لآلة تورينج مختلفًا في ظاهره. غير أن هذه التعريفات تصف العملية الحاسوبية نفسها، وتتمتع بالقدرات الحاسوبية نفسها. لذلك فهي متكافئة رغم اختلافاتها السطحية. انظر ملاحظات مرجعية لمقارنة بين تعريف — آلة تورينج وطريقة تعريف آلات تورينج في نصوص مثل Sipser [@SipserBook].

الدوال القابلة للحساب

ننتقل الآن إلى واحد من أهم تعريفات هذا الكتاب: الدوال القابلة للحساب (computable functions).

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

لتكن F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^* دالة (كلية) (total)، ولتكن MM آلة تورينج. نقول إن MM تحسب FF إذا وفقط إذا كان M(x)=F(x)M(x)=F(x) لكل x∈{0,1}∗x\in \{0,1\}^*.

نقول إن الدالة FF قابلة للحساب (computable) إذا وُجدت آلة تورينج MM تحسبها.

قد يبدو تعريف الدالة بأنها «قابلة للحساب» إذا وفقط إذا أمكن حسابها بآلة تورينج أمرًا «متهوّرًا»، لكن كما سنرى في نماذج حوسبة مكافئة، فإن القابلية للحساب بالمعنى المبيَّن في تعريف — الدوال القابلة للحساب تكافئ القابلية للحساب في أي نموذج معقول للحوسة تقريبًا. وتُعرف هذه العبارة بـ_أطروحة تشرتش-تورينج_ (Church-Turing Thesis). (وبخلاف أطروحة تشرتش-تورينج الموسَّعة (extended) التي ناقشناها في أطروحة كيرش-تورينغ الفيزيائية الموسّعة (نقاش)، فإن أطروحة تشرتش-تورينج نفسها مقبولة على نطاق واسع ولا توجد أجهزة مرشّحة لمهاجمتها.)

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

هذه مناسبة جيدة لتذكير القارئ بأن الدوال ليست هي البرامج (programs) نفسها:

Functions  ≠  Programs  . \text{Functions} \;\neq\; \text{Programs} \;.

آلة تورينج (أو برنامج) MM تستطيع أن تحسب (compute) دالة ما FF، لكنها ليست FF نفسها. وبخاصة، قد يوجد أكثر من برنامج واحد يحسب الدالة نفسها. والقابلية للحساب خاصية (property) للدوال، لا للآلات.

وسنولي في الغالب عناية خاصة للدوال F:{0,1}∗→{0,1}F:\{0,1\}^* \rightarrow \{0,1\} التي لها بتّ خرج واحد. ولذلك نمنح اسمًا خاصًا لمجموعة الدوال القابلة للحساب بهذا الشكل.

تعريف — الصنف R\mathbf{R}

نعرّف R\mathbf{R} بأنها مجموعة كل الدوال القابلة للحساب F:{0,1}∗→{0,1}F:\{0,1\}^* \rightarrow \{0,1\}.

ملاحظة — الدوال مقابل اللغات

كما ناقشنا في اللغات الشكليّة، تستخدم نصوص كثيرة مصطلح «اللغات» (languages) بدلًا من الدوال للإشارة إلى المهام الحاسوبية. نقول إن آلة تورينج MM تقرّر (decides) لغة LL إذا كان لكل مدخل x∈{0,1}∗x\in \{0,1\}^* تُخرج M(x)M(x) القيمة 11 إذا وفقط إذا كان x∈Lx\in L. وهذا يكافئ حساب الدالة البوليانية F:{0,1}∗→{0,1}F:\{0,1\}^* \rightarrow \{0,1\} المعرَّفة بأن F(x)=1F(x)=1 إذا وفقط إذا كان x∈Lx\in L. نقول إن اللغة LL قابلة للقرار (decidable) إذا وُجدت آلة تورينج MM تقرّرها. ولأسباب تاريخية، تسمّي بعض النصوص هذه اللغات أيضًا تعاودية (recursive)، ولهذا يُستخدم الحرف R\mathbf{R} غالبًا للدلالة على مجموعة الدوال البوليانية القابلة للحساب / اللغات القابلة للقرار المعرَّفة في تعريف — الصنف R.

في هذا الكتاب سنتمسّك بمصطلح الدوال بدلًا من اللغات، لكن جميع التعريفات والنتائج يمكن ترجمتها بسهولة ذهابًا وإيابًا باستخدام التكافؤ بين الدالة F:{0,1}∗→{0,1}F:\{0,1\}^* \rightarrow \{0,1\} واللغة L={x∈{0,1}∗  ∣  F(x)=1}L = \{ x\in \{0,1\}^* \;|\; F(x) = 1 \}.

الحلقات اللانهائية والدوال الجزئية

هناك فرق جوهري واحد بين الدوائر/البرامج المستقيمة (straight-line programs) وآلات تورينج، وهو كما يلي. بالنظر إلى برنامج NAND-CIRC PP، يمكننا دائمًا أن نعرف كم مدخلًا وكم خرجًا لدى PP بمجرد النظر إلى المتغيّرين X وY. وفضلاً عن ذلك، نحن مضمونون أنه إذا استدعينا PP على أي مدخل، فإن بعضَ الخرج سيُنتَج.

على النقيض من ذلك، عند إعطاء آلة تورينج MM، لا يمكننا تحديد طول خرج MM مسبقًا. بل إننا لا نعرف أصلًا هل سيُنتَج خرج أم لا! فمثلًا، من السهل جدًا أن نوجد آلة تورينج لا تُخرج أبدًا H\mathsf{H} في دالة انتقالها، وبالتالي لا تتوقّف أبدًا.

إذا فشلت آلة MM في التوقّف وإنتاج خرج على بعض المدخل xx، فإنها لا تستطيع حساب أي دالة كلية FF، لأننا بكل وضوح، على المدخل xx، ستفشل MM في إخراج F(x)F(x). غير أن MM تستطيع مع ذلك حساب دالة جزئية (partial function).[1]

فمثلًا، تأمّل الدالة الجزئية DIVDIV التي على المدخل زوج (a,b)(a,b) من الأعداد الطبيعية تُخرج ⌈a/b⌉\ceil{a/b} إذا كان b>0b > 0، وتكون غير معرَّفة في غير ذلك. يمكننا تعريف آلة تورينج MM تحسب DIVDIV على المدخل a,ba,b بإخراج أول c=0,1,2,…c=0,1,2,\ldots بحيث cb≥acb \geq a. فإذا كان a>0a>0 وb=0b=0 فإن الآلة MM لن تتوقّف أبدًا، لكن هذا مقبول، لأن DIVDIV غير معرَّفة على هذه المدخلات. وإذا كان a=0a=0 وb=0b=0، فإن الآلة MM ستُخرج 00، وهذا أيضًا مقبول، لأننا لا نباله بما يُخرجه البرنامج على المدخلات التي تكون DIVDIV فيها غير معرَّفة. رسميًا، نعرّف قابلية حساب الدوال الجزئية كما يلي:

تعريف — الدوال القابلة للحساب (جزئية أو كلية)

لتكن FF دالة كلية أو جزئية تُسقط {0,1}∗\{0,1\}^* على {0,1}∗\{0,1\}^*، ولتكن MM آلة تورينج. نقول إن MM تحسب FF إذا وفقط إذا كان M(x)=F(x)M(x)=F(x) لكل x∈{0,1}∗x\in \{0,1\}^* تكون FF معرَّفة عليه. نقول إن الدالة FF (الجزئية أو الكلية) قابلة للحساب إذا وُجدت آلة تورينج تحسبها.

لاحظ أنه إذا كانت FF دالة كلية، فإنها معرَّفة على كل x∈{0,1}∗x\in \{0,1\}^*، وبالتالي في هذه الحالة يكون تعريف — الدوال القابلة للحساب (جزئية أو كلية) مطابقًا لـتعريف — الدوال القابلة للحساب.

ملاحظة — رمز بوت

نستخدم غالبًا ⊥\bot كـ«رمز الفشل» الخاص بنا. إذا فشلت آلة تورينج MM في التوقّف على بعض المدخل x∈{0,1}∗x\in \{0,1\}^*، فإننا نرمز لذلك بـM(x)=⊥M(x) = \bot. وهذا لا يعني أن MM تُخرج ترميزًا ما للرمز ⊥\bot، بل يعني أن MM تدخل في حلقة لا نهائية عند إعطائها xx كمدخل.

إذا كانت الدالة الجزئية FF غير معرَّفة على xx، يمكننا أيضًا كتابة F(x)=⊥F(x) = \bot. لذلك قد يظن المرء أن تعريف — الدوال القابلة للحساب (جزئية أو كلية) يمكن تبسيطه إلى اشتراط أن M(x)=F(x)M(x) = F(x) لكل x∈{0,1}∗x\in \{0,1\}^*، وهو ما يعني أن MM تتوقّف على xx لكل xx إذا وفقط إذا كانت FF معرَّفة على xx. لكن ليس هذا هو الحال: لكي تحسب آلة تورينج MM دالة جزئية FF، ليس من الضروري أن تدخل MM في حلقة لا نهائية على المدخلات xx التي تكون FF غير معرَّفة عليها. كل ما يلزم هو أن تُخرج MM القيمة F(x)F(x) على قيم xx التي تكون FF معرَّفة عليها: أما على المدخلات الأخرى فيجوز أن تُخرج MM قيمة اعتباطية مثل 00 أو 11 أو أي شيء آخر، أو ألا تتوقّف أصلًا. ولنستعير مصطلحًا من لغة البرمجة C، فإن ما تفعله MM على المدخلات xx التي تكون FF غير معرَّفة عليها هو «سلوك غير معرّف» (undefined behavior).

آلات تورينج كلغات برمجة

اسم «آلة تورينج» وما يرتبط به من «شريط» و«رأس» يستحضر شيئًا ماديًا، في المقابل، فإننا نرى البرنامج (program) على أنه نص. لكن يمكننا التفكير في آلة تورينج على أنها برنامج أيضًا. فمثلًا، تأمّل آلة تورينج MM في مثال موسَّع: آلة تورينج للكلمات المتناظرة التي تحسب الدالة PALPAL بحيث PAL(x)=1PAL(x)=1 إذا وفقط إذا كان xx متناظرًا. ويمكننا أيضًا وصف هذه الآلة على أنها برنامج باستخدام شيفرة زائفة (pseudocode) شبيهة بـPython من الشكل التالي

# Gets an array Tape initialized to
# [">", x_0 , x_1 , .... , x_(n-1), "∅", "∅", ...]
# At the end of the execution, Tape[1] is equal to 1
# if x is a palindrome and is equal to 0 otherwise
def PAL(Tape):
    head = 0
    state = 0 # START
    while (state != 12):
        if (state == 0 && Tape[head]=='0'):
            state = 3 # LOOK_FOR_0
            Tape[head] = 'x'
            head += 1 # move right
        if (state==0 && Tape[head]=='1')
            state = 4 # LOOK_FOR_1
            Tape[head] = 'x'
            head += 1 # move right
        ... # more if statements here

التفاصيل الدقيقة لهذا البرنامج ليست مهمّة. المهم هو أن بإمكاننا وصف آلات تورينج بوصفها برامج. وفضلاً عن ذلك، لاحظ أن عند ترجمة آلة تورينج إلى برنامج، يتحوّل الشريط إلى قائمة (list) أو مصفوفة (array) قادرة على حمل قيم من المجموعة المتناهية Σ\Sigma.[2] ويمكن التفكير في موضع الرأس (head position) على أنه متغيّر (variable) ذا قيمة أعداد صحيحة يحمل أعدادًا صحيحة غير محدودة الحجم. أما الحالة (state) فهي سجلّ محلي (local register) يمكنه حمل إحدى قيم عدد ثابت من القيم في [k][k].

وبشكل أعمّ، يمكننا التفكير في كل آلة تورينج MM على أنها مكافئة لبرنامج شبيه بما يلي:

# Gets an array Tape initialized to
# [">", x_0 , x_1 , .... , x_(n-1), "∅", "∅", ...]
def M(Tape):
    state = 0
    i     = 0 # holds head location
    while (True):
        # Move head, modify state, write to tape
        # based on current state and cell at head
        # below are just examples for how program looks for a particular transition function
        if Tape[i]=="0" and state==7: # δ_M(7,"0")=(19,"1","R")
            Tape[i]="1"
            i += 1

            state = 19
        elif Tape[i]==">" and state == 13: # δ_M(13,">")=(15,"0","S")
            Tape[i]="0"
            state = 15
        elif ...
        ...
        elif Tape[i]==">" and state == 29: # δ_M(29,">")=(.,.,"H")
            break # Halt

ولو أردنا استخدام متغيّرات بوليانية (Boolean) فقط (أي متغيّرات قيمتها 00/11)، لاستطعنا ترميز متغيّرات state باستخدام ⌈log⁡k⌉\ceil{\log k} بت. وبالمثل، يمكننا تمثيل كل عنصر من أبجدية Σ\Sigma باستخدام ℓ=⌈log⁡∣Σ∣⌉\ell=\ceil{\log |\Sigma|} بت، وبالتالي يمكننا استبدال المصفوفة ذات القيم في Σ\Sigma والمسمّاة Tape[] بـℓ\ell مصفوفة ذات قيم بوليانية هي Tape0[]،…\ldots، Tape(ℓ−1)(\ell - 1)[].

لغة البرمجة NAND-TM

نقدّم الآن لغة البرمجة NAND-TM، التي تلتقط قدرة آلة تورينج بصيغة لغات برمجة. وكما أن الفرق بين الدوائر البوليانية وآلات تورينج، فإن الفرق الرئيسي بين NAND-TM وNAND-CIRC هو أن NAND-TM تمثّل خوارزمية واحدة موحّدة (uniform) تستطيع حساب دالة تأخذ مداخل بأطوال اعتباطية. ولتحقيق ذلك، نوسّع لغة البرمجة NAND-CIRC ببنيتين:

  • الحلقات (loops): إن NAND-CIRC هي لغة برمجة مستقيمة (straight-line) — فبرنامج NAND-CIRC بطول ss سطرًا يستغرق بالضبط ss خطوة حسابية، وبالتالي لا يستطيع حتى لمس أكثر من 3s3s متغيّرًا. وتتيح لنا الحلقات (loops) استخدام برنامج بطول ثابت لترميز تعليمات حساب قد يستغرق وقتًا اعتباطيًا.

  • المصفوفات (arrays): يلمس برنامج NAND-CIRC بطول ss سطرًا ما لا يزيد عن 3s3s متغيّرًا. ورغم أننا يمكننا استخدام متغيّرات بأسماء مثل Foo_17 أو Bar[22] في NAND-CIRC، فإنها ليست مصفوفات حقيقية، لأن الرقم في المُعرِّف هو ثابت «مضمَّن» (hardwired) في البرنامج. أما NAND-TM فتحتوي مصفوفات فعلية يمكن أن يكون طولها غير محدود مسبقًا.

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

إذن، فإن طريقة جيدة لتذكّر NAND-TM هي استخدام المعادلة غير الرسمية التالية:

NAND-TM  =  NAND-CIRC  +  loops  +  arrays \text{NAND-TM} \;=\; \text{NAND-CIRC} \;+\; \text{loops} \;+\; \text{arrays}

ملاحظة — ‏NAND-CIRC + حلقات + مصفوفات = كل شيء.

كما سنرى، فإن إضافة الحلقات والمصفوفات إلى NAND-CIRC تكفي لالتقاط القدرة الكاملة لكل لغات البرمجة! ولذلك يمكننا استبدال «NAND-TM» بأيٍّ من Python أو C أو Javascript أو OCaml وغيرها في الطرف الأيسر من المعادلة. لكننا نتقدّم بأشواط على حالنا: سنناقش هذه المسألة في نماذج حوسبة مكافئة.

عمليًّا، تضيف لغة البرمجة NAND-TM الميزات التالية فوق NAND-CIRC (انظر الشكل التوضيحي):

  • نضيف متغيّرًا خاصًا ذي قيمة عدد صحيح (integer valued) اسمه i. وجميع المتغيّرات الأخرى في NAND-TM هي ذات قيمة بوليانية (Boolean valued) (كما في NAND-CIRC).

  • إلى جانب i، تمتلك NAND-TM نوعين من المتغيّرات: قياسية (scalars) و_مصفوفات_ (arrays). فالمتغيّرات القياسية (scalar variables) تحمل بتًّا واحدًا (كما في NAND-CIRC تمامًا). أما المتغيّرات من نوع المصفوفة (array variables) فتُخزّن عددًا غير محدود من البتات. وفي أي لحظة من الحساب يمكننا الوصول إلى متغيّرات المصفوفة عند الموضع المُفهرس بـi باستخدام Foo[i]. ولا يمكننا الوصول إلى المصفوفات في مواضع غير الموضع الذي يشير إليه i.

  • نستخدم العرف أن المصفوفات (arrays) تبدأ دائمًا بحرف كبير، وأن المتغيّرات القياسية (scalar variables) (التي لا تُفهرس أبدًا بـi) تبدأ بحروف صغيرة. لذا Foo هي مصفوفة وbar متغيّر قياسي.

  • يُعتبر المدخل X والخرج Y الآن مصفوفتين (arrays) قيمتها أصفار وواحدات. (وهناك أيضًا مصفوفتان خاصتان أخريان هما X_nonblank وY_nonblank، انظر أدناه.)

  • نضيف تعليمة خاصة MODANDJUMP تأخذ كمدخل متغيّرين بوليين a,ba,b وتفعل ما يلي:

    • إذا كان a=1a=1 وb=1b=1 فإن MODANDJUMP(a,ba,b) يزيد i بمقدار واحد ويقفز إلى أول سطر في البرنامج.
    • إذا كان a=0a=0 وb=1b=1 فإن MODANDJUMP(a,ba,b) ينقص i بمقدار واحد ويقفز إلى أول سطر في البرنامج. (وإذا كان i مساويًا بالفعل لـ 00 فإنه يبقى عند 00.)
    • إذا كان a=1a=1 وb=0b=0 فإن MODANDJUMP(a,ba,b) يقفز إلى أول سطر في البرنامج دون تعديل i.
    • إذا كان a=b=0a=b=0 فإن MODANDJUMP(a,ba,b) يوقف تنفيذ البرنامج.
  • تظهر تعليمة MODANDJUMP دائمًا في آخر سطر من برنامج NAND-TM ولا تظهر في أي مكان آخر.

القيم الافتراضية. نحتاج إلى عرفٍ إضافي واحد للتعامل مع «القيم الافتراضية». تمتلك آلات تورينج الرمز الخاص ∅\varnothing للدلالة على أن موضعًا في الشريط «فارغ» أو «غير مُهيّأ». أما في NAND-TM فلا يوجد مثل هذا الرمز، وجميع المتغيّرات بوليانية (Boolean)، تحتوي إمّا على 00 أو على 11. وتأخذ جميع المتغيّرات ومواضع المصفوفات القيمة الافتراضية 00 إذا لم تكن قد هُيّئت إلى قيمة أخرى. ولكي نتتبّع ما إذا كان 00 في مصفوفة يمثّل صفرًا حقيقيًا أم خليّة غير مُهيّأة، يمكن للمبرمج دائمًا أن يضيف إلى المصفوفة Foo مصفوفة «مرافقة» (companion array) هي Foo_nonblank ويضبط Foo_nonblank[i] على 11 كلما كان الموضع رقم i مُهيّأ. وبخاصة، سنستخدم هذا العرف مع مصفوفتي المدخل والخرج X وY. يمتلك برنامج NAND-TM أربع مصفوفات خاصة هي X وX_nonblank وY وY_nonblank. وعند تنفيذ برنامج NAND-TM على مدخل x∈{0,1}∗x\in \{0,1\}^* طوله nn، تُهيّأ الخلايا nn الأولى من المصفوفة X على x0,…,xn−1x_0,\ldots,x_{n-1}، وتُهيّأ الخلايا nn الأولى من المصفوفة X_nonblank على 11. (أما جميع الخلايا غير المُهيّأة فتأخذ القيمة الافتراضية 00.) ويكون خرج برنامج NAND-TM هو السلسلة Y[00], …\ldots, Y[m−1m-1] حيث mm أصغر عدد صحيح بحيث Y_nonblank[mm]=0=0. ويُستدعى برنامج NAND-TM مع X وX_nonblank مُهيّأتين لاحتواء المدخل، ويكتب في Y وY_nonblank لإنتاج الخرج.

رسميًّا، تُعرَّف برامج NAND-TM كما يلي:

تعريف — برامج NAND-TM

يتكوّن برنامج NAND-TM (NAND-TM program) من تسلسل أسطر من الشكل foo = NAND(bar,blah) ينتهي بسطر من الشكل MODANDJUMP(foo,bar)، حيث يكون foo وbar وblah إمّا متغيّرات قياسية (scalar variables) (تسلسلات من الحروف والأرقام والشرطات السفلية) أو متغيّرات مصفوفة (array variables) من الشكل Foo[i] (تبدأ بحروف كبيرة وتُفهرس بـi). ويمتلك البرنامج مسبقًا متغيّرات المصفوفة X وX_nonblank وY وY_nonblank ومتغيّر الفهرسة i، ويمكنه استخدام متغيّرات مصفوفة وقياسية إضافية.

إذا كان PP برنامج NAND-TM وكان x∈{0,1}∗x\in \{0,1\}^* مدخلًا، فإن تنفيذ PP على xx هو العملية التالية:

  1. تُهيّأ المصفوفتان X وX_nonblank بواسطة X[ii]=xi=x_i وX_nonblank[ii]=1=1 لكل i∈[∣x∣]i\in [|x|]. وتُهيّأ جميع المتغيّرات والخلايا الأخرى على 00. كما يُهيّأ متغيّر الفهرسة i على 00.

  2. يُنفَّذ البرنامج سطرًا بسطر. وعندما يُنفَّذ آخر سطر MODANDJUMP(foo,bar) نفعل ما يلي:

    a. إذا كان foo=1=1 وbar=0=0، فإننا نقفز إلى أول سطر دون تعديل قيمة i.

    b. إذا كان foo=1=1 وbar=1=1، فإننا نزيد i بمقدار واحد ونقفز إلى أول سطر.

    c. إذا كان foo=0=0 وbar=1=1، فإننا ننقص i بمقدار واحد (ما لم يكن صفرًا بالفعل) ونقفز إلى أول سطر.

    d. إذا كان foo=0=0 وbar=0=0، فإننا نتوقّف ونُخرج Y[00], …\ldots, Y[m−1m-1] حيث mm أصغر عدد صحيح بحيث Y_nonblank[mm]=0=0.

لمحة سريعة: NAND-TM مقابل آلات تورينج

وكما يدلّ الاسم، فإن برامج NAND-TM هي تنفيذ مباشر لآلات تورينج بصيغة لغة برمجة. وسنُظهر التكافؤ أدناه، لكن يمكنك أن ترى بالفعل كيف تقابل مكوّنات آلات تورينج مكوّنات برامج NAND-TM بعضها ببعض:

---
caption: 'Turing Machine and NAND-TM analogs'
alignment: 'LL'
table-width: '1/1'
id: TMvsNANDTMtable
---
**Turing Machine** | **NAND-TM program**
*State:* single register that takes values in $[k]$ | *Scalar variables:* Several variables such as `foo`, `bar` etc.. each taking values in $\{0,1\}$.
*Tape:* One tape containing values in a finite set $\Sigma$. Potentially infinite but $T[t]$ defaults to $\varnothing$ for all locations $t$ that have not been accessed. | *Arrays:* Several arrays such as `Foo`, `Bar` etc.. for each such array `Arr` and index $j$, the value of `Arr` at position $j$ is either $0$ or $1$. The value defaults to $0$ for position that have not been written to.
*Head location:* A number $i\in \mathbb{N}$ that encodes the position of the head. | *Index variable:* The variable `i` that can be used to access the arrays.
*Accessing memory:* At every step the Turing machine has access to its local state, but can only access the tape at the position of the current head location. | *Accessing memory:* At every step a NAND-TM program has access to all the scalar variables, but can only access the arrays at the location `i` of the index variable
*Control of location:* In each step the machine can move the head location by at most one position. | *Control of index variable:* In each iteration of its main loop the program can modify the index `i` by at most one.

أمثلة

نعرض الآن بعض الأمثلة لبرامج NAND-TM.

مثال — الزيادة في NAND-TM

فيما يلي برنامج NAND-TM لحساب دالة الزيادة (increment function). أي، INC:{0,1}∗→{0,1}∗INC:\{0,1\}^* \rightarrow \{0,1\}^* بحيث لكل x∈{0,1}nx\in \{0,1\}^n، تكون INC(x)INC(x) هي السلسلة yy ذات n+1n+1 بت التي إذا كان X=∑i=0n−1xi⋅2iX = \sum_{i=0}^{n-1}x_i \cdot 2^i هو العدد الذي يمثّله xx، فإن yy هي التمثيل الثنائي (بترتيب البت الأقل قيمة أولًا) للعدد X+1X+1.

نبدأ بوصف البرنامج مستخدمين «سكرًا نحويًا» (syntactic sugar) خاصًا بـNAND-CIRC لدوال IF وXOR وAND (وكذلك الدالة الثابتة one، والدالة COPY التي تُسقط البت على نفسه فحسب).

carry = IF(started,carry,one(started))
started = one(started)
Y[i] = XOR(X[i],carry)
carry = AND(X[i],carry)
Y_nonblank[i] = one(started)
MODANDJUMP(X_nonblank[i],X_nonblank[i])

ولأننا استخدمنا سكرًا نحويًا، فإن ما سبق ليس، بصرامة، برنامج NAND-TM صالحًا. غير أنه، بفتح «كل» السكر النحوي، نحصل على البرنامج الصالح التالي «الخالي من السكر» لحساب الدالة نفسها.

temp_0 = NAND(started,started)
temp_1 = NAND(started,temp_0)
temp_2 = NAND(started,started)
temp_3 = NAND(temp_1,temp_2)
temp_4 = NAND(carry,started)
carry = NAND(temp_3,temp_4)
temp_6 = NAND(started,started)
started = NAND(started,temp_6)
temp_8 = NAND(X[i],carry)
temp_9 = NAND(X[i],temp_8)
temp_10 = NAND(carry,temp_8)
Y[i] = NAND(temp_9,temp_10)
temp_12 = NAND(X[i],carry)
carry = NAND(temp_12,temp_12)
temp_14 = NAND(started,started)
Y_nonblank[i] = NAND(started,temp_14)
MODANDJUMP(X_nonblank[i],X_nonblank[i])

مثال — XOR في NAND-TM

فيما يلي برنامج NAND-TM لحساب دالة XOR على مداخل بطول اعتباطي. أي XOR:{0,1}∗→{0,1}XOR:\{0,1\}^* \rightarrow \{0,1\} بحيث XOR(x)=∑i=0∣x∣−1ximod  2XOR(x) = \sum_{i=0}^{|x|-1} x_i \mod 2 لكل x∈{0,1}∗x\in \{0,1\}^*. ومرة أخرى، نستخدم نوعًا من «السكر النحوي». وبتحديد، فإننا نصل إلى المصفوفتين X وY عند الخانة رقم صفر فيها، في حين أن NAND-TM يسمح بالوصول إلى المصفوفات عند إحداثي المتغيّر i فقط.

temp_0 = NAND(X[0],X[0])
Y_nonblank[0] = NAND(X[0],temp_0)
temp_2 = NAND(X[i],Y[0])
temp_3 = NAND(X[i],temp_2)
temp_4 = NAND(Y[0],temp_2)
Y[0] = NAND(temp_3,temp_4)
MODANDJUMP(X_nonblank[i],X_nonblank[i])

لتحويل البرنامج أعلاه إلى برنامج NAND-TM صالح، يمكننا تحويل المراجع مثل X[0] وY[0] إلى متغيّرات قياسية x_0 وy_0 (وبالمثل يمكننا تحويل أي مرجع من الشكل Foo[17] أو Bar[15] إلى متغيّرات قياسية مثل foo_17 وbar_15). ثم نحتاج بعد ذلك إلى إضافة شيفرة لتحميل قيمة X[0] إلى x_0، وبالمثل للكتابة في Y[0] القيمة y_0، لكن هذا ليس صعبًا. ولأننا نعرف أن المتغيّرات تُهيَّأ على الصفر افتراضيًا، يمكننا إنشاء متغيّر init سيضبط على 11 في نهاية التكرار الأول ولن يتغيّر منذ ذلك الحين. يمكننا بعد ذلك إضافة مصفوفة Atzero وشيفرة تعدّل Atzero[i] لتصبح 11 إذا كان init يساوي 00، وتتركها كما هي في غير ذلك. وهذا سيضمن أن Atzero[i] يساوي 11 إذا وفقط إذا كان i مضبوطًا على الصفر، ويسمح للبرنامج بأن يعرف متى كنّا عند الموضع صفر. وبالتالي يمكننا إضافة شيفرة لقراءة المتغيّرين القياسيين x_0 وy_0 وكتابتهما عندما نكون عند الموضع صفر، وكذلك شيفرة لنقل i إلى الصفر ثم التوقّف في النهاية. وإنّ إتمام هذا الجزء بالكامل مملّ بعض الشيء، لكنه قد يكون تمرينًا جيدًا.

توقّف وتأمّل

إنّ اشتقاق المثالين أعلاه بالكامل سيقطع شوطًا طويلًا نحو فهم لغة NAND-TM. انظر مستودعنا على GitHub للحصول على المواصفة الكاملة للغة NAND-TM.

تكافؤ آلات تورينج وبرامج NAND-TM

بالنظر إلى النقاش أعلاه، قد لا يكون من المُفاجئ أن تكتشف آلات تورينج أنها متكافئة مع برامج NAND-TM. وفي الواقع، لقد صمّمنا لغة NAND-TM لتتمتع بهذه الخاصية. ومع ذلك، فهذه نتيجة مهمة، وهي أولى نتائج التكافؤ الإضافية العديدة التي سنراها في هذا الكتاب.

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

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

لإثبات مثل هذه النسخة من التكافؤ، نحتاج إلى بيان اتجاهين. فنحن بحاجة إلى أن نتمكّن من (1) تحويل آلة تورينج MM إلى برنامج NAND-TM PP يحسب الدالة نفسها التي يحسبها MM، و__(2)__ تحويل برنامج NAND-TM PP إلى آلة تورينج MM تحسب الدالة نفسها التي يحسبها PP.

وتوضَّح فكرة البرهان في الشكل التوضيحي. ولإثبات (1)، عند إعطائنا آلة تورينج MM، سننشئ برنامج NAND-TM PP يملك مصفوفة Tape لشريط MM، ومتغيّرًا قياسيًا (أي غير مصفوفة) واحدًا أو أكثر اسمه state لحالة MM. وبتحديد، بما أن حالة آلة تورينج ليست في {0,1}\{0,1\} بل في مجموعة أكبر [k][k]، فإننا سنستخدم ⌈log⁡k⌉\ceil{\log k} متغيّرًا state_00 ، …\ldots، state_⌈log⁡k⌉−1\ceil{\log k}-1 لتخزين تمثيل الحالة. وبالمثل، ولترميز الأبجدية الأكبر Σ\Sigma للشريط، سنستخدم ⌈log⁡∣Σ∣⌉\ceil{\log |\Sigma|} مصفوفة Tape_00 ، …\ldots، Tape_⌈log⁡∣Σ∣⌉−1\ceil{\log |\Sigma|}-1، بحيث يرمز الموضع ithi^{th} في هذه المصفوفات إلى الرمز ithi^{th} في الشريط. وباستخدام الحقيقة أن كل دالة يمكن حسابها ببرنامج NAND-CIRC، فإننا سنتمكّن من حساب دالة انتقال MM، مع استبدال الحركة إلى اليسار وإلى اليمين بنقصان i وزيادته على التوالي.

ونُثبّت (2) باستخدام أفكار مشابهة جدًا. فإذا كان لدينا برنامج PP يستخدم aa متغيّر مصفوفة وbb متغيّرًا قياسيًا، فإننا سننشئ آلة تورينج لها نحو 2b2^b حالة لترميز قيم المتغيّرات القياسية، وأبجدية حجمها نحو 2a2^a حتى نتمكن من ترميز المصفوفات باستخدام شريطنا. (والسبب في أن الأحجام «نحو» 2a2^a و2b2^b فقط هو أننا نحتاج إلى إضافة بعض الرموز والخطوات لأغراض المساءلة.) وتُحاكي آلة تورينج MM كل تكرار من تكرارات البرنامج PP بتحديث حالتها وشريطها تبعًا لذلك.

/images/introtcs/lec_06_loops-6.webp

برهان

نبدأ بإثبات اتجاه «إذا» من مبرهنة — آلات تورينج وبرامج NAND-TM متكافئة. أي إننا سنبيّن أنه عند إعطائنا آلة تورينج MM، يمكننا إيجاد برنامج NAND-TM PMP_M بحيث لكل مدخل xx، إذا توقّفت MM على المدخل xx بالخرج yy فإن PM(x)=yP_M(x)=y. ولأن هدفنا هو بيان أن مثل هذا البرنامج PMP_M يوجد فحسب، فلا نحتاج إلى كتابة الشيفرة الكاملة لـPMP_M سطرًا بسطر، ويمكننا الاستفادة من مختلف أنواع «السكر النحوي» في وصفه.

والملاحظة المفتاحية هي أنه، بواسطة مبرهنة — شمولية NAND، يمكننا حساب كل دالة متناهية باستخدام برنامج NAND-CIRC. وبخاصة، تأمّل دالة الانتقال δM:[k]×Σ→[k]×Σ×{L,R,S,H}\delta_M:[k]\times \Sigma \rightarrow [k] \times \Sigma \times \{\mathsf{L},\mathsf{R},\mathsf{S},\mathsf{H}\} لآلة تورينج لدينا. ويمكننا ترميز مكوّناتها كما يلي:

  • نرمّز [k][k] باستخدام {0,1}ℓ\{0,1\}^\ell وΣ\Sigma باستخدام {0,1}ℓ′\{0,1\}^{\ell'}، حيث ℓ=⌈log⁡k⌉\ell = \ceil{\log k} وℓ′=⌈log⁡∣Σ∣⌉\ell' = \ceil{\log |\Sigma|}.

  • نرمّز المجموعة {L,R,S,H}\{\mathsf{L},\mathsf{R}, \mathsf{S},\mathsf{H} \} باستخدام {0,1}2\{0,1\}^2. وسنختار الترميز L↦01\mathsf{L} \mapsto 01، R↦11\mathsf{R} \mapsto 11، S↦10\mathsf{S} \mapsto 10، H↦00\mathsf{H} \mapsto 00. (وهذا يوافق بالمصادفة دلالة العملية MODANDJUMP.)

إذن يمكننا مطابقة δM\delta_M مع دالة M‾:{0,1}ℓ+ℓ′→{0,1}ℓ+ℓ′+2\overline{M}:\{0,1\}^{\ell+\ell'} \rightarrow \{0,1\}^{\ell+\ell'+2}، تُسقط السلاسل ذات الطول ℓ+ℓ′\ell+\ell' على السلاسل ذات الطول ℓ+ℓ′+2\ell+\ell'+2. وبموجب مبرهنة — شمولية NAND، يوجد برنامج NAND-CIRC ComputeM ذو طول متناهٍ يحسب هذه الدالة M‾\overline{M}. وتتمثّل فكرة برنامج NAND-TM الذي يحاكي MM فيما يلي:

  1. استخدام المتغيّرات state_00 …\ldots state_ℓ−1\ell-1 لترميز حالة MM.

  2. استخدام المصفوفات Tape_00[] …\ldots Tape_ℓ′−1\ell'-1[] لترميز شريط MM.

  3. استخدام الحقيقة بأن الانتقال متناهٍ ويمكن حسابه ببرنامج NAND-CIRC.

وبناءً على ما سبق، يمكننا كتابة شيفرة من الشكل:

state_00 …\ldots state_ℓ−1\ell-1, Tape_00[i]…\ldots Tape_ℓ′−1\ell'-1[i], dir0,dir1 ←\leftarrow TRANSITION( state_00 …\ldots state_ℓ−1\ell-1, Tape_00[i]…\ldots Tape_ℓ′−1\ell'-1[i] )

MODANDJUMP(dir0,dir1)

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

وفي الاتجاه الآخر، نفترض أن PP برنامج NAND-TM فيه ss أسطر، وℓ\ell متغيّرًا قياسيًا، وℓ′\ell' متغيّر مصفوفة. وسنبيّن أن وُجدت آلة تورينج MPM_P لها 2ℓ+C2^\ell+C حالة وأبجدية Σ\Sigma بحجم C′+2ℓ′C' + 2^{\ell'} تحسب الدوال نفسها التي يحسبها PP (حيث CC وC′C' ثابتان سنحدّدهما لاحقًا).

وبتحديد، تأمّل الدالة P‾:{0,1}ℓ×{0,1}ℓ′→{0,1}ℓ×{0,1}ℓ′\overline{P}:\{0,1\}^\ell \times \{0,1\}^{\ell'} \rightarrow \{0,1\}^\ell \times \{0,1\}^{\ell'} التي، على مدخل محتوى متغيّرات PP القياسية ومحتوى متغيّرات المصفوفة عند الموضع i في بداية تكرار، تُخرج كل القيم الجديدة لهذه المتغيّرات عند آخر سطر في التكرار، قبيل تنفيذ تعليمة MODANDJUMP مباشرةً.

إذا كان foo وbar هما المتغيّران المستخدمان كمدخل لتعليمة MODANDJUMP، فإننا، بناءً على قيم هذين المتغيّرين، نستطيع حساب ما إذا كان i سيزيد أم ينقص أم يبقى على قيمته، وما إذا كان البرنامج سيتوقّف أم سيقفز إلى البداية. إذن يمكن لآلة تورينج أن تحاكي تنفيذًا واحدًا من تكرارات PP باستخدام دالة متناهية مطبَّقة على أبجديتها. وسيكون عمل آلة تورينج إجمالًا كما يلي:

  1. تُرمّز الآلة MPM_P محتوى متغيّرات مصفوفة PP في شريطها، ومحتوى المتغيّرات القياسية في (جزء من) حالتها. وبتحديد، إذا كان لدى PP عدد ℓ\ell من المتغيّرات المحلية وعدد tt من المصفوفات، فإن فضاء حالات MM سيكون كبيرًا بما يكفي لترميز كل تعيينات الـ2ℓ2^\ell للمتغيّرات المحلية، وستكون أبجدية Σ\Sigma لـMM كبيرة بما يكفي لترميز كل تعيينات الـ2t2^t لمتغيّرات المصفوفة عند كل موضع. ويقابل موضع الرأس متغيّر الفهرسة i.

  2. تذكّر أن كل سطر في البرنامج PP يقابل قراءة وكتابة إمّا متغيّر قياسي أو متغيّر مصفوفة عند الموضع i. وفي تكرار واحد من تكرارات PP تبقى قيمة i ثابتة، ولذلك يمكن للآلة MM أن تحاكي هذا التكرار بقراءة قيم كل متغيّرات المصفوفة عند i (وهي مرمّزة بالرمز الواحد في الأبجدية Σ\Sigma الموجود في الخلية رقم i من الشريط)، وقراءة قيم كل المتغيّرات القياسية (وهي مرمّزة في الحالة)، ثم تحديث كليهما. ويمكن لدالة انتقال MM أن تُخرج L\mathsf{L} أو S\mathsf{S} أو R\mathsf{R} بحسب ما إذا كانت القيم المعطاة إلى العملية MODANDJUMP هي 0101 أو 1010 أو 1111 على التوالي.

  3. عندما يتوقّف البرنامج (أي، حين يحصل MODANDJUMP على 0000)، تدخل آلة تورينج في حلقة خاصة تنسخ نتائج مصفوفة Y إلى الخرج ثم تتوقّف. ويمكننا تحقيق ذلك بإضافة بضع حالات أخرى.

ما سبق ليس وصفًا رسميًّا كاملًا لآلة تورينج، لكن هدفنا هو بيان أن آلة من هذا النوع موجودة فحسب. ويمكن ملاحظة أن MPM_P تحاكي كل خطوة من خطوات PP، وبالتالي تحسب الدالة نفسها التي يحسبها PP.

ملاحظة — تكافؤ زمن التنفيذ (اختياري)

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

المواصفة مقابل التنفيذ (مرة أخرى)

ما إن تفهم تعريفات كلٍّ من برامج NAND-TM وآلات تورينج، تصبح مبرهنة — آلات تورينج وبرامج NAND-TM متكافئة بديهية. وفي الواقع، إن برامج NAND-TM ليست نموذجًا مختلفًا عن آلات تورينج بقدر ما هي مجرد إعادة صياغة للنموذج نفسه باستخدام تدوين لغات البرمجة. ويمكنك أن تفكّر في الفرق بين آلة تورينج وبرنامج NAND-TM على أنه الفرق بين تمثيل عدد بكتابة عشرية أو كتابة ثنائية. على النقيض من ذلك، فإن الفرق بين دالة FF وبين آلة تورينج تحسب FF أعمق بكثير: إنه أشبه بالفرق بين المعادلة x2+x=12x^2 + x = 12، والعدد 33 الذي حلّ للمعادلة. ولهذا السبب، ومع أخذنا عناية خاصة في التمييز بين الدوال وبين البرامج أو الآلات، سنُعرّف غالبًا المفهومين الأخيرين على أنهما واحد. وسنتنقّل بحرّية بين وصف خوارزمية على أنها آلة تورينج أو أنها برنامج NAND-TM (وكذلك بعض نماذج الحوسبة المكافئة الأخرى التي سنراها في نماذج حوسبة مكافئة وما بعدها).

---
caption: 'Specification vs Implementation formalisms'
alignment: 'LL'
table-width: ''
id: specvsimp
---
*Setting* ; *Specification* ; *Implementation*
_Finite computation_ ; __Functions__ mapping $\{0,1\}^n$ to $\{0,1\}^m$ ; __Circuits__, __Straightline programs__
_Infinite computation_ ; __Functions__ mapping $\{0,1\}^*$ to $\{0,1\}$ or to $\{0,1\}^*$. ; __Algorithms__, __Turing Machines__, __Programs__

السكر النحوي في NAND-TM

تمامًا كما فعلنا مع NAND-CIRC في السكر النحوي، وحساب كل دالة، يمكننا استخدام «السكر النحوي» لتسهيل كتابة برامج NAND-TM. وللبداية، يمكننا استخدام كل السكر النحوي الخاص بـNAND-CIRC، مثل تعريفات الماكروّات والشروط (أي if/then). غير أننا يمكننا الذهاب إلى أبعد من ذلك وتحقيق (مثلًا):

  • حلقات داخلية مثل عمليات while وfor الشائعة في كثير من لغات البرمجة.

  • متغيّرات فهرسة متعدّدة (مثلًا، ليس i فحسب، بل يمكننا إضافة j وk وغيرها).

  • مصفوفات بأكثر من بُعد واحد (مثل Foo[i][j]، Bar[i][j][k] وغيرها.)

في كل هذه الحالات (وغيرها الكثير) يمكننا تنفيذ الميزة الجديدة بوصفها مجرد «سكر نحوي» فوق NAND-TM القياسي. وهذا يعني أن مجموعة الدوال القابلة للحساب بـNAND-TM مع هذه الميزة هي نفسها مجموعة الدوال القابلة للحساب بـNAND-TM القياسي. وبالمثل، يمكننا أن نبيّن أن مجموعة الدوال القابلة للحساب بآلات تورينج التي لها أكثر من شريط واحد، أو بشرائط ذات أبعاد أكثر من بعد واحد، هي نفسها مجموعة الدوال القابلة للحساب بآلات تورينج القياسية.

‏GOTO والحلقات الداخلية

يمكننا تنفيذ بنيات تكرارية (looping constructs) أكثر تقدّمًا من البسيطة MODANDJUMP. فمثلًا، يمكننا تنفيذ GOTO. وتُقابل تعليمة GOTO القفز إلى سطر معيّن أثناء التنفيذ. فمثلًا، إذا كان لدينا شيفرة من الشكل

"start":  do foo
   GOTO("end")
"skip": do bar
"end": do blah

فإن البرنامج سينفّذ فقط foo وblah، لأنه عند الوصول إلى السطر GOTO("end") سيقفز إلى السطر الموسوم بـ"end". ويمكننا تحقيق أثر GOTO في NAND-TM باستخدام الشروط (conditionals). وفي الشيفرة أدناه، نفترض لدينا متغيّر pc يمكن أن يأخذ سلاسل ذات طول ثابت ما. وهذا يمكن ترميزه باستخدام عدد متناهٍ من المتغيّرات البوليانية pc_0، pc_1، …\ldots، pc_k−1k-1، بحيث عندما نكتب أدناه pc = "label" فإننا نعني شيئًا مثل pc_0 = 0,pc_1 = 1, …\ldots (حيث تقابل البتات 0,1,…0,1,\ldots ترميز السلسلة المتناهية "label" كسلسلة من الطول kk). ونفترض أيضًا أننا توفّر على تعليمات شرطية (أي تعليمات if)، يمكننا محاكاتها باستخدام السكر النحوي بالطريقة نفسها التي فعلناها في NAND-CIRC.

لمحاكاة تعليمة GOTO، سنعدّل أولًا برنامج P من الشكل

do foo
do bar
do blah

ليصبح بالشكل التالي (باستخدام السكر النحوي لـif):

pc = "line1"
if (pc=="line1"):
    do foo
    pc = "line2"
if (pc=="line2"):
    do bar
    pc = "line3"
if (pc=="line3"):
    do blah

وهذان البرنامجان يفعلان الشيء نفسه. ويُقابل المتغيّر pc «عدّاد البرنامج» (program counter)، وهو يخبر البرنامج أي سطر ينفّذ تاليًا. ويمكننا أن نرى أن لو أردنا محاكاة GOTO("line3") لاستطعنا ببساطة تعديل التعليمة pc = "line2" لتصبح pc = "line3".

في NAND-CIRC لم تكن لدينا سوى عمليات GOTO تتقدّم إلى الأمام في الشيفرة، لكن بما أن كل شيء في NAND-TM مندرج ضمن حلقة خارجية كبيرة، يمكننا استخدام الأفكار نفسها لتنفيذ عمليات GOTO إلى الخلف، وكذلك الحلقات الشرطية.

حلقات أخرى. وبمجرد أن تتوفّر لدينا GOTO، يمكننا محاكاة جميع بنيات الحلقة القياسية مثل while أو do .. until أو for في NAND-TM أيضًا. فمثلًا، يمكننا استبدال الشيفرة

while foo:
    do blah
do bar

بما يلي:

"loop":
    if NOT(foo): GOTO("next")
    do blah
    GOTO("loop")
"next":
    do bar

ملاحظة — ‏GOTO في لغات البرمجة

كانت تعليمة GOTO عنصرًا أساسيًا في معظم لغات البرمجة المبكرة، لكنها فقدت كثيرًا من شعبيتها ولم تعد موجودة في كثير من اللغات الحديثة مثل Python و_Java_ و_Javascript_. وفي سنة 1968، كتب إيدسغر دايكسترا (Edsger Dijkstra) رسالة شهيرة بعنوان "عبارة Go to ضارّة" (وانظر أيضًا الشكل التوضيحي). والعيب الرئيس في GOTO أنه يجعل تحليل البرامج أصعب، إذ يجعل البرهنة على الثوابت (invariants) البرنامج أصعب.

عندما يحتوي برنامج على حلقة من الشكل:

for j in range(100):
    do something

do blah

تعلم أن سطر الشيفرة do blah لا يمكن بلوغه إلا إذا انتهت الحلقة، وعندها تعلم أن j يساوي 100100، وقد تتمكّن أيضًا من البرهنة على خصائص أخرى لحالة البرنامج. على النقيض من ذلك، إذا كان البرنامج قد يقفز إلى do blah من أي نقطة أخرى في الشيفرة، فسيكون من الصعب عليك كمبرمج أن تعرف ما يمكنك الاعتماد عليه في هذه الشيفرة. وكما قال دايكسترا، فإن هذه الثوابت مهمّة لأن «قدراتنا الذهنية مهيّأة إلى حدٍّ بعيد لإتقان العلاقات الساكنة، وقدراتنا على تصوّر عمليات تتطوّر مع الزمن متطوّرة نسبيًا»، وبالتالي «علينا... أن نبذل... أقصى جهدنا لتقليص الفجوة المفاهيمية بين البرنامج الساكن والعملية الديناميكية.»

ومع ذلك، ما تزال GOTO جزءًا رئيسيًا من اللغات منخفضة المستوى حيث تُستخدم لتنفيذ بنيات حلقة أعلى مستوى مثل حلقتَي while وfor. فمثلًا، ورغم أن Java ليس فيها تعليمة GOTO، فإن Java Bytecode (وهو تمثيل أدنى مستوى لـJava) يحتوي على تعليمة من هذا النوع. وبالمثل، فإن شيفرة بايثون (Python bytecode) لها تعليمات مثل POP_JUMP_IF_TRUE تنفّذ وظيفة GOTO، وتتضمّن لغات التجميع الكثير تعليمات مشابهة. والطريقة التي نستخدم بها GOTO لتنفيذ وظيفة أعلى مستوى في NAND-TM تذكّرنا بالطريقة التي تُستخدم بها تعليمات القفز المختلفة هذه لتنفيذ بنيات حلقة أعلى مستوى.

/images/introtcs/lec_06_loops-7.webp

التوحيد، وNAND مقابل NAND-TM (نقاش)

رغم أن NAND-TM تضيف عمليات زائدة على NAND-CIRC، فإن من غير الدقيق تمامًا القول إن برامج NAND-TM أو آلات تورينج «أقوى» من برامج NAND-CIRC أو من الدوائر البوليانية. فبرامج NAND-CIRC، لأنها بلا حلقات، لا تنطبق ببساطة على حساب دوال لها عدد غير محدود من المداخل. ولذلك، لحساب دالة F:{0,1}∗:→{0,1}∗F:\{0,1\}^* :\rightarrow \{0,1\}^* باستخدام NAND-CIRC (أو بما يكافئه من الدوائر البوليانية) نحتاج إلى مجموعة (collection) من البرامج/الدوائر: برنامج واحد لكل طول مدخل.

الفرق الجوهري بين NAND-CIRC وNAND-TM هو أن NAND-TM تتيح لنا التعبير عن الحقيقة أن خوارزمية حساب التكافؤات (parities) للسلاسل ذات الطول 100100 هي في الحقيقة الخوارزمية نفسها التي تحسب تكافؤات السلاسل ذات الطول 55 (أو بشكل مماثل، الحقيقة أن خوارزمية جمع الأعداد ذات nn بت هي نفسها لكل nn، إلخ). أي يمكننا التفكير في برنامج NAND-TM للتكافؤ العام على أنه «البذرة» التي ننمو منها برامج NAND-CIRC لتكافؤات الطول 1010، أو الطول 100100، أو الطول 10001000، حسب الحاجة.

وتُعرف هذه الفكرة، وهي فكرة خوارزمية واحدة تستطيع حساب دوال بكل أطوال المداخل، بـ_توحيد_ (uniformity) الحوسبة. ولذلك نعتبر آلات تورينج / NAND-TM نماذج حوسبة موحّدة (uniform)، على النقيض من الدوائر البوليانية أو NAND-CIRC التي هي نماذج غير موحّدة (non-uniform)، يتعيّن فيها أن نحدّد برنامجًا مختلفًا لكل طول مدخل.

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

في الوقت الحالي، ما ينبغي أن تحفظه هو الفروق التالية بين نماذج الحوسبة الموحّدة و_غير الموحّدة_:

  • نماذج الحوسبة غير الموحّدة: من أمثلتها برامج NAND-CIRC و_الدوائر البوليانية_. هذه نماذج يستطيع فيها كل برنامج/دائرة فردي حساب دالة متناهية f:{0,1}n→{0,1}mf:\{0,1\}^n \rightarrow \{0,1\}^m. وقد رأينا أن كل دالة متناهية يمكن حسابها بعض برنامج/دائرة. ولمناقشة حساب دالة لا نهائية F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^* نحتاج إلى السماح بـ_متسلسلة_ (sequence) {Pn}n∈N\{ P_n \}_{n\in \N} من البرامج/الدوائر (واحد لكل طول مدخل)، لكن هذا لا يلتقط فكرة خوارزمية واحدة لحساب الدالة FF.

  • نماذج الحوسبة الموحّدة: من أمثلتها آلات تورينج و_برامج NAND-TM_. هذه نماذج يستطيع فيها برنامج/آلة واحد أن يأخذ مداخل بطول اعتباطي وبالتالي يحسب دالة لا نهائية F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*. عدد الخطوات التي يقطعها البرنامج/الآلة على مدخل ما غير محدّد مسبقًا، وبخاصة أن هناك احتمالًا في أن يدخل في حلقة لا نهائية. وعلى النقيض من الحالة غير الموحّدة، فإننا لم نُبَيِّن أن كل دالة لا نهائية يمكن حسابها ببرنامج NAND-TM/آلة تورينج ما. وسنعود إلى هذه النقطة في الشمولية وغياب قابلية الحساب.

خلاصة

  • آلات تورينج تلتقط فكرة خوارزمية واحدة تستطيع تقييم دوال بكل أطوال المداخل.
  • وهي مكافئة لـ_برامج NAND-TM_، التي تضيف الحلقات والمصفوفات إلى NAND-CIRC.
  • وعلى النقيض من NAND-CIRC أو الدوائر البوليانية، فإن عدد الخطوات التي تقطعها آلة تورينج على مدخل معيّن غير محدّد مسبقًا. وفي الواقع، يمكن لآلة تورينج أو لبرنامج NAND-TM أن يدخل في حلقة لا نهائية على مداخل معيّنة، وألا يتوقّف إطلاقًا.

تمارين

تمرين — البرمجة الصريحة في NAND TM

أنتج شيفرة برنامج NAND-TM PP (خالٍ من السكر النحوي) يحسب دالة الأغلبيّة (Majority) Maj:{0,1}∗→{0,1}Maj:\{0,1\}^* \rightarrow \{0,1\} ذات طول المدخل غير المحدود، حيث لكل x∈{0,1}∗x\in \{0,1\}^*، يكون Maj(x)=1Maj(x)=1 إذا وفقط إذا كان ∑i=0∣x∣xi>∣x∣/2\sum_{i=0}^{|x|} x_i > |x|/2. نقول «أنتج» بدلًا من «اكتب» لأنك لست مضطرًا إلى كتابة شيفرة PP يدويًا، بل يمكنك استخدام لغة البرمجة التي تختارها لحساب هذه الشيفرة.

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

اثبت أن الدوال التالية قابلة للحساب. بالنسبة لكل هذه الدوال، لا يلزمك تحديد آلة تورينج أو برنامج NAND-TM الذي يحسب الدالة كاملًا، بل يكفي أن تثبت أن آلة أو برنامجًا من هذا النوع موجود:

  1. INC:{0,1}∗→{0,1}∗INC:\{0,1\}^* \rightarrow \{0,1\}^* التي تأخذ كمدخل تمثيل عدد طبيعي nn وتُخرج تمثيل n+1n+1.

  2. ADD:{0,1}∗→{0,1}∗ADD:\{0,1\}^* \rightarrow \{0,1\}^* التي تأخذ كمدخل تمثيل زوج من الأعداد الطبيعية (n,m)(n,m) وتُخرج تمثيل n+mn+m.

  3. MULT:{0,1}∗→{0,1}∗MULT:\{0,1\}^* \rightarrow \{0,1\}^*, التي تأخذ تمثيل زوج من الأعداد الطبيعية (n,m)(n,m) وتُخرج تمثيل nm˙n\dot m.

  4. SORT:{0,1}∗→{0,1}∗SORT:\{0,1\}^* \rightarrow \{0,1\}^* التي تأخذ كمدخل تمثيل قائمة من الأعداد الطبيعية (a0,…,an−1)(a_0,\ldots,a_{n-1}) وتُرجع نسختها المرتَّبة (b0,…,bn−1)(b_0,\ldots,b_{n-1}) بحيث لكل i∈[n]i\in [n] يوجد j∈[n]j \in [n] بحيث bi=ajb_i=a_j و b0≤b1≤⋯≤bn−1b_0 \leq b_1 \leq \cdots \leq b_{n-1}.

تمرين — NAND-TM بفهرسين

عرّف NAND-TM' لتكون الصيغة البديلة من NAND-TM التي فيها مُتغيّرا فهرسة (two index variables) هما i وj. يمكن فهرسة المصفوفات بـi أو بـj. وتأخذ العملية MODANDJUMP أربعة متغيّرات a,b,c,da,b,c,d وتستخدم قيمتي c,dc,d لتقرر هل تزيد j أم تنقص j أم تبقيها على القيمة نفسها (المقابلة لـ0101 و1010 و0000 على التوالي). اثبت أن لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون FF قابلة للحساب ببرنامج NAND-TM إذا وفقط إذا كانت FF قابلة للحساب ببرنامج NAND-TM'.

تمرين — آلات تورينج بشريطين

عرّف آلة تورينج بشريطين (two tape Turing machine) لتكون آلة تورينج لها شريطان منفصلان ورأسان منفصلان. وفي كل خطوة، تأخذ دالة الانتقال موضعَ الخليّتين في الشريطين كمدخل، ويمكنها أن تقرر ما إذا كانت تنقل كل رأس على حدة. اثبت أن لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون FF قابلة للحساب بآلة تورينج قياسية إذا وفقط إذا كانت FF قابلة للحساب بآلة تورينج بشريطين.

تمرين — مصفوفات ثنائية البُعد

عرّف NAND-TM" لتكون الصيغة البديلة من NAND-TM التي، تمامًا مثل NAND-TM' المعرَّفة في تمرين — NAND-TM بفهرسين، فيها متغيّرا فهرسة i وj، لكن المصفوفات الآن ثنائية البُعد (two dimensional)، ولذلك نفهرس المصفوفة Foo بـFoo[i][j]. اثبت أن لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون FF قابلة للحساب ببرنامج NAND-TM إذا وفقط إذا كانت FF قابلة للحساب ببرنامج NAND-TM''.

تمرين — آلات تورينج ثنائية البُعد

عرّف آلة تورينج ثنائية البُعد (two-dimensional Turing machine) لتكون آلة تورينج يكون شريطها ثنائي البُعد. وفي كل خطوة يمكن للآلة أن تتحرك U\mathsf{U}p (لأعلى)، أو D\mathsf{D}own (لأسفل)، أو L\mathsf{L}eft (إلى اليسار)، أو R\mathsf{R}ight (إلى اليمين)، أو S\mathsf{S} (تبقى مكانها). اثبت أن لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون FF قابلة للحساب بآلة تورينج قياسية إذا وفقط إذا كانت FF قابلة للحساب بآلة تورينج ثنائية البُعد.

تمرين

اثبت خصائص الإغلاق التالية للمجموعة R\mathbf{R} المعرَّفة في تعريف — الصنف R:

  1. إذا كان F∈RF \in \mathbf{R} فإن الدالة G(x)=1−F(x)G(x) = 1 - F(x) تكون في R\mathbf{R}.

  2. إذا كان F,G∈RF,G \in \mathbf{R} فإن الدالة H(x)=F(x)∨G(x)H(x) = F(x) \vee G(x) تكون في R\mathbf{R}.

  3. إذا كان F∈RF \in \mathbf{R} فإن الدالة F∗F^* تكون في R\mathbf{R} حيث F∗F^* معرَّفة كما يلي: F∗(x)=1F^*(x)=1 إذا وفقط إذا وُجدت سلاسل w0,…,wk−1w_0,\ldots,w_{k-1} بحيث x=w0w1⋯wk−1x = w_0 w_1 \cdots w_{k-1} وF(wi)=1F(w_i)=1 لكل i∈[k]i\in [k].

  4. إذا كان F∈RF \in \mathbf{R} فإن الدالة G(x)={∃y∈{0,1}∣x∣F(xy)=10otherwise G(x) = \begin{cases} \exists_{y \in \{0,1\}^{|x|}} F(xy) = 1 \\ 0 & \text{otherwise} \end{cases} تكون في R\mathbf{R}.

تمرين — آلات تورينج «الغافِلة» (صعبة)

عرّف آلة تورينج MM بأنها غافِلة (oblivious) إذا كانت تحركات رأسها مستقلة عن مدخلها. أي نقول إن MM غافِلة إذا وُجدت متسلسلة لا نهائية MOVE∈{L,R,S}∞MOVE \in \{\mathsf{L},\mathsf{R}, \mathsf{S} \}^\infty بحيث لكل x∈{0,1}∗x\in \{0,1\}^*، تكون تحركات MM عند إعطائها المدخل xx (حتى النقطة التي تتوقّف عندها، إن وُجدت) هي MOVE0,MOVE1,MOVE2,…MOVE_0,MOVE_1,MOVE_2,\ldots.

اثبت أن لكل دالة F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، إذا كانت FF قابلة للحساب فهي قابلة للحساب بآلة تورينج غافِلة. انظر الحاشية للدلالة.[3]

تمرين — بتّ واحد مقابل عدة بتّات

اثبت أن لكل F:{0,1}∗→{0,1}∗F:\{0,1\}^* \rightarrow \{0,1\}^*، تكون الدالة FF قابلة للحساب إذا وفقط إذا كانت الدالة التالية G:{0,1}∗→{0,1}G:\{0,1\}^* \rightarrow \{0,1\} قابلة للحساب، حيث GG معرَّفة كما يلي: G(x,i,σ)={F(x)ii<∣F(x)∣,σ=01i<∣F(x)∣,σ=10i≥∣F(x)∣G(x,i,\sigma) = \begin{cases} F(x)_i & i < |F(x)|, \sigma =0 \\ 1 & i < |F(x)|, \sigma = 1 \\ 0 & i \geq |F(x)| \end{cases}

تمرين — عدم القابلية للحساب بالعدّ

تذكّر أن R\mathbf{R} هي مجموعة كل الدوال الكلّية من {0,1}∗\{0,1\}^* إلى {0,1}\{0,1\} القابلة للحساب بآلة تورينج (انظر تعريف — الصنف R). اثبت أن R\mathbf{R} قابلة للعدّ (countable). أي، اثبت أن وُجدت دالة أحادية التطابق DtN:R→NDtN:\mathbf{R} \rightarrow \mathbb{N}. يمكنك استخدام التكافؤ بين آلات تورينج وبرامج NAND-TM.

تمرين — ليست كل الدوال قابلة للحساب

اثبت أن مجموعة كل الدوال الكلّية من {0,1}∗→{0,1}\{0,1\}^* \rightarrow \{0,1\} ليست قابلة للعدّ. يمكنك استخدام نتائج مبرهنة كانتور، والمجموعات القابلة للعدّ، وتمثيلاتُ الأعداد الحقيقية في سلاسل. (وسنرى دالة صريحة غير قابلة للحساب في الشمولية وغياب قابلية الحساب.)

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

أوغستا آدا بايرون، كاونتيسة لوفلايس (1815-1852) عاشت حياة قصيرة لكنها مضطربة، غير أنها معروفة اليوم في الأغلبية العظمى بفضل تعاونها مع تشارلز بابيج (انظر [@stein1987ada] لسيرة ذاتية). أبدت آدا اهتمامًا هائلًا بمحرك بابيج التحليلي، الذي ذكرناه في تعريف الحوسبة. وفي سنة 1842-3، ترجمت من الإيطالية ورقة لمينابريا عن المحرك، مضيفةً ملاحظات وفيرة (أطول من الورقة نفسها). والاقتباس في بداية الفصل مأخوذ من الملاحظة A في هذا النص. تحتوي ملاحظات لوفلايس على عدة أمثلة لبرامج للمحرك التحليلي، ولهذا السبب سُمّيت «أول مبرمجة حاسوب في العالم»، وإن كان من غير الواضح ما إذا كانت هذه الأمثلة قد كُتبت من لوفلايس أو من بابيج نفسه [@holt2001ada]. وعلى أي حال، كانت آدا بوضوح إحدى القلائل جدًا (ربما الوحيدة خارج بابيج نفسه) التي أدركت تمامًا مدى أهمية فكرة مكننة الحوسبة وثوريتها.

تناقش كتابا شيتري [@shetterly2016hidden] وسوبل [@sobel2017the] تاريخ «الحاسوبين البشريين» (كانوا نساء في أغلب الأحيان) ومساهماتهم المهمّة في الاكتشافات العلمية في الفلك واستكشاف الفضاء.

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

يعرّف نصّ سيبسر [@SipserBook] آلة تورينج على أنها سباعية (seven tuple) تتكوّن من فضاء الحالات، وأبجدية المدخل، وأبجدية الشريط، ودالة الانتقال، وحالة البدء، وحالة القبول، وحالة الرفض. وفي الظاهر يبدو هذا تعريفًا مختلفًا جدًا عن تعريف — آلة تورينج، لكنه ببساطة تمثيل مختلف للمفهوم نفسه، تمامًا كما يمكن تمثيل الرسم البياني إمّا بقائمة تجاور أو بمصفوفة تجاور.

أحد الفروق أن سيبسر يتناول مجموعة عامة من الحالات QQ ليست بالضرورة من الشكل Q={0,1,2,…,k−1}Q=\{0,1,2,\ldots, k-1\} لبعض عدد طبيعي k>0k>0. ويقتصر سيبسر أيضًا انتباهه على آلات تورينج التي تُخرج بتًّا واحدًا فقط، ولذلك يسمّي حالتَي توقّف خاصتين: «حالة التوقّف 0» (وهي المعروفة غالبًا بـ_حالة الرفض_) والأخرى «حالة التوقّف 1» (وهي المعروفة غالبًا بـ_حالة القبول_). وبذلك، بدلًا من كتابة 00 أو 11 على شريط خرج، تدخل الآلة في إحدى هاتين الحالتين وتتوقّف. وهذا أيضًا لا يُحدث فرقًا في القدرة الحاسوبية، وإن كنا نفضّل النموذج الأكثر عمومية ذي المخارج متعددة البتّات. (يقدّم سيبسر المهمة الأساسية لآلة تورينج على أنها قرار لغة بدلًا من حساب دالة، لكنهما متكافئتان، انظر ملاحظة — الدوال مقابل اللغات.)

ويتناول سيبسر أيضًا دوال مدخلها في Σ∗\Sigma^* لأبجدية اعتباطية Σ\Sigma (وعليه فإنما يميّز بين أبجدية المدخل التي يسمّيها Σ\Sigma و_أبجدية الشريط_ التي يسمّيها Γ\Gamma)، في حين أننا نقتصر على الدوال التي مدخلها سلاسل ثنائية. ومرة أخرى، هذه ليست مسألة كبيرة، يمكننا دائمًا ترميز عنصر من Σ\Sigma باستخدام سلسلة ثنائية من الطول log⁡⌈∣Σ∣⌉\log \ceil{|\Sigma|}. وأخيرًا (وهذه نقطة صغيرة جدًا) يشترط سيبسر على الآلة أن تتحرك إلى اليسار أو إلى اليمين في كل خطوة، دون عملية S\mathsf{S} (البقاء في المكان)، مع أن البقاء في المكان سهل جدًا في المحاكاة بمجرد الحركة إلى اليمين ثم العودة إلى اليسار.

ومن التعريفات الأخرى المستخدمة في الأدبيات أن آلة تورينج MM تتعترف (recognizes) بلغة LL إذا كان لكل x∈Lx\in L، M(x)=1M(x)=1 ولكل x∉Lx\not\in L، M(x)∈{0,⊥}M(x) \in \{0,\bot \}. وتُسمّى اللغة LL قابلة للتعداد تعاوديًا (recursively enumerable) إذا وُجدت آلة تورينج MM تتعرف عليها، وغالبًا ما تُرمز مجموعة كل اللغات القابلة للتعداد تعاوديًا بـRE\mathbf{RE}. لن نستخدم هذا المصطلح في هذا الكتاب.

ومن أوائل الصياغات لآلات تورينج بلغة برمجة ما قدّمه وانغ [@Wang1957]. وتستهدف صياغتنا لـNAND-TM جعل الصلة بالدوائر أكثر مباشرة، بهدف استعمالها لاحقًا في مبرهنة كوك-ليفن، وكذلك في نتائج مثل P⊆P/poly\mathbf{P} \subseteq \mathbf{P_{/poly}} و BPP⊆P/poly\mathbf{BPP} \subseteq \mathbf{P_{/poly}}. يضم الموقع esolangs.org تنوّعًا كبيرًا من لغات البرمجة الغامضة التورينجية الكاملة. ومن أشهرها على الإطلاق Brainf*ck.


  1. الدالة الجزئية FF من مجموعة AA إلى مجموعة BB هي دالة معرَّفة على مجموعة جزئية من AA فقط (انظر الدوال). يمكننا أيضًا التفكير في مثل هذه الدالة على أنها تُسقط AA على B∪{⊥}B \cup \{ \bot \} حيث ⊥\bot رمز «فشل» خاص بحيث F(a)=⊥F(a)=\bot يدل على أن الدالة FF غير معرَّفة على aa. ↩︎

  2. معظم لغات البرمجة تستخدم مصفوفات ذات حجم ثابت، في حين أن شريط آلة تورينج غير محدود. لكن بالطبع ليس هناك حاجة لتخزين عدد لا نهائي من رموز ∅\varnothing. وإذا شئت، يمكنك التفكير في الشريط على أنه قائمة تبدأ بطول كافٍ لتخزين المدخل فقط، لكنها تنمو ديناميكيًا مع استكشاف رأس آلة تورينج لمواضع جديدة. ↩︎

  3. يمكنك استخدام المتسلسلة R\mathsf{R}، L\mathsf{L}،R\mathsf{R}، R\mathsf{R}، L\mathsf{L}، L\mathsf{L}، R\mathsf{R}،R\mathsf{R}،R\mathsf{R}، L\mathsf{L}، L\mathsf{L}، L\mathsf{L}، …\ldots. ↩︎