اذهب إلى المحتوى

تطوير الخوارزميات في جافا


رضوى العربي

قد تَكُون البرمجة (programming) صعبة نوعًا ما، وذلك كغَيْرها من الأنشطة المُفيدة والجديرة بالاهتمام، ومع ذلك، فهي عادة ما تَكُون مُجْزيّة ومُمتعة. عند كتابة برنامج (program)، لابُدّ أن تُخبِر الحاسوب بكُل تَفصيلة صغيرة يَنبغي له تَّنْفيذها، وبصورة صحيحة تمامًا؛ وذلك لأنه سيَتَّبِع البرنامج كما هو مَكتوب تمامًا وبطريقة عمياء. إذن، كيف تُكتَب البرامج غَيْر البسيطة؟ في الواقع، إنه ليس أمرًا غامضًا، فالأمر بِرُمَّته يَعتمِد فقط على تَعَلُّم طريقة التفكير الصحيحة.

يُعدّ البرنامج (program) تعبيرًا عن فكرة (idea)، حيث يبدأ المُبرمج بفكرة عامة عن مُهِمّة (task) معينة يُريد من الحاسوب تَّنْفيذها، وعادةً ما يكون المُبرمج على عِلم بكيفية تَّنْفيذ تلك المُهِمّة يدويًا، أو لديه تَصَوُّر عام على الأقل. تتمحور المشكلة حول طريقة تَحْوِيل هذا التَصَوُّر العام إلى إِجراء (procedure)، واضح، ومُكتمِل، ومُحدَّد الخُطوات لتََّنْفيذ تلك المُهِمّة. يُسمَى مثل هذا الإِجراء باسم "الخوارزمية (algorithm)"، والتي هي إِجراء واضح، ومُحدَّد الخُطوات، والذي لابُدّ أن ينتهي دائمًا بَعْد عَدَد مُتناهي (finite number) من الخُطوات؛ فنحن لا نُريد عدّ الإِجراءات التي قد تَستمِر للأبد. لاحِظ أن الخوارزمية (algorithm) تَختلف عن البرنامج (program)، فالبرنامج يُكتَب بلغة برمجية معينة، أما الخوارزمية، فتُكتَب بأيّ لغة بما فيها الإنجليزية، وهي أَشْبَه بالفكرة وراء البرنامج، ويُقصَد بالفكرة هنا مجموعة الخُطوات المطلوب القيام بها حتى يَتمّ تَّنْفيذ المُهِمّة، وليس مُجرَّد مُلخَّص لما ينبغي للمُهِمّة إِنجازه في النهاية. عند وصف الخوارزمية، ليس من الضروري أن تَكُون الخُطوات مُفَصَّلة، وذلك طالما كانت واضحة بما فيه الكفاية لبَيَان أن تَّنْفيذها سوف يُنجِز المُهِمّة المطلوبة، ولكن بالطبع لا يُمكِن التعبير عنها كبرنامج (program) فِعليّ بدون مَلْئ جميع تلك التفاصيل.

من أين تأتي الخوارزميات؟ ينبغي عادةً تطويرها، وهو ما يَتطلَّب كثيرًا من التفكير والعمل الجاد. يُمكِن القول أن عملية تطوير الخوارزميات (algorithm development) هي مهارة تُكتسَب مع المُمارسة المستمرة، ولكن، مع ذلك، تَتَوفَّر بعض التقنيات (techniques) والقواعد الإرشادية (guidelines) التي يُمكِنها مُساعدتك. سنتناول في هذا القسم بَعضًا منها، وبالأخص ما يَتعلَّق بالبرمجة "في نطاق ضيق"، كما سنعود للحديث عن نفس هذا الموضوع أكثر من مرة بالفصول القادمة.

الشيفرة الوهمية والتصميم المتدرج

عند البرمجة "في نطاق ضيق"، فأنت مُقيَّد نوعًا ما باِستخدَام عَدَد قليل من الأوامر، وهي كالتالي: المُتَغيِّرات (variables)، وتَعْليمَات الإِسْناد (assignment statements)، والبرامج (routines) الخاصة بعَمليتي الإِدْخال (input) والإِخراج (output). قد يَتوفَّر لك أيضًا اِستخدَام بعض البرامج الفرعية (subroutines)، والكائنات (objects)، أو رُبما حتى بعض اللَبِنات الأساسية الآخرى، ولكن بشَّرْط أن تَكُون قد كُتِبَت مُسْبَّقًا إِمّا بواسطتك أو بواسطة شخص آخر (لاحِظ أن برامج الإِدْخال والإِخراج تَقع ضِمْن هذا التصنيف). والآن، تَستطِيع بناء مُتتالية مِنْ تلك التَعْليمَات البسيطة، أو ربما قد تَدمجهم دِاخل بُنَى تحكُّم (control structures) أكثر تعقيدًا، مثل تَعْليمَة حَلْقة التَكْرار (loops)‫ while، وتَعْليمَة التَفْرِيع الشَّرْطيّة if.

