النص الكامل للمحاضرة 15: العودية
النص الكامل للمحاضرة 15: العودية
المصدر الأصلي: النص الرسمي الإنجليزي، PDF، صفحة المحاضرة، صفحة الفيديو الرسمي.
النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة وتكييف عربي غير رسمي، لا تعتمده MIT ولا يعني نشره تأييدها له. الاستخدام غير تجاري، وتُنشر الترجمة والمواد الأصلية المشمولة وفق رخصة CC BY-NC-SA 4.0، مع وجوب المشاركة بالمثل. لا تشمل الرخصة الافتراضية مواد الأطراف الثالثة المستثناة.
منهج الترجمة: المصدر هو النص المستخرج من PDF الرسمي، لا ملخص الشرائح. تتبع الترجمة فقراته بالترتيب، مع التكرار والتردد والتصحيح الشفهي ومداخلات الحضور والإشارات الصوتية. عناوين «صفحة المصدر» تتبع الصفحات غير الفارغة المفصولة في الاستخراج بمحرف فصل الصفحات، وليست تقسيمًا موضوعيًّا. تُحفظ أسماء الكود والتعبيرات، وتُميَّز توضيحات المترجم عن كلام المصدر؛ ولا تُصحَّح زلات المصدر صامتًا.
صفحة المصدر 1
MITOCW | المحاضرة 15 من 6.100L، الإصدار 2.mp4
[صرير]
[حفيف]
[نقرات]
آنا بيل (ANA BELL):
حسنًا، لنبدأ محاضرة اليوم. اليوم سنقدّم واحدة من محاضرتين عن موضوع العودية (Recursion). وربما سمعتم بالعودية وربما لم تسمعوا بها. إنها تقنية برمجية وطريقة لحل المسائل خوارزميًّا. لن تأتي بسهولة، لأنها ستجبر عقولنا على التفكير في مسائل سبق أن رأيناها بطريقة مختلفة تمامًا.
لا يلزمكم استخدام العودية إن لم ترغبوا في ذلك، لكن ستكون هناك مسائل تأتي فيها فكرة العودية وتطبيق — أو كتابة كود عودي — بصورة طبيعية أكثر بكثير من كتابة الكود بالطريقة التي اتبعناها حتى الآن. لكنني أنبّهكم فقط: سنحتاج إلى أن ننسى قليلًا كل ما تعلّمناه عن الحلقات وما شابه، حتى ندرّب عقولنا على التفكير عوديًّا خلال المحاضرتين القادمتين.
ولمساعدتكم، سيكون لدينا جزء تفاعلي في محاضرة اليوم. ففكّروا إن كنتم تريدون الصعود إلى المنصة، أو أيًّا كان اسم هذا المكان، إلى المقدمة، والمشاركة في التفاعل. ستُخلَّدون إلى الأبد على OpenCourseWare — رائع، يعجبني هذا — في فيديوهات OpenCourseWare.
حسنًا، لنفكّر في الخوارزميات التكرارية (Iterative Algorithms) التي رأيناها حتى الآن. الخوارزمية التكرارية تعني أساسًا أننا نكتب كودًا فيه حلقة، إما حلقة for أو حلقة while. كتابة الكود بهذه الحلقات، for أو while، تؤدي إلى خوارزميات تكرارية، أي أشياء تنفّذ مهمة ما عددًا من المرات.
فكرة الخوارزمية التكرارية هي أن بعض المتغيرات تسجّل حالة الحساب (State of the Computation). في كل مرور عبر الحلقة، تتغيّر قيم هذه المتغيرات، فتسجّل أساسًا القيم عند كل خطوة في الحلقة. وعندما نكتب هذه الخوارزميات التكرارية، نفكّر أساسًا: ما الشيء الذي يتغيّر في كل مرور عبر الحلقة؟ مثل الاحتفاظ بمجموع تراكمي، وهذا أبسط مثال.
ما المتغير الذي يتغيّر في كل مرور عبر الحلقة، مثل عدّاد يتتبّع عدد المرات التي مررنا فيها عبرها؟ ومتى تتوقف؟ في حلقات for، تتوقف بعد استنفاد تسلسل (Sequence). وفي حلقات while، تتوقف عندما يصبح شرط ما خطأ. ثم في نهاية الحلقة يكون لديك نوع من النتائج، كنت تخزّنه وتراكمه أو تغيّره في كل مرور عبر الحلقة. هذه إذًا خوارزمية تكرارية، وقد عملنا عليها كثيرًا.
ولأريكم ذلك — سنمرّ في الشرائح القليلة القادمة بخوارزمية تكرارية لإجراء الضرب. ستكون بسيطة جدًّا، جدًّا. لكننا بعد ذلك سننظر أيضًا إلى المسألة نفسها، وهي إجراء الضرب، في سياق العودية. وآمل أن يعطيكم ذلك إحساسًا بكيفية التفكير في المسألة نفسها تمامًا التي نحاول حلّها، وهي ضرب عددين، بطريقة مختلفة تمامًا.
هذه ليست الدالة التي أريد كتابتها باستخدام التكرار. لا أريد إنشاء دالة اسمها mult ثم إرجاع a*b. لا أريد استخدام الدالة المدمجة (Built-in Function). أريد أن أفترض أنني لا أعرف كيف أنفّذ النجمة، أي الضرب. وبدلًا من ذلك، سأعتمد على — لنقل إنني أعرف كيف أجمع. سأعتمد على فكرة الجمع لكتابة دالة الضرب فعلًا.
صفحة المصدر 2
لنفكّر كيف نجعل الضرب تكراريًّا. يمكن أن تكون لدينا حلقة تضيف a إلى نفسه b مرة. هذا هو تعريف الضرب. فلنكتب دالة تفعل هذا باستخدام حلقة for، ثم نكتبها باستخدام حلقة while. باستخدام حلقة for سنكتب هذه الخوارزمية التكرارية.
إنها تسجّل حالة الحساب، تمامًا كما قلنا إنها ينبغي أن تفعل. ستتكرر حلقة for — سيكون نطاق القيم (Range) عندي من 0 إلى b. لذلك سنكرر هذه الحلقة b مرة. المتغير total يسجّل حالة الحساب عندي؛ فهو يتتبّع المجموع عند كل خطوة في الحلقة. وفي نهاية الحلقة أُرجع المجموع. إذًا لدينا هنا دالة تكرارية بسيطة جدًّا، جدًّا.
والآن لنفكّر في حل تكراري آخر. بدلًا من الاحتفاظ بمتغير حلقة b يمتد من 0 وصولًا إلى b — أو ما كان متغير الحلقة عندي؟ n، على ما أظن. نعم، بدلًا من الاحتفاظ بمتغير حلقة n يمتد من 0 إلى b، فلنعمل بالعكس. وهذه المرة لنستخدم حلقة while لمجرد التنويع. لنقل إنني سأبدأ عند b وأعدّ تنازليًّا إلى 0.
مرة أخرى، نمضي ونكرر مهمة ما b مرة. سأجعل لدي عدّادًا يبدأ عند b وينقص وصولًا إلى 0. ومرة أخرى، داخل الحلقة، يجب أن أتتبّع النتيجة. فالمتغير total في الكود السابق أصبح اسمه result في هذا الكود.
وسأجعل التكرار يبدأ عند 0 مباشرة. ثم سأواصل إضافة a إلى نفسه b مرة. فيبدو الكود هكذا. لدي حلقة while هذه المرة بدلًا من حلقة for. سأبدأ وأنا أعرف ما قيمة b، وسأنقص b بواحد في كل مرور عبر الحلقة. هنا لدي b يساوي b ناقص 1. وهذا يسجّل حالة العدّاد في كل تكرار.
والمتغير result، تمامًا مثل total في الشريحة السابقة، يسجّل حالة المجموع في كل مرة خلال التكرار. وفي النهاية أُرجع result. آمل أن يكون هذا كودًا بسيطًا جدًّا، ومراجعة محضة. لكن لننظر الآن إلى الكود من منظور عودي. هنا، دعونا لا ننظر إلى الكود بعد. بل لنفكّر: هل هناك شيء نكرره مرارًا ومرارًا ومرارًا؟
إذا تعرّفنا عليه، نستطيع التفكير عوديًّا. حسنًا، فلنحاول اكتشاف هذا النمط العودي (Recursive Pattern). أنا أعمل بأفضل صورة مع الأمثلة، مثل الأعداد الفعلية. فبدلًا من استخدام a وb المجردين، لنستخدم a يساوي 5 وb يساوي 4 مثالًا. لنقل إنني أريد استخدام معامل النجمة (Star Operator) — وهو أساسًا الدالة التي أحاول تنفيذها — معامل النجمة بين 5 و4.
بالمعنى التكراري، قلنا إن ذلك يساوي 5 زائد 5 زائد 5 زائد 5، أي جمع 5 أربع مرات. وفكرة العودية هي أننا نحاول أخذ مسألتنا الأصلية، وهي استخدام معامل النجمة بين عددين، ومحاولة حل مسألة مشابهة جدًّا، إن لم تكن نفسها، لكن بطريقة تغيّرت قليلًا.
فبدلًا من القول إنني سأضرب 5 في 4، سألاحظ أن 5 في 4، وهي مسألتي الأصلية، يمكن إعادة كتابتها بإخراج إحدى الخمسات. سأخرج 5 وأضيفها إلى 5 في 3. هذا هو النمط العودي عندي. أستخدم معامل النجمة بين 5 وعدد ما.
لكن إذا أخرجت 5، أستطيع استخدام معامل النجمة بين 5 وعدد أصغر قليلًا، أقل بواحد من 4. حسنًا، ماذا لو فعلت — ماذا عن 5 في 3؟ هل أستطيع فعل الشيء نفسه مجددًا؟ أستطيع، صحيح؟ بالنسبة إلى 5 في 3، يمكنني مرة أخرى ملاحظة أنني أستطيع إخراج 5 مجددًا. ويصبح لدي 5 زائد 5 في 2. ثم أستطيع فعل الشيء نفسه مجددًا لمعرفة ما قيمة 5 في 2؛ أخرج 5 مرة أخرى ويتبقى لي 5 في 1، أو 5*1.
صفحة المصدر 3
لاحظوا أن الشيء الموجود داخل الإطار هو أساسًا حلّي لمسألتي الأصلية، وهي استخدام معامل النجمة بين 5 وعدد ما. لكن ذلك العدد يتغيّر في كل مرة، في كل سطر. عند نقطة ما أستطيع القول إن هذه المسألة سهلة إلى درجة أنني أعرف الإجابة. فـ5*1، أي عدد مضروب في واحد، هو العدد نفسه فقط. وعند تلك النقطة أستطيع القول إنني لا أحتاج إلى مواصلة تقسيم المسألة إلى أجزاء أصغر فأصغر.
ولتأكيد الفكرة فقط، لنستخدم الأقواس لتوضيح الأجزاء التي أستبدلها وأين أستبدلها. لدي مسألتي الأصلية، تطبيق معامل النجمة في ضرب 5 و4. أخرج 5، وألاحظ أنني أستطيع أن أحصل على 5 زائد، ثم حل 5*3.
أحتاج هنا إلى بعض الثقة، أليس كذلك؟ لا أعرف ما قيمة 5*3. لكن إذا فكّكت تلك المسألة بالطريقة نفسها تمامًا، أستطيع إخراج 5 منها وإضافتها إلى 5*2. إذًا الأشياء الموجودة داخل الإطارات متكافئة. ومرة أخرى، بالنسبة إلى 5*2، سألاحظ أنها المسألة نفسها التي كنت أحاول حلّها. فلنطبّق الحل نفسه، وهو إخراج 5 وإضافتها إلى ضرب 5، نجمة، عدد أقل بواحد. إذًا الإطاران متكافئان مرة أخرى.
هذه الفكرة هنا، حيث نتعرّف على المسألة نفسها ونقسّمها، ونقسّمها، ونقسّمها، مع الثقة بأننا سنقسّمها عند نقطة ما إلى حد بلوغ حقيقة أساسية نستطيع حلّها، هي خطوة التقسيم (Divide Step). سنقسّمها حتى نصل — حتى ننزل إلى هنا، حيث لدي 5 زائد 5*1. عند هذه النقطة أستطيع القول: حسنًا، 5*1 سيساوي 5. هذه حقيقة أساسية أستطيع حلّها مباشرة.
لا أحتاج إلى تقسيم هذه المسألة أكثر من ذلك. وبمجرد حل هذه الحقيقة، أستطيع البدء في إعادة بناء إجابتي. ويمكنني البدء في تمرير الإجابة إلى أعلى سلسلة استدعاءات الضرب (Multiplication Calls). فإذا كنت عند هذه الخطوة هنا، واكتشفت أن هذا هو 5*1 — وأن 5*1 هذا يساوي 5، أستطيع استبداله بـ5. ثم أستطيع بناء حل 5*2، لأن 5*2 الآن ليس إلا 5 زائد 5.
إذًا سيكون هذا 10. إنه جمع بسيط فقط. لم يعد هناك ضرب، وهو الشيء الذي كنّا نحاول تجنّبه. ثم يُستبدل بـ5*2 العدد 10. وما زلت أعيد بناء الحل حتى أصل إلى 5*4. كنت أحاول معرفة ما قيمة 5*3، لكن قبل أن أستطيع فعل ذلك، كان عليّ حل بقية السطرين تحته. أما الآن فأستطيع أخيرًا حلّه. إنه 5 زائد 10 فقط. هذه هي المسألة المشابهة التي كنت أحاول حلّها.
فأستطيع استبدال 5*3 بـ15. وأخيرًا، كانت مسألتي الأصلية هي محاولة معرفة 5*4، والآن أستطيع أخيرًا حلّها، لأنني أعدت أخيرًا بناء الحل في صورة 5 زائد 15. هل هناك أسئلة عن هذه الخطوات؟ ينبغي أن تكون مباشرة إلى حدٍّ ما. أعرف أنها طريقة غريبة وملتوية لمعرفة الإجابة، لكن ما أحاول الوصول إليه هو محاولة التعرّف على المسألة التي نحاول حلّها، ثم حل مسألة مشابهة جدًّا تغيّرت قليلًا فقط.
في هذه الحالة، نضرب 5، نجمة، عددًا أقل بواحد مما كنّا نحاول معرفته للتو. وبالنسبة إلى العودية في هذه المسألة المحددة، ضرب a في b، نلاحظ أن a*b سيكون a زائد a زائد a زائد a، عدد b من المرات. إذا أخرجنا a، تمامًا كما أخرجنا 5 وأضفناها إلى شيء آخر، سنلاحظ أن ذلك ليس إلا a زائد a زائد a زائد a زائد a زائد a، عدد b ناقص 1 من المرات.
حسنًا، تلك الـa زائد a زائد a، عدد b ناقص 1 من المرات، هي فقط مسألتنا الأصلية: ضرب a في b ناقص 1. هذه إذًا خطوتي العودية (Recursive Step). نلاحظ أن a*b يساوي a زائد a في b ناقص 1. فأنا أستخدم العملية نفسها التي أحاول إيجادها هنا، في الأسفل هنا، في «الحل»، بين علامتي اقتباس.
ملاحظة المترجم: عبارة «
aفيbناقص 1» في هذا الشرح تشير إلىa*(b-1)، لا إلىa*b-1؛ أي إن العدد الثاني في الضرب هو الذي ينقص بواحد.
صفحة المصدر 4
لكن هذه ليست نهاية العودية، لأنني إذا جعلت هذا وحده تعريفي، فستكون لدي عودية لا نهائية (Infinite Recursion). ليس لدي سبيل للتوقف. وهذه الخطوة العودية، مع حالة أساس (Base Case)، أي شيء نعرفه معرفة أساسية عن معامل النجمة، ستعطينا الحل. عرفنا في الشريحة السابقة أننا عندما نضرب a في 1، نحصل على a فقط. لذلك فإن حالة الأساس لدينا، وهي حالة بسيطة جدًّا من ضرب a وb، ستكون عندما يكون b هو 1. وعندها يكون a في b مساويًا لـa.
قد تبدو هذه مثل التعريفات الرياضية التي يمكنكم التوصل إليها في درس رياضيات. وهي عندنا هنا. فإذا لم يكن b مساويًا لـ1، فإن a في b هو a زائد a في b ناقص 1. ثم حالة الأساس هي عندما يساوي b العدد 1، يكون a في b مساويًا لـa. بهذين السطرين نستطيع فعلًا التوصل إلى الكود، النسخة البرمجية لهذه الدالة.
هنا ننشئ دالة اسمها mult_recur، وستكون معاملاتها (Parameters) هي a وb. فأنا أضرب a في b. وعليّ ترميز هاتين الحالتين: عندما يكون b هو 1، وفي غير ذلك. عادة نبدأ بحالة الأساس؛ فهي الأبسط في التفكير. عندما يكون b هو 1، فإن a في b يساوي a. فإذا كان b مساويًا لـ1، فما قيمة a في b؟ إنها a فقط، صحيح؟ فبوسع الدالة أن تُرجع a فورًا.
هذه هي حالة الأساس لدينا. أما else، فستكون خطوتنا العودية. لن نُرجع a، بل سنُرجع هذا: a زائد a في b ناقص 1. حسنًا، الـa هي مجرد a زائد، وهذا، معامل النجمة بين a وb ناقص 1، هو الدالة مرة أخرى. أليس هذا رائعًا حقًّا؟ نستخدم اسم الدالة داخل جسم هذه الدالة التي نعرّفها.
وهذا ليس مشكلة، لأن المعامل للدالة الموجودة في الأسفل، في الخطوة العودية، يتغيّر. لا أستدعي mult_recur مع a, b مرة أخرى. سيكون ذلك تصرفًا سخيفًا جدًّا مني، لأنه سيؤدي إلى عودية لا نهائية. لن أحرز أي تقدّم نحو حالة أساس. لكنني أستدعيها مع b ناقص 1. وستواصل هذه الدالة استدعاء mult_recur مع a، ومع b، وb ناقص 1، وb ناقص 2، وb ناقص 3، وهكذا، حتى تصل إلى b يساوي 1. وبعد ذلك ستعيد بناء الحل، تمامًا كما رأينا في الشرائح مع كل الأقواس التي كنّا نستبدلها.
حسنًا، لنتتبّع الخطوات في Python Tutor، وسأريكم كيف يبدو الأمر فعلًا عندما نجري كل هذه الاستدعاءات للدوال (Function Calls). ثم سنقدّم مثالًا آخر. هنا لدي mult_recur مع a يساوي — أظن أنني شغّلتها مع 5 و4، تمامًا مثل المثال الذي كنّا ننظر إليه. ستكون هذه دالتي الرئيسية. إنها تستدعي mult_recur — [تسعل] أوه، عذرًا — مع 5 و4. إذًا a عندي هو 5، وb عندي هو 4.
هذا الشيء الأزرق الصغير هنا هو بيئة دالة واحدة (Function Environment). مثلما أرسم إطارات برتقالية في شرائحي، هم يرسمونها بالأزرق. حسنًا، الآن، في استدعاء هذه الدالة، ماذا نفعل؟ هل b هو 1؟ لا. إذًا ندخل حالة else، ونُرجع 5 زائد — ماذا يحدث بعد ذلك؟ هل يعرف أحد؟ نعم.
أحد الحضور (AUDIENCE):
ستعمل mult_recur مرة أخرى مع a يساوي 5 وb يساوي 3.
آنا بيل:
نعم، ستعمل mult_recur مرة أخرى مع a يساوي 5 وb يساوي 3، بالضبط. إنه استدعاء دالة، صحيح؟ وبصفته استدعاء دالة، سننشئ بيئة جديدة. فهنا، ها هو، إطار آخر. إطاري السابق معلّق حاليًّا. لا يستطيع الانتهاء، لأنه يحاول معرفة ما قيمة 5 زائد نتيجة استدعاء هذه الدالة. لكن هذه لم تنتهِ بعد؛ فما زالت تحاول معرفة نتيجتها.
لذلك وضعنا ذلك الاستدعاء في الانتظار. والآن نحاول معرفة mult_recur مع 5 و3. حسنًا، ما قيمة mult_recur مع 5 و3؟ سيكون b غير مساوٍ لـ1. إذًا هذه ستدخل أيضًا فرع else عندها. وستتكرر عوديًّا — ستُرجع 5 زائد ماذا؟ بالضبط، استدعاء الدالة عندما يصبح b هو 2، بالضبط. لكن لاحظوا أنه استدعاء دالة آخر، صحيح؟ فهنا لدي، ها هو، إطار آخر.
صفحة المصدر 5
لدي الآن استدعاءا دالة، هذا الأصلي هنا في الخلف، الذي كان ينتظر هذا الذي ظلّلته هنا. لكن هذا الذي ظلّلته هنا اضطر الآن إلى إجراء استدعاء دالة آخر هنا في الأسفل. لذلك لدي حاليًّا ثلاثة استدعاءات دوال قيد التنفيذ، تحاول معرفة نتائجها.
حسنًا، أخيرًا، هذا الاستدعاء mult_recur مع 5 و2 سيجري استدعاء دالة آخر. قيمة b عنده ليست واحدًا. لذلك سندخل فرع else. وماذا سيقول فرع else عنده؟ سنُرجع 5 زائد — وهو استدعاء دالة آخر. الآن وصلت إلى عمق أربعة استدعاءات دوال، ولم أفعل أي نوع من العمل الظاهر. ظللت فقط أؤجّل الأمر إلى الخطوة التالية لمحاولة معرفة القيم. والجميع ينتظر أن يُرجع شخص آخر قيمة أخيرًا. فهذا الأول ينتظر أن يُرجع الذي تحته مباشرة قيمة. لكن هذا ينتظر أن يُرجع الذي تحته قيمة. وهذا ينتظر أن يُرجع الذي تحته قيمة. تلك هي سلسلة الاستدعاءات (Chain of Calls).
ماذا سيفعل هذا الأخير؟ هل سينشئ بيئة دالة أخرى؟ لا، سيُرجع a، وهي 5. حسنًا، ها هي قيمة الإرجاع (Return Value) عندي، 5. سيُرجع هذا الـ5 إلى من استدعاه. والذي استدعاه هو هذا هنا، mult_recur مع 5 و2. الاستدعاء مع 5 و2 كان يحاول معرفة ما قيمة 5 زائد استدعاء الدالة هذا في الأسفل.
حسنًا، الآن يستطيع معرفة أنها ستكون 5 زائد 5. فستكون قيمة إرجاعه 10. هذا يُرجع قيمة إلى المستوى الأعلى، إلى من استدعاه. وكان ذلك mult_recur مع 5 و3. والآن تستطيع mult_recur مع 5 و3 إنهاء عملها. كانت تحاول معرفة ما قيمة 5 زائد استدعاء دالتها، الذي اكتشفنا أنه 10. لذلك يستطيع هذا معرفة أنها 15. وأخيرًا، يمكن أن تعود هذه القيمة الأخيرة إلى أعلى، إلى استدعاء الدالة الأصلي، مع 5 و4. والاستدعاء مع 5 و4 سيُرجع 5 زائد الـ15 التي أُرجعت، أي 20.
حسنًا، هل هناك أسئلة عما حدث للتو؟ هل كل شيء واضح؟ حسنًا. لننظر إلى مثال آخر. أعني، سننظر إلى بضعة أمثلة أخرى في هذه المحاضرة، لكن لننظر إلى مثال من العالم الحقيقي الآن. آمل أن يوضح هذا الفرق بين الخوارزميات التكرارية والخوارزميات العودية (Recursive Algorithms) في وضع أقرب إلى الحياة الواقعية.
لنفترض أنه في هذا الوضع الواقعي، يطلب طالب إعادة تصحيح (Regrade) اختبار. في الوضع التكراري، لدينا أساسًا استدعاء دالة واحد، regrade، أو أي اسم تريدون تسميتها به. ها هو الطالب. كيف سيحصل الطالب على إعادة التصحيح تكراريًّا؟ حسنًا، سيكون الطالب مسؤولًا أساسًا عن المرور في حلقة على كل فرد من طاقم التدريس.
يذهب الطالب إلى المدرّسة ويقول: هل يمكنني الحصول على إعادة تصحيح، من فضلك؟ ربما صحّحت المدرّسة مسألة واحدة. وربما لم تفعل، لكنها ستعيد تصحيح المسألة التي ربما كانت مسؤولة عنها. ثم يذهب الطالب إلى الشخص التالي في الطاقم، مساعد التدريس (TA). هل يمكنني الحصول على إعادة تصحيح، من فضلك؟ لنقل إن مساعد التدريس ربما يعيد تصحيح المسألة التي كان مسؤولًا عنها. وربما لم يصحّحها، لكن في كل الأحوال سيعيد الدرجة، أو سيجيب عن طلب الطالب.
ثم يذهب الطالب إلى الشخص التالي في الطاقم، مساعد المختبر (Lab Assistant). هل يمكنني الحصول على إعادة تصحيح، من فضلك؟ قد يعيد مساعد المختبر تصحيح المسألة التي هو مسؤول عنها، أو أيًّا كان، ويعيد الدرجة إلى الطالب. والطالب يتتبّع كل درجات إعادة التصحيح التي يحصل عليها ليعرف ما مجموعه الجديد. وأخيرًا، قد يذهب الطالب إلى المصحّح (Grader)، الذي قام على الأرجح بمعظم العمل، ويطلب إعادة التصحيح. وسيوافق المصحّح ملتزمًا بواجبه على إعادة التصحيح وإرجاع القيم.
صفحة المصدر 6
لاحظوا هنا أن الطالب مسؤول عن الذهاب تكراريًّا إلى كل شخص في الطاقم والحصول على النتيجة منه. والطالب يتتبّع درجته الجديدة. بالطبع سيفعل أفراد الطاقم ذلك أيضًا لأغراض إسناد الدرجات، لكن الطالب يفعل ذلك كذلك. فالطالب أساسًا يجمع كل هذه القيم. لكن هناك استدعاء دالة واحد فقط. ولذلك مثّلت استدعاء الدالة باستخدام هذه الدائرة السوداء وحدها هنا.
حسنًا، ذلك هو التكرار (Iteration). نعرف كيف نفعله. ظللنا نفعله مدة طويلة جدًّا في هذا المقرر. لكن لننظر الآن إلى المسألة نفسها عوديًّا. في العودية، لدينا هاتان الخطوتان. هناك مهمة تصغير مسألتنا الأصلية إلى مسائل أصغر حتى نصل إلى نوع من حالات الأساس. ثم عند تلك النقطة، تكون لدينا مهمة إعادة بناء إجابتنا.
في الوضع العودي، لدي مرة أخرى استدعاء الدالة الواحد لإعادة التصحيح نيابة عن الطالب. لكن الطالب سيتعامل مع شخص واحد فقط، ربما المدرّسة. لن يتعامل الطالب مع أي شخص آخر في الطاقم. سيذهب الطالب إلى المدرّسة فقط ويقول: مرحبًا، أود إعادة تصحيح هذا الاختبار.
والآن سينتظر الطالب. والمدرّسة أيضًا استدعاء دالة لإعادة التصحيح. ربما لم تقم المدرّسة بأي تصحيح، لكنها ستجري استدعاء الدالة الخاص بها إلى مساعد التدريس: هل يمكنك إعادة تصحيح هذا الاختبار، من فضلك؟ ربما صحّح مساعد التدريس مسألة واحدة. سيتتبّع المسألة التي يحتاج إلى تصحيحها، لكن هناك مسائل أخرى تحتاج إلى التصحيح. لذلك سيجري مساعد التدريس استدعاء الدالة الخاص به إلى مساعد المختبر. ربما صحّح مساعد المختبر بعض المسائل. ثم سيتابع مساعد المختبر أيضًا الطلب، فيمرّر استدعاء الدالة إلى المصحّح، نوعًا ما.
لدينا إذًا مهمة إجراء التصحيح بوصفها دالة يجري تمريرها عبر جميع أفراد الطاقم. وعندما نصل إلى حالة الأساس، وهي المصحّح الذي يعرف على الأرجح — الذي صحّح على الأرجح السؤال الأخير، تبدأ الإجابة بالعودة إلى أعلى سلسلة استدعاءات الدوال. فيقول المصحّح: حسنًا، صحّحت مسألتي. لا يوجد أي شخص آخر أحتاج إلى سؤاله. فهذه هي درجتي.
تُمرَّر هذه الدرجة إلى أعلى سلسلة استدعاءات الدوال، إلى مساعد المختبر. سيأخذ مساعد المختبر تلك الدرجة ويضيفها إلى درجته، ويمرّرها إلى أعلى سلسلة استدعاءات الدوال، إلى مساعد التدريس. ويضيف مساعد التدريس تلك الدرجة إلى درجته. ربما صحّح مسألة، وربما لم يفعل. لكن على أي حال، إنهم يجمعون النتائج، شيئًا فشيئًا، إلى أعلى، حتى تُمرَّر إلى المدرّسة. ثم تقول المدرّسة: تفضّل، هذه درجتك.
ترون الفرق، صحيح؟ الطالب هو التكرار. يسأل جميع أفراد الطاقم، ولذلك يتعامل مع الجميع. لكن في العودية يكون الطالب أساسًا معلّقًا، ينتظر إجابة حتى نمرّ نزولًا عبر سلسلة استدعاءات الدوال هذه كلها وتُعاد بناء الإجابة صعودًا. فالطالب لا يتتبّع الإجابة مطلقًا. لا يحصل إلا على الإجابة النهائية في النهاية. هل ساعدكم هذا على الإطلاق؟ حسنًا، نقّحت هذا المثال بضع مرات. آمل أن يكون جيدًا بهذا الشكل.
الفكرة الكبرى في العودية هنا هي أن لدي هذه الاستدعاءات «المبكرة»، بين علامتي اقتباس، أي تلك التي أجريتها في البداية. هذه الاستدعاءات تنتظر فقط أن تعود النتائج. إنها لا تنفّذ أي عمل مفيد في البداية. لا تنفّذ عملًا مفيدًا إلا عندما تجمع النتائج بعد تلقي قيمة إرجاع من استدعاءات الدوال اللاحقة.
صفحة المصدر 7
آمل أن يعطيكم ذلك إحساسًا بكيفية تطبيق العودية. والآن، ما العودية بالضبط؟ خوارزميًّا، هي طريقة نتوصل بها إلى بعض الحلول لبعض المسائل، وفق نهج التقسيم والحل (Divide-and-Conquer)، أو التصغير والحل (Decrease-and-Conquer). لديك مسألتك الأصلية — تقسّمها كثيرًا إلى المسألة نفسها مع تغيير طفيف فقط، حتى تصل إلى حالة أساس. تستطيع حالة الأساس هذه بدء خطوة الحل (Conquer Step)، والبدء في إرجاع قيمة يمكنك البدء في تجميعها من استدعاءات الدوال السابقة.
أما من حيث الدلالة (Semantics)، فكما رأينا في المثال الذي ضربنا فيه الدوال، لدينا دالة تستدعي نفسها. بالطبع لا تستدعي نفسها بالمعاملات نفسها تمامًا، لأن ذلك سيؤدي إلى عودية لا نهائية، وهذا ليس ما نريده. سنستدعي أنفسنا مع تغيير طفيف في معاملاتنا، بطريقة تجعلنا نصل إلى حالة الأساس. وبمجرد الوصول إلى حالة الأساس، نبدأ مرة أخرى خطوة الحل، ويمكننا البدء في إعادة التجميع صعودًا. ورأيتم كيف تفعل استدعاءات الدوال ذلك عندما تساعد بعضها بعضًا في العودة إلى أعلى.
حسنًا، سأعطيكم بضع دقائق لتجربة هذا. أكملوا الدالة التي تحسب n مرفوعًا للقوة p، لهذه المتغيرات. إذا توصلتم إلى التعريف الرياضي، فستكون ترجمته إلى الكود مباشرة إلى حدٍّ ما. أدرجت حالتي أساس هنا. ربما تكون إحدى حالتي الأساس عندما يكون n هو 0، وحالة أساس أخرى عندما يكون n هو 1. اكتشفوا ما الذي ينبغي إرجاعه، ثم كيف تكتبون هذه الخطوة العودية.
لدي سطر، في حدود 50، يمكنكم كتابة الكود فيه. حسنًا، ما حالة الأساس الأولى عندي؟ نعم.
أحد الحضور:
إذا كان p هو 0، فستُرجع 1.
آنا بيل:
نعم، إذا كان p مساويًا لـ0، يمكننا إرجاع 1. عفوًا، مرة واحدة فقط. ما حالة أساس أخرى؟ p هو 1؛ يمكننا إرجاع 1. رائع. وماذا عن خطوتي العودية؟
أحد الحضور:
يمكننا إرجاع n في n.
آنا بيل:
نعم، يمكننا إرجاع n في —
أحد الحضور:
[غير مسموع]
آنا بيل:
هكذا؟ والآن لنفترض أنني لا أعرف كيف أستخدم **، كيف تعيد كتابة ذلك بدلالة الشيء الذي نحاول كتابته؟ كان هناك حل في الخلف.
أحد الحضور:
power_recur مع n وp ناقص 1.
آنا بيل:
نعم، يمكننا فعل ذلك أيضًا. نعم، بالضبط. هنا نفترض أننا لا نعرف المعامل **، وإلا لكانت هذه دالة سهلة جدًّا في الكتابة. نحاول تعريف المعامل ** بأنفسنا باستخدام هذه الدالة التي اسمها power_recur. لذلك سنستدعيها مرة أخرى هنا في الأسفل مع n وp ناقص 1. فإذا شغّلناها، فستعطينا 8. نعم.
أحد الحضور:
ما ضرورة وجود p يساوي 1 [غير مسموع]؟
آنا بيل:
نعم، سؤال ممتاز. ما ضرورة هذا؟ لا توجد ضرورة. في الواقع أدرجته هناك فقط لأريكم كيف يمكن أن تكون لدينا حالتا أساس. ففي هذه الحالة بالتحديد، لن نصل إلى هذه أبدًا إذا كان n أكبر من 1، لأننا نتوقف دائمًا هنا. وإذا أعطانا المستخدم 0، فسنُرجع تلك فقط. لكنها ستعمل أيضًا إذا أزلنا ذلك تمامًا. نعم، سؤال ممتاز.
ملاحظة المترجم: يحفظ هذا المقطع زلات النص المنطوق كما وردت: يبدأ بذكر
nلحالتي الأساس، ثم يناقشp، ويقول «pهو 1؛ يمكننا إرجاع 1». رياضيًّا،n**0يساوي 1 وn**1يساويn؛ هذه توضيحات للمترجم، وليست استبدالًا لكلام المتحدثة أو استنتاجًا لكود غير ظاهر في التفريغ.
صفحة المصدر 8
حسنًا، لننظر إلى مثال آخر. وهذا هو المثال الذي سأطلب فيه بعض المشاركة. أود أن ينزل أربعة منكم معي. لكن قبل أن نفعل ذلك، لنفكّر في المضروب (Factorial). تعريف مضروب n هو n في n ناقص 1 في n ناقص 2 في n ناقص 3، نزولًا إلى 1. ما حالة الأساس؟ ما أبسط شيء نعرف مضروبه؟ أخبروني أنتم.
أحد الحضور:
آنا بيل:
ما قيمة n — ما قيمة مضروب 0؟ 1، جيد. اخترت 1، لكن كليهما يمكن أن يعمل. إذا كان n مساويًا لـ0، يمكنك أيضًا إرجاع 1. أو نستطيع جعل n مساويًا لـ1، وإرجاع 1. ما خطوتنا العودية؟ هل تتعرّفون على النمط العودي هنا؟ مضروب n يساوي — n في مضروب n ناقص 1. إذا أخرجنا أول n، فإن n ناقص 1 في n ناقص 2 في n ناقص 3، وهكذا، هو فقط مضروب n ناقص 1. لذلك تقول خطوتنا العودية إنه n في الدالة نفسها، المضروب، مع n ناقص 1. هل هذا واضح للجميع؟ جيد.
حسنًا، لنمرّ بهذا المثال مع بعض المشاركة، فأحتاج إلى أربعة أشخاص. واحد، نعم. وستكونون على OCW إلى الأبد، يا جماعة. اثنان، نعم. اثنان آخران. نعم، شكرًا. شكرًا. رائع. حسنًا، سأجعلكم تقفون هنا مباشرة. سأطلب منكم الدخول واحدًا واحدًا ونحن نعمل على هذا الاختبار.
سنوضّح فقط، نوعًا ما، مرة أخرى ما يحدث عندما نجري استدعاءات دوال. هل تريدون الوقوف هنا مباشرة خلف حاسوبي؟ شكرًا. نعم، خلف حاسوبي. جيد. حسنًا، ممتاز. حسنًا، سأقف هنا فقط. سأكون البرنامج الرئيسي (Main Program). تشغّلون هذا الكود، وسأكون أنا البرنامج الرئيسي.
سأتتبّع المتغيرات وكل شيء في هذا النطاق العام (Global Scope). حسنًا، في النطاق العام، تمامًا كما فعلنا في السابق، لدي هنا تعريف دالة المضروب الخاصة بي. وهذا مجرد كود. في هذه اللحظة عرّفتها فقط. لا يهمني ما هي فعلًا. لكن لدي استدعاء دالة واحدًا. ومهمتي الوحيدة هي طباعة نتيجة مضروب 4. لدي مهمة سهلة إلى حدٍّ ما.
ماذا يحدث — أنتم، الحضور، أخبروني ماذا يحدث عندما يكون لدي مضروب 4. ما هذا؟ هل أعرف فورًا ما مضروب 4؟ لا. إنه استدعاء دالة، صحيح؟ وبصفته استدعاء دالة، ماذا أحتاج إلى فعله؟
أحد الحضور:
إنشاء بيئة.
آنا بيل:
بالضبط. أحتاج إلى إنشاء بيئة. حسنًا، ستكون أنت بيئتي الأولى. مرحبًا، اسمي — يمكنك وضعها عليك. تفضّل. مرحبًا، اسمي — ثم يمكنك الانتقال إلى هناك مباشرة. أنت استدعاء الدالة الأول عندي. اسمك fact، اختصارًا للمضروب. رائع. لقد استدعيتك للتو. ما مهمتك؟ أخبروني إذًا، ما مهمة مضروب 4 عند تشغيل الكود؟ هل سينفّذ if أم else؟
أحد الحضور:
else.
آنا بيل:
else. هذه إذًا مهمتك. تتبّع ذلك. قيمة n عندك ستكون 4. ومهمتك أن تُرجع 4 في مضروب 3. هل تعرف ما مضروب 3 الآن؟ لا. فما الذي تحتاج إلى فعله؟
أحد الحضور:
استدعاء شخص.
آنا بيل:
نعم، استدعِ شخصًا آخر، من فضلك. من ستستدعي؟ التالي. ماذا سيكون اسمك؟
أحد الحضور:
مضروب 3.
صفحة المصدر 9
آنا بيل:
اسمك أيضًا المضروب، بالضبط. وسيجري استدعاؤك مع n يساوي 3. يمكنك الوقوف بجانب مضروب 4 مباشرة. جميل جدًّا. لاحظوا الآن أن لدينا استدعاءَي دالة. اسم كليهما المضروب، صحيح؟ لكنهما استدعاءا دالة منفصلان تمامًا. إنهما بيئتان مختلفتان تمامًا. لكل منهما قيمة n الخاصة به، ولكل منهما مهمته الخاصة التي ينبغي تنفيذها.
كون اسميهما كليهما المضروب لا يعني أنهما سيتداخلان مع متغيرات بعضهما. هذه نقطة مهمة جدًّا، جدًّا. يا مضروب 3، هل تعرف ما مضروب 2؟
أحد الحضور:
لا أعرف.
آنا بيل:
لا. فما الذي تحتاج إلى فعله؟
أحد الحضور:
أحتاج إلى استدعاء شخص.
آنا بيل:
بالضبط. من ستستدعي؟
أحد الحضور:
المضروب.
آنا بيل:
تفضّل. ماذا سيكون اسمك؟
أحد الحضور:
المضروب عند — هل هو 2 الآن؟
آنا بيل:
نعم، نحن عند 2، بالضبط. أنت إذًا المضروب. اسمك أيضًا المضروب. وسيجري استدعاؤك مع n يساوي 2. مرة أخرى، لدي الآن ثلاثة استدعاءات للمضروب. كلها إلى الاسم المضروب، لكنها جميعًا استدعاءات دوال مستقلة. مهمتك إذًا إرجاع 2 في مضروب 1. هل تعرف ما مضروب 1؟
أحد الحضور:
نعم. مهلًا، لا.
[ضحك]
آنا بيل:
بصفتك إنسانًا تعرف، لكن بصفتك المضروب لا تعرف. ما الذي تحتاج إلى فعله؟
أحد الحضور:
استدعاؤها.
آنا بيل:
استدعاؤها، بالضبط. تفضّلي. اسمك أيضًا المضروب. يمكنك الوقوف بجانب استدعاءات المضروب الأخرى اللطيفة عندنا. مهمتك إذًا — يا حضُور، لقد كشفتها بالفعل. مهمتك الأخيرة هي إرجاع 1. حسنًا، ممتاز. هذه هي قيمة إرجاعك. والآن، يا مضروب 1، هل ستُرجعين تلك القيمة إليّ؟ إلى أي واحد ستُرجعينها؟
بالضبط. يستطيع إذًا المضروب مع n يساوي 2 أن يستبدل الآن بدالة مضروب واحد عنده العدد 1. فما قيمة إرجاعك، يا مضروب 2؟
أحد الحضور:
2؟
أحد الحضور:
(همسًا) أجبت إجابة صحيحة.
آنا بيل:
2، بالضبط. إلى أين تمرّر قيمتك إذًا؟ حسنًا، هناك شيء نسيناه: بمجرد أن تعطي قيمة الإرجاع، تختفي.
[ضحك]
صفحة المصدر 10
كانت لديك مهمة بسيطة جدًّا. آسفة، لكنها كانت مهمة حقًّا. كنتِ حالة الأساس عندنا. من دونك كانت ستكون لدينا عودية لا نهائية. حسنًا، مرّرت قيمتك. وبصفتك دالة انتهت من مهمتها، ماذا تفعل؟ تختفي، بالضبط. شكرًا. حسنًا، مضروب، أين نحن، 3، بالضبط، ماذا — ما قيمتك الآن؟
أحد الحضور:
أنا 6.
آنا بيل:
6، بالضبط. فهذه قيمة إرجاعك. هل تعطيني إياها أم — تفضّل. بالضبط، جيد جدًّا. نختفي. لدينا إذًا ثلاثة استدعاءات دوال اختفت بمجرد إرجاع قيمة. وأخيرًا، 4 في 6.
أحد الحضور:
آنا بيل:
ولمن تعطي قيمتك؟ لي، وأنا أعطيتك إياها للتو. آسفة، نعم، كان ذلك مربكًا. شكرًا جزيلًا لكم يا جماعة. وضّح ذلك بضعة أشياء. يمكنكم العودة. شكرًا جزيلًا.
[تصفيق]
لقد وضّحنا بضعة أشياء هنا. سأقوم — أستطيع فعل ذلك على الشرائح أيضًا، لتأكيد الفكرة فقط. لكن لنمرّ بها. لدي مضروب 4. في كل مرة أجري استدعاء دالة، حتى وإن كان الاسم نفسه، وكلها المضروب، تكون بيئة منفصلة تمامًا. صادف أنها تحمل الاسم نفسه، لكنها مسؤولة فقط عن تنفيذ مهمتها الخاصة. هنا لدي مضروب 4 يستدعي 4 في مضروب 3.
بمجرد أن أرى مضروب 3، ينشئ هذا بيئة أخرى. هذه ستُرجع 3 في مضروب 2. ومرة أخرى، بيئة أخرى. هذه تُرجع 2 في مضروب 1. وبيئة أخيرة. أهم بيئة عندنا هي الأخيرة، التي فيها حالة الأساس. إنها تسمح لنا بإطلاق خطوة الحل. ستُرجع هذه الخطوة الأساسية القيمة 1 إلى من استدعاها. ومرة أخرى، نحن لا نقفز من مكان إلى آخر.
لا نُرجع القيمة إلا إلى الدالة التي استدعتنا. وأعرف أن الأمر يصبح مربكًا جدًّا لأن كل شيء اسمه fact في هذه الحالة بالتحديد. لكن علينا فقط تذكّر أي دالة استدعتنا. فنُرجع 1 إلى أعلى هنا. يصبح هذا 2 في 1. ويمكنهم إنهاء مهمتهم. لاحظوا أننا عند هذه النقطة، كان لدينا — كنّا 1، 2، 3، 4 دوال معلّقة نوعًا ما، تنتظر أن تُمرَّر القيم إليها. لكن يمكننا الآن أخيرًا إنهاء مهامنا واحدة تلو الأخرى.
هذه تُرجع 2. وهذه تُرجع 6. وهذه تُرجع 24. ويُطبع العدد 24. الفكرة الكبيرة هنا أن كل استدعاء دالة، حتى وإن حمل الاسم نفسه، هو بيئة منفصلة تمامًا ومستقلة تمامًا، لها معاملاتها الخاصة. يمكن أن تتغيّر تلك المعاملات داخل تلك البيئات. وهذا مقبول تمامًا. لن تتداخل مع أي معاملات في أي بيئات أخرى.
حسنًا. فلنفتح رابط Python Tutor. ومرة أخرى، يمكننا فعل ذلك مرة أخرى فقط لنريكم كيف يبدو من حيث Python Tutor. هنا لدي المضروب مع n يساوي 4، يستدعي n يساوي 3، ويستدعي المضروب مع n يساوي 2، ويستدعي المضروب مع n يساوي 1.
عند هذه النقطة، تمامًا كما حدث مع الضرب، لدي جميع استدعاءات المضروب هذه قيد التنفيذ، لكن يمكننا البدء في إرجاع القيم إلى من استدعانا، حتى نعود إلى الأصلي، استدعاء الدالة الأصلي. حسنًا، هذه مراجعة أخرى للملاحظات التي رأيناها. لكل استدعاء دالة مختلف بيئته الخاصة. والمتغيرات داخل هذه البيئات خاصة بتلك البيئات. إنها لا تتداخل مع بعضها.
صفحة المصدر 11
وتدفّق التحكم (Flow of Control) — عندما نجري استدعاء دالة، فكل ما نعرفه هو الدالة التي نستدعيها تاليًا. لا نقفز من مكان إلى آخر. كل ما نعرفه هو من نستدعي تاليًا، وإلى من نحتاج إلى إعادة القيمة صعودًا.
هناك شيء أخير أردت الإشارة إليه. هنا لدي كود المضروب، النسخة التكرارية والنسخة العودية. فالتي على اليسار — آسفة، التي على اليمين هي ما كتبناه بالفعل، أي المضروب العودي. والتي على اليسار هي النسخة التكرارية. شخصيًّا، أعتقد أن التي على اليمين أسهل قراءة، لأنها تشبه كثيرًا الطريقة التي نكتب بها التعبير رياضيًّا.
لكن لو أتيح لكم قليل من الوقت للتفكير فيه، فيمكنكم بالسهولة نفسها التوصل إلى كود ينفّذ المهمة نفسها تمامًا تكراريًّا. تذكّروا أن لدينا في التكرار حلقتنا. لا توجد دالة أخرى — لا توجد استدعاءات دوال أخرى. لدينا حلقة تتكرر عددًا ما من المرات. وهناك نوع من متغيرات الحلقة أو عدّاد الحلقة (Loop Counter). وهناك متغير حالة (State Variable) يتتبّع الإجابة التي تهمّنا — وهي في هذه الحالة بالتحديد حاصل الضرب من 1 وصولًا إلى n، شاملًا إياه.
أريد إنهاء محاضرة اليوم ببضع ملاحظات فقط. رأينا اليوم أمثلة بسيطة حقًّا على العودية. لكنني أعتقد أنها عرضت بعض الأفكار الصعبة جدًّا، جدًّا، التي يجد الناس عادة صعوبة في استيعابها عند رؤية العودية لأول مرة. وذلك لأنك أساسًا تكتب دالة بدلالة نفسها. وقد يكون ذلك مربكًا قليلًا. لذلك، بالطبع، طبّقنا العودية على أشياء بسيطة جدًّا، جدًّا. قمنا بالضرب، وقمنا بالمضروب.
بحسب إحساسكم، قد تكون النسخة العودية أو النسخة التكرارية أكثر بداهة بالنسبة إليكم. وبالتأكيد، في هذه الأمثلة لم تحتاجوا إلى كتابتها عوديًّا. هناك كثير من الكود الذي لا تحتاجون في الواقع إلى تنفيذه عوديًّا. الحل التكراري أكثر بداهة بكثير، خصوصًا لأنكم تعرّفتم إلى التكرار أولًا. تعرّفتم إلى حلقات for وحلقات while في المحاضرة الثالثة، أو نحو ذلك. وإذا كان ذلك أول ما رأيتموه، فعادة سيكون أول ما تلجؤون إليه.
لكن هناك مسائل عدة تكون كتابتها باستخدام العودية أكثر بداهة. فمن الأمثلة التي تكون فيها العودية أكثر بداهة: أي وقت تحتاج فيه إلى تكرار مهمة لا تعرف كم تحتاج إلى التعمّق فيها، وفي تلك الحالة ستتولى الاستدعاءات العودية استدعاء نفسها، ثم نفسها، ثم نفسها، ثم نفسها، حتى تصل إلى حالة الأساس. لا تحتاج إلى التفكير في ذلك في تكرارك.
مثال على ذلك هذا المثال التقليدي نوعًا ما، حيث يوجد ملف داخل نظام ملفات (File System). إذا كنّا نبحث عن مجموعة مسائل، pset.txt، فقد يكون لدينا طالب يقع ملفه pset.txt مباشرة تحت مجلد user/pset.txt. لكن قد يكون لدينا شخص آخر سيكون ملفه pset.txt داخل المستخدمين، والمستندات، والمدارس، وMIT، والمقررات، و6.100L، ومجموعات المسائل، وpset1/pset.txt.
هذا الغموض بشأن العمق الذي تحتاج إلى البحث فيه داخل نظام الملفات للوصول إلى الملف الذي يهمّك هو موضع مثالي لتطبيق العودية. وهناك مثال آخر حين يكون لديك تعبير (Expression). إذا كنت تبني آلتك الحاسبة الخاصة في الكود، وكان لديك ترتيب التعبيرات — آسفة، ترتيب العمليات (Order of Operations) باستخدام الأقواس، فإنك أيضًا لا تعرف كم قوسًا قد تحتاج إلى أن تمرّ به حلقة حتى تصل إلى التعبير الأساسي لتحدّد الذي تحتاج إلى تنفيذه أولًا.
وهذه حالة أخرى يكون فيها استخدام العودية مفيدًا جدًّا. في المحاضرة القادمة سنراجع العودية باستخدام مثال آخر، متتالية فيبوناتشي (Fibonacci Sequence). ثم سنبدأ النظر إلى العودية المطبّقة على القوائم (Lists). وتحديدًا، إذا كانت لدينا قوائم داخل قوائم داخل قوائم داخل قوائم، ولا نعرف كم قائمة متداخلة (Nested Lists) قد تكون لدينا، فستكون العودية مثالًا مثاليًّا لذلك.