لنفْترِض أننا نريد برمجة الحاسوب ليُنفِّذ مُهِمّة (task) معينة، يُمكِننا البدء بكتابة تَوصِيف (description/specification) مَبدئي لتلك المُهِمّة، بحيث يُلخِّص وظيفة الخوارزمية (algorithm) المطلوب تَطْويرها، ثُمَّ نُضيف مزيدًا من الخطوات والتفاصيل إلى ذلك التَوصِيف تدريجيًا، وعبر سِلسِلة من التصميمات المُحسَّنة، إلى أن نَصِل إلى الخوارزمية الكاملة التي يُمكِن ترجمتها مباشرة إلى لغة برمجية. تُكتَب عادة تلك التَوصِيفات باِستخدَام ما يُعرَف باسم الشيفرة الوهمية (pseudocode)، وهي مجموعة من التَعْليمَات العَاميَّة، التي تُحاكِي بِنْية اللغات البرمجية، بصورة مُبسَّطة، وبدون قواعد الصيغة (syntax) الصارمة المُعتادة بالشيفرة الفعليّة. يُطلَق على هذا الأسلوب من كتابة البرامج اسم التصميم المُتدرج (stepwise refinement)، ويُصنَّف ضِمْن استراتيجيات التصميم من أعلى لأسفل (top-down design).

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

// اقرأ مدخل المستخدم
Get the user's input
// احسب قيمة الاستثمار بعد عام
Compute the value of the investment after 1 year
// اطبع القيمة
Display the value
// احسب قيمة الاستثمار بعد عامين
Compute the value after 2 years
// اطبع القيمة
Display the value
// احسب قيمة الاستثمار بعد ثلاث أعوام
Compute the value after 3 years
// اطبع القيمة
Display the value
// احسب قيمة الاستثمار بعد أربع أعوام
Compute the value after 4 years
// اطبع القيمة
Display the value
// احسب قيمة الاستثمار بعد خمس أعوام
Compute the value after 5 years
// اطبع القيمة
Display the value

على الرغم من أن الخوارزمية بالأعلى سليمة وستؤدي الغرض منها، لكنها مُكرَّرة، وهو ما يَعنِي ضرورة اِستخدَام حَلْقة تَكْرار (loop)؛ لأنها ستُمكِّننا من كتابة شيفرة مُعمَّمة أكثر وبعَدَد سطور أقل. يُقصَد بالتَعْميم (generalization) هنا أنه يُمكِن اِستخدَام نفس حَلْقة التَكْرار بغض النظر عن عدد الأعوام المطلوب مُعالجتها. والآن، سنُعيد كتابة مُتتالية الخطوات السابقة كالتالي:

// اقرأ مدخل المستخدم
Get the user's input
// طالما ما يزال هناك عدد من الأعوام للمعالجة
while there are more years to process:
    // احسب قيمة الاستثمار بعد العام التالي
    Compute the value after the next year
    // اطبع القيمة
    Display the value

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

// اسأل المستخدم عن قيمة الاستثمار المبدئي
Ask the user for the initial investment
// اقرأ مدخل المستخدم
Read the user's response
// اسأل المستخدم عن قيمة سعر الفائدة
Ask the user for the interest rate
// اقرأ مدخل المستخدم
Read the user's response

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

// طالما ما يزال هناك عدد من الأعوام للمعالجة
while there are more years to process:
    // احسب الفائدة
    Compute the interest
    // أضف الفائدة إلى قيمة الاستثمار
    Add the interest to the value
    // اطبع القيمة
    Display the value

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

// ابدأ بعدد أعوام يساوي الصفر
years = 0
// طالما عدد الأعوام أقل من الخمسة
while years < 5:
    // أزد عدد الأعوام بمقدار الواحد
    years = years + 1
    // احسب الفائدة
    Compute the interest
    // أضف الفائدة إلى تلك القيمة
    Add the interest to the value
    // اطبع القيمة
    Display the value

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

// اسأل المستخدم عن قيمة الاستثمار المبدئي
Ask the user for the initial investment
// اقرأ مدخل المستخدم
Read the user's response
// اسأل المستخدم عن قيمة سعر الفائدة
Ask the user for the interest rate
// اقرأ مدخل المستخدم
Read the user's response
// ابدأ بعدد أعوام يساوي الصفر
years = 0
// طالما عدد الأعوام أقل من الخمسة
while years < 5:
    // أزد عدد الأعوام بمقدار الواحد
    years = years + 1
    // احسب الفائدة بحيث تساوي حاصل ضرب القيمة مع سعر الفائدة
    Compute interest = value * interest rate
    // أضف الفائدة إلى تلك القيمة
    Add the interest to the value
    // اطبع القيمة
    Display the value

وَصلنا إلى النقطة التي يُمكِن معها الترجمة المُباشرة إلى لغة برمجة مناسبة، فقط نحتاج إلى اِختيار أسماء المُتَغيِّرات (variables)، وتَقْرير نص العبارات التي سنَطبَعها للمُستخدِم، وهكذا. نستطيع الآن كتابة الخوارزمية (algorithm) بلغة الجافا كالتالي:

double principal, rate, interest;  // التصريح عن المتغيرات
int years;
System.out.print("Type initial investment: ");
principal = TextIO.getlnDouble();
System.out.print("Type interest rate: ");
rate = TextIO.getlnDouble();
years = 0;
while (years < 5) {
   years = years + 1;
   interest = principal * rate;
   principal = principal + interest;
   System.out.println(principal);
}

ما زال أمامنا بعض التَحسِّينات الإضافية، مِن بينها تَضْمِين هذه الشيفرة داخل برنامج كامل، وإضافة التعليقات (comments)، وطِباعة المَزيد من المعلومات للمُستخدِم، ولكنه يظِلّ نفس البرنامج بالقسم السابق.

في حين تَستخدِم خوارزمية الشيفرة الوهمية (pseudocode algorithm) المسافات البادئة (indentation) لتوضيح التَعْليمَات الواقعة ضِمْن حَلْقة التَكْرار (loop)، تُهمِل لغة الجافا هذه المسافات البادئة تمامًا، ولهذا أَضفنا قوسين معقوصين (curly brackets/braces) {} لتوضيح أيّ مجموعة تَعْليمَات (statements) تقع ضِمْن حَلْقة التَكْرار. إذا لم تَستخدِم هذه الأقواس بشيفرة الجافا، فإن الحاسوب سيفْترِض أن التَعْليمَة الوحيدة الواقعة ضِمْن حَلْقة التَكْرار هي years = years + 1;‎، أمَا بقية التَعْليمَات فسيُنفِّذها مرة واحدة فقط بَعْد انتهاء حَلْقة التَكْرار. للأسف، لا يُبلِّغ الحاسوب عن هذه النوعية من الأخطاء، بنفس الطريقة التي يُبلِّغ بها عن حُدوث خطأ في حالة عدم اِستخدَام القوسين الهلاليين (rounded brackets/parentheses) () حول (years < 5)؛ وذلك لأن تلك الأقواس مطلوبة وِفقًا لصيغة (syntax) تَعْليمَة while، أمَا قوسيّ المعقوصين {}، فإنها مطلوبة فقط لأغراض دَلاليّة (semantics)، أيّ أغراض مُتعلِّقة بالمعنى. يَستطيع الحاسوب عمومًا تَمييز أخطاء بناء الجملة (syntax errors) فقط، لا الأخطاء الدَلاليّة (semantic errors).

لاحِظ أن التَوصِيف الأصلي للمسألة التالي لم يَكُن مُكتملًا:

اقتباس

"اِحسب قيمة الاستثمار واطبعها لكل عام من الأعوام الخمسة التالية، بحيث تُحدَّد قيمة الاستثمار المبدئي وسعر الفائدة مِن قِبَل المُستخدِم".

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

اقتباس

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

مسألة متتالية "3N+1"

لنفْحَص مثالًا آخرًا لم نتَعرَّض له من قَبْل، السؤال هنا عبارة عن مَسألة رياضية مُجرَّدة، والتي يَعُدّها الكاتب تَحْدِيدًا واحدة مِن تمارينه البرمجية المُفضلة. سنبدأ هذه المرة، بعكس المثال السابق، بتَوصِيف كامل (specification) لمُهِمّة (task) البرنامج:

اقتباس

"بفَرْض أن لديك عددًا صحيحًا موجبًا (positive integer)، وليَكُن N، اِحسب قيم مُتتالية الأعداد (sequence) ‫"3N+1"، والتي تبدأ من العَدَد N، وبحيث تُحسَب قيمة عنصر المُتتالية التالي وِفقًا للآتي: إذا كان N عَدَدًا زوجيًا، اِحسب حاصل قِسمته على العَدَد ٢، أمَا إذا كان فرديًا، فاِحسب حاصل ضربه في العَدَد ٣، ثُمَّ أَزِد قيمة حاصل الضرب بمقدار ١. اِستمر بحِسَاب قيم عناصر مُتتالية الأعداد بنفس الطريقة حتى تُصبِح قيمة N مُساوِية للعَدَد ١. على سبيل المثال، لمّا كانت قيمة N تُساوِي ٣، والتي هي عَدَد فردي، فإنها ضُرِبت في ٣ وأُزِيدَت نتيجة حاصل الضرب بمقدار ١، أيّ، N = 3*3+1 = 10. لمّا أصبحت قيمة N زوجية، فإنها قُسمت على ٢، أيّ، N = 10/2 = 5. نَستمر بحِسَاب قيم عناصر المُتتالية، ونَتوقَف عندما تُصبِح قيمة N مُساوِية للقيمة 1، لنَحصُل على مُتتالية الأعداد التالية: ٣، ١٠، ٥، ١٦، ٨، ٤، ٢، ١.

اُكْتُب برنامجًا يَقرأ عَدَدًا صحيحًا موجبًا من المُستخدِم، ثُمَّ يَطبَع مُتتالية الأعداد "3N+1"، بحيث تبدأ من العَدَد المُدْخَل، كما يَنبغي للبرنامج أن يعدّ عَدَد عناصر المُتتالية ويَطبعها."

اُنظر الخوارزمية المبدئية التالية، والتي تُوضِح فقط التَصَوُّر العام لمثل هذا البرنامج:

// اقرأ عدد صحيح موجب من المستخدم
Get a positive integer N from the user.
// احسب قيمة كل عنصر بالمتتالية، واطبعه وعدّه
Compute, print, and count each number in the sequence.
// اطبع عدد عناصر المتتالية
Output the number of terms.

يَتضح لنا أن الخطوة الثانية تَحتوِي على المَضمون الفِعليّ للبرنامج، وبالطبع تحتاج إلى مزيد من الإيضاح.

لمّا كنا نُريد الاستمرار بحِسَاب قيم عناصر المُتتالية حتى تُصبِح قيمة N الحالية مُساوِية للعَدَد ١، فإننا سنحتاج ببساطة إلى اِستخدَام حَلْقة تَكْرار (loop)، ولذلك دَعْنَا نُعيد صياغة نفس الجملة السابقة بحيث تَتوافق مع حَلْقة التَكْرار while. إننا في حاجة إلى مَعرِفة متى نَستمِر بتَّنْفيذ حَلْقة التَكْرار ومتى نُوقِّفها، في الواقع، سنستمر طالما كانت قيمة N الحالية لا تُساوِي ١، ولهذا يُمكِننا إعادة كتابة خوارزمية الشيفرة الوهمية (pseudocode algorithm) كالتالي:

// اقرأ عدد صحيح موجب من المستخدم
Get a positive integer N from the user;
// ‫‫طالما كانت قيمة `N` الحالية لا تساوي 1
while N is not 1:
    // ‫احسب قيمة عنصر المتتالية التالي واسنده إلى N
    Compute N = next term;
    // ‫اطبع قيمة N
    Output N;
    // عدّ عنصر المتتالية
    Count this term;
// اطبع عدد عناصر المتتالية
Output the number of terms;

لمّا كان حِسَاب قيمة عنصر المُتتالية التالي يَعتمِد على ما إذا كانت قيمة N الحالية هي عَدَد زوجي (even) أم فردي (odd)، يَعنِي ذلك أن الحاسوب بحاجة إلى تَّنْفيذ حَدَثين مُختلفين لكل حالة، وهو ما يَعنِي أن اِستخدَام تَعْليمَة التَفْرِيع الشَّرْطيّة if بات ضروريًا؛ وذلك للاختيار ما بين تلك الحالتين، اُنظر الخوارزمية بَعْد التعديل:

// اقرأ عدد صحيح موجب من المستخدم
Get a positive integer N from the user;
// ‫‫طالما كانت قيمة `N` الحالية لا تساوي 1
while N is not 1:
    // ‫إذا كان N عددًا زوجيًا
    if N is even:
        // ‫احسب قيمة العنصر التالي وأسنده إلى N
       Compute N = N/2;
    // ‫إذا كان N عددًا فرديًا
    else
        // ‫احسب قيمة العنصر التالي وأسنده إلى N
       Compute N = 3 * N + 1;
    // ‫اطبع قيمة N
    Output N;
    // عدّ عنصر المتتالية
    Count this term;
// اطبع عدد عناصر المتتالية
Output the number of terms;

انتهينا تقريبًا، يَتبقَّى فقط العدّ (counting)؛ وذلك لطباعة عَدَد عناصر المُتتالية. يَعنِي العدّ ببساطة أن تبدأ بالقيمة صفر، ثم تُضيف المقدار واحد في كل مرة يَكُون لديك فيها ما تعدّه، ولهذا نحتاج إلى مُتَغيِّر (variable) للقيام بالعدّ، يُعرَف باسم العَدَّاد (counter). يَنبغي ضَبْط قيمة ذلك المُتَغيِّر إلى القيمة صفر قَبْل بداية الحَلْقة (loop)، بحيث تَزداد (increment) تلك القيمة أثناء تَّنْفيذ الحَلْقة. (يُعدّ ذلك أحد الأنماط الشائعة [common pattern]، ولذلك تَوقَّع أن تراه بكثير من البرامج). تُصبِح الخوارزمية، بَعْد إضافة العَدَّاد (counter)، كالتالي:

// اقرأ عدد صحيح موجب من المستخدم
Get a positive integer N from the user;
// اضبط قيمة العداد إلى القيمة صفر
Let counter = 0;
// ‫‫طالما كانت قيمة N الحالية لا تساوي 1
while N is not 1:
    // ‫إذا كان N عددًا زوجيًا
    if N is even:
        // ‫احسب قيمة العنصر التالي وأسنده إلى N
       Compute N = N/2;
    // ‫إذا كان N عددًا فرديًا
    else
        // ‫احسب قيمة العنصر التالي وأسنده إلى N
       Compute N = 3 * N + 1;
    // اطبع قيمة‫ N
    Output N;
    // ‫أزد قيمة العداد بمقدار 1
    Add 1 to counter;
// اطبع عدد عناصر المتتالية
Output the counter;

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

// اطلب من المستخدم إدخال عدد صحيح موجب
Ask user to input a positive number;
// ‫أسند القيمة المدخلة إلى N
Let N be the user's response;
// طالما‫ N ليست موجبة
while N is not positive:
   // اطبع رسالة خطأ
   Print an error message;
   // ‫اقرأ قيمة أخرى واسندها إلى N
   Read another value for N;
// اضبط قيمة العداد إلى القيمة صفر
Let counter = 0;
// ‫‫طالما كانت قيمة `N` الحالية لا تساوي 1
while N is not 1:
    // ‫إذا كان N عددًا زوجيًا
    if N is even:
       // ‫احسب قيمة العنصر التالي وأسنده إلى N
       Compute N = N/2;
    // ‫إذا كان N عددًا فرديًا
    else
        // ‫احسب قيمة العنصر التالي وأسنده إلى N
       Compute N = 3 * N + 1;
    // اطبع قيمة‫ N
    Output N;
    // ‫أزد قيمة العداد بمقدار 1
    Add 1 to counter;
// اطبع عدد عناصر المتتالية
Output the counter;

لاحِظ أن حَلْقة while الأولى ستنتهي فقط عندما تُصبِح قيمة N زوجية.

عند محاولة كتابة شيفرة التَوصِيف التالي: "إذا كانت قيمة N غَيْر زوجية، اُطلب من المُستخدِم إِدْخَال عدد آخر"، يقع الكثير من المبرمجين، وبالأخص المبتدئين، في خطأ اِستخدَام تَعْليمَة التَفْرِيع if بدلًا من تَعْليمَة حَلْقة التَكْرار while. تَظهر المشكلة تَحْدِيدًا عندما يُدْخِل المُستخدِم عددًا غَيْر زوجي مرة آخرى. لمّا كانت تَعْليمَة التَفْرِيع if تُنفَّذ مرة واحدة فقط، فإنه لا يَتمّ فَحْص مُدْخَل المُستخدِم إلا مرة واحدة فقط، مما يعني أن البرنامج سينتقل إلى تَّنْفيذ التَعْليمَة التالية بغض النظر عما إذا كانت قيمة المُدْخَل الثاني للمُستخدِم زوجية أم لا، وهو ما يَتسبَّب بحُدوث حَلْقة لا نهائية (infinite loop) كما ذَكرنا آنفًا. أمَا في حالة اِستخدَام حَلْقة التَكْرار while، فإن الحاسوب سيَقفِز (أو سيَنقِل التَحكُّم بتعبير أدق) إلى بداية الحَلْقة بَعْد كل عملية إِدْخَال؛ لاختبار ما إذا كانت القيمة المُدْخَلة زوجية أم لا، مما يَعنِي أنه سيستمر في طلب إِدْخَال عَدَد جديد إلى أن يُدْخِل المُستخدِم قيمة مقبولة، أيّ عدد زوجي. وبالتالي، في حالة انتقال البرنامج إلى تَّنْفيذ ما بَعْد حَلْقة while، فإن قيمة N هي زوجية حتمًا.

ها هو نفس البرنامج بشيفرة الجافا. لاحِظ اِستخدَام العَامِلين (operators)‏ ‎<=‎ بمعنى "أقل من أو يُساوِي" و ‎!=‎ بمعنى "لا يُساوِي"، بالإضافة إلى اِستخدَام التعبير N % 2 == 0؛ لاختبار ما إذا كانت قيمة N زوجية. نُوقِشَت كل هذه العَوامِل في القسم ٢.٥.

import textio.TextIO;

 public class ThreeN1 {

      public static void main(String[] args) {                

         int N;       // لحساب العناصر بالمتتالية
         int counter; // لعد عدد عناصر المتتالية

         System.out.print("Starting point for sequence: ");
         N = TextIO.getlnInt();
         while (N <= 0) {
            System.out.print(
                   "The starting point must be positive. Please try again: " );
            N = TextIO.getlnInt();
         }

         // ‫نعلم أن N هي عدد صحيح موجب عند هذه النقطة

         counter = 0;
         while (N != 1) {
             if (N % 2 == 0)
                N = N / 2;
             else
                N = 3 * N + 1;
             System.out.println(N);
             counter = counter + 1;
         }

         System.out.println();
         System.out.print("There were ");
         System.out.print(counter);
         System.out.println(" terms in the sequence.");

     }  // نهاية‫ main

 }  // ‫نهاية الصنف ThreeN1

مُلاحظتان أخيرتان على هذا البرنامج:

أولًا، ربما لاحَظت أن البرنامج لم يَطبَع قيمة أول عنصر بالمُتتالية -أيّ قيمة N المُدْخَلة من قِبَل المُستخدِم-، وكذلك لم يعدّها. هل هذا خطأ؟ يَصعُب القول. ربما ينبغي أن نَطرح سؤالًا آخر: هل كان تَوصِيف البرنامج (specification) صريحًا بما يَكفي بخُصوص تلك النقطة؟ في الواقع، للإجابة على مثل هذا السؤال، ستَحتاج إلى طَلَب مزيد من الإيضاح من أستاذك/مديرك. يُمكِن عمومًا حل هذه المشكلة -في حال كانت- بسهولة، فقط اِستبدل السَطْرين التاليين بتَعْليمَة counter = 0 قَبْل حَلْقة التَكْرار while :

System.out.println(N);   // print out initial term
counter = 1;       // and count it

ثانيًا، لماذا تُعدّ هذه المسألة تَحْدِيدًا مثيرة؟ في الواقع، يَجِدْ كثير من علماء الرياضيات والحاسوب هذه المسألة مُشوقة؛ بسبب سؤال بسيط، يَخُص تلك المسألة، والذي لم يَتوصَّلوا للإجابة عليه بَعْد. السؤال هو "هل عملية حِسَاب قيم مُتتالية '3N+1' دومًا ما ستنتهي بَعْد عَدَد مُتناهي (finite) من الخطوات لجميع قيم N المبدئية؟" على الرغم من سهولة حِسَاب قيم المُتتاليات بشكل مُفرد، لم يُجِبْ أحد على السؤال الأعم حتى الآن، أيّ بصياغة آخرى، لا أحد يَعلم ما إذا كان من الصحيح تسمية عملية حِسَاب قيم مُتتالية "3N+1" بـ"الخوارزمية (algorithm)"؛ فبالنهاية، لابُدّ لأيّ خوارزمية أن تَنتهي بَعْد عَدَد مُتناهي من الخطوات.

لاحظ: يَنطبق ذلك على الأعداد الصحيحة (integers) بمفهومها الرياضي، وليس القيم من النوع العددي الصحيح int! بمعنى أننا نفْترِض هنا أن قيمة N قد تَكُون أيّ عدد صحيح مُمكن مهما كَبُر، وهو ما لا يَنطبق على مُتَغيِّر من النوع int داخل برنامج بلغة الجافا. إذا أَصبحت قيمة N كبيرة جدًا ليَتمّ تَمثيلها بمُتَغيِّر من النوع int ‏(32 بت)، فلا يُمكِن عدّ قيم خَرْج البرنامج صحيحة رياضيًا، أيّ أن البرنامج لا يَحسِب قيم متتالية "3N+1" بشكل صحيح عندما تَكُون قيمة N كبيرة. اُنظر تمرين ٨.٢.

كتابة الشيفرة (coding) والاختبار (testing) وتنقيح الأخطاء (debugging)

بَعْد انتهائك من تَطوير خوارزمية البرنامج (algorithm)، سيَكُون من اللطيف لو كان بإمكانك الضغط فقط على زِر معين؛ لتَحصُل بَعْدها على برنامج قابل للتَّنْفيذ (working program) بصورة ممتازة. في الواقع، عملية تَحْوِيل الخوارزمية إلى شيفرة بلغة الجافا لا تَتمّ دومًا بمثل هذه السَلاسَة لسوء الحظ، وحتى عندما تَصِل إلى تلك المرحلة من الحُصول على برنامج قابل للتَّنْفيذ (working program)، فإنه غالبًا ما يَكُون قابلًا للتَّنْفيذ بمعنى أنه يُنفِّذ "شيء ما"، لا بالضرورة الشيء الذي تريده أن يَفعَله.

بَعْد الانتهاء من تصميم البرنامج (program design)، يَحيِن موعد كتابة الشيفرة (coding): أيّ ترجمة التصميم إلى برنامج مكتوب بلغة الجافا أو بأيّ لغة برمجية اخرى. مَهمَا كنت حَريصًا أثناء كتابة الشيفرة، فعادةً ما ستَجِدْ بعض أخطاء بناء الجملة (syntax errors) طريقها إلى الشيفرة، ولذلك سيَرفُض مُصرِّف (compiler) الجافا البرنامج، وسيُبلِّغك عن نوع معين من رسائل الخطأ (error message). لاحِظ أنه في حين يستطيع المُصرِّف اكتشاف أخطاء بناء الجملة (syntax errors) دائمًا، فإنه لسوء الحظ ليس بنفس الكفاءة في اكتشاف مَاهية الخطأ، بل أنه قد لا يَتَمكَّن، في بعض الأحيان، من معرفة مكان حُدوث الخطأ الفِعليّ، فمثلًا، قد يَتسبَّب وجود خطأ إملائي أو نَسْيَان قوس "{" بالسطر رقم ٤٥ بتَوَقُّف المُصرِّف بالسطر ١٠٥. يَظِلّ، مع ذلك، الفهم الجيد لقواعد صياغة (syntax rules) اللغة البرمجية مع اِتباع بعض القواعد الإرشادية البرمجية البسيطة الطريقة الأفضل لتَلافِي كثير من تلك الأخطاء. لنسْتَعْرِض بعضًا من تلك القواعد، أولًا، لا تَكتُب أبدًا قوس حَاصِرة "{" بدون كتابة زوجه الآخر "}"، ثُمَّ عُد بَعْدها لكتابة التَعْليمَات بينهما؛ وذلك لأن نَسْيَان قوس أو إضافة قوس في غَيْر مَحَلّه يُعدّ من أكثر الأخطاء التي يَصعُب اكتشافها خاصة بالبرامج الضخمة. ثانيًا، اِستخدِم دائما المسافات البادئة (indentation) لتَنسيق الشيفرة، وإن عَدَّلت البرنامج، عَدِّل أيضًا المسافات البادئة بحيث تُصبِح مُتوافقة مع التَعْدِيل الجديد. ثالثًا، اِستخدِم نَمط تَسمية (naming scheme) ثابت؛ حتى لا تُعانِي بَعْد ذلك بينما تَتَذكَّر ما إذا كان اسم مُتَغيِّر ما (variable) هو "interestrate" أم "interestRate". رابعًا، عندما يُبلِّغك المُصرِّف بأكثر من رسالة خطأ (error message) واحدة، لا تُحاوِل إصلاح رسالة الخطأ الثانية حتى تَنتهي من إِصلاح الأولى؛ لأن المُصرِّف عادةً ما يَرتبك بعد إِيجاده لأول خطأ، ولذلك قد تَكُون رسائل الخطأ التالية هي مُجرَّد تَخمينات. وأخيرًا، وربما هي النصيحة الأفضل: خُذ الوقت الكافي لفِهم الخطأ قبل مُحاولة إِصلاحه؛ فالبرمجة، بالنهاية، ليست علمًا تجريبيًا (experimental science).

إذا تمّ تَصرِيف برنامجك بنجاح، لا يَعنِي ذلك أنك قد انتهيت؛ فمن الضروري أن تَختبر (test) البرنامج لتتأكد مما إذا كان يَعمَل بشكل صحيح، وهو ما لا يَقْتصِر على مُجرَّد الحُصول على الخَرْج الصحيح (output) لعينة المُدْخَلات (inputs) التي أَعطاك إِياها الأستاذ، بل ينبغي للبرنامج أن يَعمَل بشكل سليم لجميع المُدْخَلات المقبولة، وفي حالة اِستقباله لمُدَْخَل غَيْر صالح، فينبغي أن يُوبِخ البرنامج المُستخدِم بلطف، لا أن يَنَهار (crashing) تمامًا. عمومًا، ينبغي أن تَختبِر البرنامج على نطاق واسع من المُدْخَلات. قد تُحاوِل أيضًا إِيجاد مجموعة المُدْخَلات التي بإِمكانها اِختبار جميع الوظائف التي أَدْرَجتها بالشيفرة. في حالة كتابة برامج كبيرة، حَاوِل تقسيمها إلى عدة مراحل، بحيث تَختبِر كل مرحلة قَبْل البدء بالمرحلة التالية، حتى لو اضطررت لكتابة شيفرة إِضافية تقوم بالاختبار مثل أن تَستدعِي أحد البرامج الفرعية التي قُمت بكتابتها للتو؛ فأنت حتمًا لا تُريد أن يَنتهي بك الحال بخُمسمائة سَطْر جديد من الشيفرة مَصحوبة بخطأ ما في مكان ما.

الغرض من الاختبار (testing) هو مُحاولة العُثور على الأخطاء البرمجية (bugs)، وهي -بعكس أخطاء التَصرِيف (compilation errors)- أخطاء دَلاليّة (semantic errors)، أيّ تَكُون في صورة سُلوك غَيْر سليم. المُحزن في الأمر هو أنك غالبًا ما ستَجِدهم. تستطيع، مع ذلك، تَقْليل -لا التَفادِي التام- هذه النوعية من الأخطاء البرمجية (bugs)، من خلال الانتباه أثناء قيامك بكُلًا من التصميم (design)، وكتابة الشيفرة (coding). عندما تَكتشف خطأً برمجيًا (bug)، يَحيِن موعد تَنْقِيح الأخطاء (debugging)، بمعنى تَعَقُّب سبب الخطأ بالشيفرة بهدف التخلص منه. لاحظ أنك لن تُصبِح خبيرًا بتَنْقِيح الأخطاء إلا من خلال المُمارسة المستمرة؛ فهي مَهارة تكتسبها، مثلها مثل جميع نواحي البرمجة الآخرى، ولذلك لا تَكُن خائفا منها، وإنما تَعلَّم منها. تُعدّ القدرة على قراءة الشيفرة واحدة من أهم مهارات تَنْقِيح الأخطاء الاساسية، والمقصود بقراءة الشيفرة هنا: القدرة على تَنْحية تَصوراتك المُسْبَّقة عما ينبغي للشيفرة أن تقوم به، وبدلًا من ذلك، تَعقُّب الطريقة التي يُنفِّذها بها الحاسوب خَطوة بخَطوة؛ لتُدرِك ما تَقوم به فِعليًا. في الواقع، هذا ليس بالأمر السهل. ما يزال الكاتب يَذكُر تلك المرة التي قضى فيها عدة ساعات يَبحث عن خطأ برمجي ليَكتشِف بالنهاية أن سَطْرًا ما بالشيفرة، كان قد نَظَر إليه عشرات المرات، يَحتوِي على القيمة ١ بدلًا من الحرف i، أو تلك المرة التي كَتَب فيها برنامجًا فرعيًا (subroutine) اسمه هو WindowClosing، والذي كان سيُؤدي غرضه تمامًا لولا أن الحاسوب كان يَبحث عن البرنامج الفرعي windowClosing (بحرف w صغير). قد يُساعِدك أحيانًا الاستعانة بشخص آخر لفَحْص الشيفرة، خاصة وأنه لن يَملك نفس تَصوراتك المُسْبَّقة عنها.

أحيانًا ما يَكُون مُجرَّد العثور على ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ مُشكلة بحد ذاته، ولهذا تُوفِّر مُعظم بيئات التطوير البرمجية (programming environments) برنامجًا يُسمَى مُنقِّح الأخطاء (debugger)؛ لمساعدتك بالعثور على الأخطاء البرمجية (bugs)، بحيث يَتمّ تَشْغِيل البرنامج (program) تحت تَحكُّم ذلك المُنقِّح، والذي يَسمَح لك بضَبْط ما يُعرَف باسم نقاط المُقاطعة (breakpoints)، وهي نقاط بالبرنامج سيَتوقَّف عندها المُنقِّح مؤقتًا (pause)؛ حتى تَتَمكَّن من فَحْص قيم مُتَغيِّرات البرنامج عند تلك النقطة، مما سيُساعدك في العُثور على المكان الذي بدأت فيه الأخطاء البرمجية بالظهور أثناء تَّنْفيذ البرنامج. بمُجرَّد تَحْدِيد ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ البرمجي (bug)، يَسمَح لك المُنقِّح أيضًا بتَّنْفيذ البرنامج سَطْرًا سَطْرًا، ومِنْ ثَمَّ، تستطيع مشاهدة ما يَحدُث تفصيليًا.

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

System.out.println("At start of while loop, N = " + N);

ينبغي لنص الخَرْج أن يُساعِدك على تَحْدِيد مكان تَعْليمَة الطباعة المسئولة عن ذلك الخَرْج، وكذلك معرفة قيم المُتَغيِّرات (variables) المُهمة. ستكتشف، في بعض الأحيان، أن الحاسوب لم يَمر حتى على ذلك الجزء من البرنامج الذي كنت تَظن أنه يُنفِّذه. تَذكَّر أن الهدف هو اكتشاف أول نُقطة بالبرنامج أَصبَحت فيها حالة البرنامج (state) مُخالِفة للحالة المُتوقَّعة، فهذا هو مكان الخطأ البرمجي (bug).

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

ترجمة -بتصرّف- للقسم Section 3.1 Blocks, Loops, and Branches من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...