السابقالفهرسالتالي

الفصل 6

العمليات المثمرة

 

6.1 القيم المعادة

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

double e = Math.exp (1.0);
double height = radius * Math.sin (angle);
لكن جميع العمليات التي كتبناها حتى الآن كانت عمليات فارغة (أو عقيمة void methods)؛ أي أنها عمليات لا تعيد أية قيمة. عندما تستدعي عملية فارغة، ستكون العملية نموذجياً على سطر لوحدها، بدون أي إسناد:
nLines (3);
g.drawOval (0, 0, width, height);
في هذا الفصل، سوف نكتب عمليات تعيد أشياء، وسأدعوها بالعمليات المثمرة (fruitful methods). المثال الأول هو area، التي تأخذ عدداً عشرياً (double) كمعامل، وتعيد مساحة الدائرة ذات نصف القطر المعطى:
public static double area (double radius) {
  double area = Math.PI * radius * radius;
  return area;
}
أول شيء عليك ملاحظته هو أن بداية تعريف العملية قد اختلفت. بدلاً من public static void، التي تشير إلى عملية فارغة، توجد public static double، التي تشير إلى أن القيمة المعادة من هذه العملية ستكون من النوع double. لم أشرح معنى public static حتى الآن، تحلى بالصبر.

أيضاً، لاحظ أن السطر الأخير هو شكل آخر لتعليمة return يتضمن قيمة معادة. هذه التعليمة تعني، "ارجع فوراً من هذه العملية واستعمل العبارة التالية كقيمة معادة." العبارة التي تقدمها لتعليمة return يمكن أن تكون معقدة بقدر ما ترغب، لذا كان من الممكن أن نكتب هذه العملية بشكل أكثر اختصاراً:

public static double area (double radius) {
   return Math.PI * radius * radius;
}
من جهة أخرى، المتغيرات المؤقتة (temporary variables) مثل area غالباً ما تجعل تنقيح الأخطاء أسهل. في كلا الحالتين، يجب أن يوافق نوع العبارة المستخدمة في تعليمة العودة نوع القيمة التي ترجعها العملية. بكلام آخر، عندما تصرح بأن نوع الإرجاع هو double، فإنك تعطي وعداً بأن هذه العملية ستنتج قيمة من نوع double في النهاية. إذا حاولت العودة بدون أي قيمة، أو باستخدام عبارة تنتج قيمة من نوع مخالف، فسيعاقبك المترجم.

أحياناً يكون من المفيد استعمال عدة تعليمات عودة، واحدة في كل فرع من فروع تعليمة شرطية:

public static double absoluteValue (double x) {
   if (x < 0) {
      return -x;
   } else {
      return x;
   }
}
بما أن تعليمات العودة هذه موجودة في تعليمة شرطية من نمط التنفيذ البديل، سيتم تنفيذ واحدة منها فقط. على الرغم من أن وجود أكثر من تعليمة return في عملية واحدة مشروع، إلا أنك يجب أن تتذكر دائماً أنه بمجرد تنفيذ واحدة منها، سيتم إنهاء العملية بدون تنفيذ أية تعليمات لاحقة.

الشفرة التي تظهر بعد تعليمة return، أو أي مكان آخر لا يمكن أن يتم تنفيذها فيه أبداً، تدعى بالشفرة الميتة (dead code). بعض المجمعات تحذرك في حال وجود جزء ميت في شفرتك.

إذا وضعت تعليمات العودة داخل تعليمات شرطية، فعليك أن تضمن أن كل مسار ممكن للبرنامج ينتهي بتعليمة عودة. مثلاً:

public static double absoluteValue (double x) {
  if (x < 0) {
    return -x;
  } else if (x > 0) {
    return x;
  }			// WRONG!!
}
هذا البرنامج غير مشروع بسبب وجود الحالة التي يكون فيها x=0، عندها لن يكون أي من الشروط صحيحاً وستنتهي العملية بدون الوصول إلى تعليمة عودة. إن الرسالة الصادرة عن مجمع نموذجي ستكون "مطلوب تعليمة عودة للعملية absoluteValue" - "return statement required in absoluteValue"، وهي رسالة محيرة إذ أنه يوجد تعليمتي عودة فيها.

6.2 تطوير البرامج

عند هذه النقطة يجب أن تكون قادراً على النظر إلى عمليات كاملة في Java ومعرفة ما تفعله. لكن قد لا تكون كيفية كتابة عمليات كهذه واضحة بعد. سأطرح إحدى التقنيات التي أدعوها التطوير التصاعدي (incremental development).

كمثال، تخيل أنك تريد حساب المسافة بين نقطتين، إحداثياتهما (x1, y1) و(x2, y2). باستخدام التعريف المعتاد،

الخطوة الأولى هو الأخذ بعين الاعتبار الشكل الذي ستظهر به عملية distance في Java. بكلمات أخرى، ما هو الدخل (المعاملات) وما هو الخرج (القيمة المعادة).

في هذه الحالة، النقطتين هما المعاملات، ومن الطبيعي أن نعبر عنهما باستخدام أربع أعداد عشرية، مع أننا سنرى فيما بعد أن Java تملك كائناً يدعى Point يمكننا استعماله. القيمة المعادة هي المسافة، والتي ستكون من النوع double.

يمكننا الآن كتابة مخططاً تمهيدياً للعملية:

public static double distance (double x1, double y1, double x2, double y2) {
  return 0.0;
}
التعليمة return 0.0; هي إشارة علام ضرورية حتى يتم تجميع البرنامج. من الواضح أن البرنامج لا يجري أي شيء مفيد في هذه المرحلة، لكنه يستحق تجربة تجميعه لنتمكن من العثور على أية أخطاء نحوية قبل أن نضيف المزيد من الشفرة.

حتى نختبر العملية الجديدة، علينا استدعاؤها مع قيم اختبار. في مكان ما من main يمكن أن أضيف:

double dist = distance (1.0, 2.0, 4.0, 6.0);
لقد اخترت هذه القيم بحيث تكون المسافة الأفقية 3 والمسافة الشاقولية 4؛ وبهذا ستكون المسافة بين النقطتين 5 (الوتر في مثلث قائم الزاوية أضلاعه 3-4-5). عندما تختبر عملية ما، من المفيد أن تعلم الإجابة الصحيحة.

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

الخطوة التالية في حسبتنا هي إيجاد الفرق x2 – x1 و y2 – y1. سأخزن هذه القيم في متغيرات مؤقتة اسمها dx وdy.

public static double distance (double x1, double y1, double x2, double y2) {
    double dx = x2 - x1;
    double dy = y2 - y1;
    System.out.println ("dx is " + dx);
    System.out.println ("dy is " + dy);
    return 0.0;
}
أضفت تعليمات طباعة تمكنني من معرفة القيم الوسيطة الناتجة قبل المتابعة. كما ذكرت من قبل، أنا أعلم مسبقاً أن القيم يجب أن تكون 3.0 و4.0.

عندما أنتهي من كتابة العملية سأزيل تعليمات الطباعة. شفرة كهذه تدعى سقالات (scaffolding)، لأنها تساعد في بناء البرنامج، لكنها ليست جزءاً من البرنامج النهائي. أحياناً يكون إبقاء السقالات فكرة جيدة، لكن حوِّلها لتعليقات، لكي تجدها في حال احتجت إليها لاحقاً.

الخطوة التالية في تطوير عمليتنا هو تربيع dx وdy. يمكننا استخدام عملية Math.pow، لكن من الأبسط والأسرع أن نضربهما بنفسيهما.

public static double distance (double x1, double y1, double x2, double y2) {
  double dx = x2 - x1;
  double dy = y2 - y1;
  double dsquared = dx*dx + dy*dy;
  System.out.println ("dsquared is " + dsquared);
  return 0.0;
}
مرة أخرى، سأترجم البرنامج وأشغله للتحقق من القيمة الوسيطة (التي يجب أن تكون 25.0).

أخيراً، يمكننا استخدام عملية Math.sqrt لحساب وإرجاع النتيجة.

public static double distance (double x1, double y1, double x2, double y2) {
    double dx = x2 - x1;
    double dy = y2 - y1;
    double dsquared = dx*dx + dy*dy;
    double result = Math.sqrt (dsquared);
    return result;
}
بعد ذلك في main، يجب أن نطبع القيمة الناتجة وأن نتحقق من صحتها.

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

المقومات الأساسية لهذه العملية هي:

6.3 التركيب

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

دعنا نقل أن نقطة المركز مخزنة في المتغيرين xc وyc، ونقطة المحيط مخزنة في xp وyp. الخطوة الأولى هي إيجاد نصف قطر الدائرة، وهو البعد بين النقطتين. لحسن الحظ، لدينا عملية، distance قادرة على القيام بذلك.

double radius = distance (xc, yc, xp, yp);
الخطوة التالية هي حساب مساحة الدائرة، وإعادة قيمته:
double area = area (radius);
return area;
بجمع كل ذلك في عملية، سنحصل على:
public static double circleArea (double xc, double yc, double xp, double yp) {
  double radius = distance (xc, yc, xp, yp);
  double area = area (radius);
  return area;
}
المتغيرات المؤقتة radius وarea مفيدة أثناء التطوير والتصحيح، بعد أن يعمل البرنامج بصورة صحيحة يمكننا جعله أكثر اختصاراً بتركيب استدعاءات العمليات:
public static double circleArea (double xc, double yc, double xp, double yp) {
   return area (distance (xc, yc, xp, yp));
}

6.4 التحميل الزائد

لربما لاحظت في الفقرة السابقة أن circleArea وarea تجريان وظيفة مشابهة — حساب مساحة دائرة — لكنهما تأخذان معاملات مختلفة. بالنسبة إلى area، يجب أن نعطيها نصف القطر؛ أما circleArea، فيجب إعطاؤها نقطتين.

إذا قامت عمليتان بنفس الوظيفة فمن الطبيعي أن نعطيهما نفس الاسم. إن وجود أكثر من عملية بنفس الاسم، وهو ما يدعى بالتحميل الزائد (Overloading)، مشروع في Java طالما أن كل نسخة من العملية تأخذ معاملات مختلفة. لذا يمكننا أن نعيد تسمية circleArea:

public static double area (double x1, double y1, double x2, double y2) {
    return area (distance (xc, yc, xp, yp));
}
عندما تستدعي عملية زائدة التحميل(Overloaded method) ، ستعرف Java أي نسخة تريدها أنت بالاعتماد على المتحولات التي تعطيها. إذا كتبت:
double x = area (3.0);
ستبحث Java عن عملية اسمها area تأخذ متحولاً واحداً من نوع double، وهكذا تستعمل النسخة الأولى، التي تعامل المتحول على أنه نصف القطر. أما إذا كتبت:
double x = area (1.0, 2.0, 4.0, 6.0);
ستستخدم Java النسخة الثانية من area. لاحظ أيضاً لأن النسخة الثانية من area تستدعي النسخة الأولى عملياً.

العديد من أوامر Java الجاهزة (built-in commands) محمَّلة بشكل زائد، ما يعني وجود نسخ مختلفة تقبل عدداً مختلفاً أو أنواعاً مختلفة من المعاملات. مثلاً، هناك نسخ من print وprintln تقبل معاملاً واحداً من أي نوع. في صنف Math، توجد نسخة من abs تعمل مع الأعداد العشرية، ونسخة أخرى تعمل مع الأعداد الصحيحة.

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

في الواقع، هذا يذكرني بإحدى القواعد الأساسية في تصحيح الأخطاء: تأكد أن نسخة البرنامج التي تنظر إليها هي نفس نسخة البرنامج قيد التشغيل!

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

6.5 العبارات البوليانية

معظم العمليات التي رأيناها تعطي نتائجاً من نفس نوع معاملاتها (operands). مثلاً، عملية + تأخذ عددين صحيحين وتعطي عدد صحيح، أو عددين عشريين وتعطي عدد عشري، الخ.

الاستثناء الذي شاهدناه كان مع العمليات المنطقية (relational operators)، التي تقارن بين أعداد صحيحة أو بين أعداد عشرية وتعيد إما true أو false. true وfalse قيمتان خاصتان في Java، ومعاً يؤلفان نوعاً للقيم يدعى النوع البولياني (boolean). لربما تتذكر عندما عرفت النوع، وقلت أنه مجموعة من القيم. في حالة الأعداد الصحيحة، والأعداد العشرية والسلاسل المحرفية، كانت هذه المجموعات كبيرة جداً. بالنسبة إلى booleans، فالمجموعة ليست كبيرة حقاً.

العبارات والمتغيرات البوليانية تعمل تماماً كأنواع العبارات والمتغيرات الأخرى:

boolean bob;
bob = true;
boolean testResult = false;
المثال الأول هو تصريح بسيط عن متغير؛ المثال الثاني هو عملية إسناد، والثالث هو عمليتي تصريح وإسناد مدمجتين، أحياناً ندعو ذلك بالتهيئة (initialization). القيم true وfalse هي كلمات مفتاحية في Java، لذا قد تظهران بلون مختلف، بحسب بيئة البرمجة التي تستخدمها.

كما ذكرت سابقاً، إن نتيجة العامل الشرطي بوليانية، لذا يمكنك تخزين نتيجة مقارنة في متغير:

boolean evenFlag = (n%2 == 0);          // true if n is even
boolean positiveFlag = (x > 0);         // true if x is positive
وأن تستعمله كجزء من تعليمة شرطية لاحقاً:
if (evenFlag) {
  System.out.println ("n was even when I checked it");
}
إن المتغير المستعمل بهذه الطريقة غالباً ما يدعى علم (flag)، نظراً لأنه يعلمنا بوجود أو غياب شرط معين.

6.6 العوامل المنطقية

هناك ثلاثة عوامل منطقية (logical operators) في Java: (و) AND، (أو) OR، و(النفي) NOT، التي ترمز بالرموز &&، || و!. إن معنى هذه العوامل مشابه لمعناها اللغوي. مثلاً، x > 0 && x < 10 محقق فقط إذا كان x أكبر من صفر و كان أصغر من 10.

evenFlag || n%3 == 0 محقق إذا كان أحد الشرطين محققاً، أي إذا كان evenFlag يحمل القيمة true أو كان n قابلاً للقسمة على 3.

أخيراً، عامل النفي NOT له أثر نقض أو عكس عبارة بوليانية، لذا فإن !evenFlag له قيمة true إذا كان evenFlag يحمل القيمة false — أي إذا كان العدد فردياً.

غالباً ما توفر العوامل المنطقية طريقة لتبسيط التعليمات الشرطية المتداخلة. مثلاً، كيف يمكننا كتابة الشفرة التالية باستخدام تعليمة شرطية واحدة؟

if (x > 0) {
  if (x < 10) {
    System.out.println ("x is a positive single digit.");
  }
}

6.7 العمليات البوليانية

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

public static boolean isSingleDigit (int x) {
  if (x >= 0 && x < 10) {
    return true;
  } else {
    return false;
  }
}
اسم هذه العملية isSingleDigit. من الشائع إعطاء العمليات البوليانية أسماء تشبه yes/no questions. نوع القيمة المرجعة هو boolean، ما يعني أن تعليمة العودة يجب أن توفر عبارة بوليانية.

الشفرة بحد ذاتها بسيطة، إلا أنها أطول قليلاً مما تحتاج. تذكر أن العبارة x >= 0 && x < 10 ذات نوع بولياني، لذا يمكننا إرجاعها مباشرة، وتجنب عبارة if مرة واحدة:

public static boolean isSingleDigit (int x) {
  return (x >= 0 && x < 10);
}
في main يمكنك استدعاء عملية كهذه باستخدام الطرق المعتادة:
boolean bigFlag = !isSingleDigit (17);
System.out.println (isSingleDigit (2));
السطر الأول يسند القيمة true إلى bigFlag فقط في حال لم يكن 17 عدداً مؤلفاً من خانة واحدة. السطر الثاني يطبع true لأن 2 عدد مؤلف من خانة واحدة. نعم، تم التحميل الزائد للعملية println لتستطيع التعامل مع المتحولات البوليانية أيضاً.

الاستعمال الأكثر شيوعاً للعمليات البوليانية هو داخل التعليمات الشرطية

if (isSingleDigit (x)) {
  System.out.println ("x is little");
} else {
  System.out.println ("x is big");
}

6.8 المزيد من التعاود

الآن بعد أن أصبح لدينا عمليات ترجِـع قيماً، أصبح لدينا لغة برمجة كاملة وفقاً لمعيار تورين (Turing complete programming language)، وذلك يعني أننا قادرين الآن أن نحسب أي شيء قابل للحوسبة، وذلك بالنسبة للتعاريف المنطقية لكلمة "حوسبة".

تم تطوير هذه الفكرة على يد العالمان ألونزو تشيرتش (Alonzo Church) وآلان تورين (Alan Turing)، وهي تعرف بفرضية تشيرتش-تورين. يمكنك قراءة المزيد عنها على http://en.wikipedia.org/wiki/Turing_thesis.

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

التعاودية: صفة تستخدم لوصف العمليات التعاودية.

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

0! = 1
n! = n . (n - 1)!
(غالباً ما يتم ترميز التابع العاملي بالرمز !، الذي لا يجب الخلط بينه وبين عامل Java المنطقي ! الذي يعني النفي (NOT)). هذا التعريف يقول أن 0 عاملي يساوي 1، وأن عاملي أي قيمة أخرى، n، يساوي n مضروباً بعاملي n-1. إذاً 3! هو !2 × 3، و!2 هو !1 ×2، و!1 هو !0 × 1. بوضعها جميعاً معاً، نحصل على أن !3 يساوي 1 × 1 × 2 × 3، يساوي 6.

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

public static int factorial (int n) {
}
إذا تصادف أن يكون المتحول صفراً، كل ما علينا فعله هو إرجاع القيمة 1:
public static int factorial (int n) {
  if (n == 0) {
  return 1;
  }
}
وهذه هي الحالة القاعدية.

وإلا، هذا هو الجزء الممتع، علينا عمل استدعاء تعاودي لحساب عاملي n-1، ثم ضربه بn.

public static int factorial (int n) {
  if (n == 0) {
    return 1;
  } else {
    int recurse = factorial (n-1);
    int result = n * recurse;
    return result;
  }
}
لو نظرنا إلى مجرى تنفيذ هذا البرنامج، لوجدناه مشابهاً لـ countdown من القسم 4.8. إذا استدعينا factorial مع القيمة 3:

بما أن 3 ليست صفراً، نتبع الفرع الثاني ونحسب العاملي لـ n-1.

  بما أن 2 ليس صفراً، نتبع الفرع الثاني ونحسب العاملي لـ n-1.

    بما أن 1 ليست صفراً، نتبع الفرع الثاني ونحسب العاملي لـ n-1.

      بما أن 0 هو الصفر، نتبع الفرع الأول ونعيد القيمة 1 فوراً بدون القيام بأية استدعاءات تعاودية أخرى.

    يتم ضرب القيمة المعادة (1) بـ n، الذي يساوي 1، وتتم إعادة النتيجة.

  يتم ضرب القيمة المعادة (1) بـ n، الذي يساوي 2، وتتم إعادة النتيجة.

يتم ضرب القيمة المعادة (2) بـ n، الذي يساوي 3، وتتم إعادة النتيجة، 6، إلى main، أو أياً كان من استدعى العملية factorial (3).

هنا تجد المخطط الهرمي لهذه السلسلة من استدعاءات العمليات:

يتم إظهار القيم المعادة وهي تمرر عائدة إلى أعلى الهرم.

لاحظ أنه في آخر حالة للعملية factorial، لا يوجد متغيرات محلية باسم recurse أو result لأنه عندما يكون n=0 لا يتم تنفيذ الفرع الذي ينشئهم.

6.9 وثبة الثقة

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

في الواقع، لقد مارست هذه الطريقة سابقاً عندما كنت تستعمل العمليات المبنية مسبقاً في Java. عندما تستدعي Math.cos أو drawOval، فلن تفحص مجريات هذه العمليات. بل تفترض أنها تعمل بشكل صحيح، لأن الناس الذي كتبوا هذه العمليات كانوا مبرمجين جيدين.

حسن، نفس الشيء يصح عندما تستدعي واحدة من العمليات التي كتبتها بنفسك. مثلاً، في القسم 6.7 كتبنا عملية اسمها isSingleDigit تحدد ما إذا كان العدد بين 0 و9 أو لا. بمجرد إقناع أنفسنا بأن هذه العملية صحيحة — بعد اختبار وفحص الشفرة — يمكننا استخدام تلك العملية بدون النظر إلى الشفرة مرة أخرى أبداً.

نفس الشيء صحيح أيضاً مع البرامج التعاودية. عندما تصل إلى الاستدعاء التعاودي، بدلاً من لحاق مجرى التنفيذ، عليك أن تفترض أن الاستدعاء التعاودي صحيح (وأنه يعطي النتيجة الصحيحة)، وبعدها اسأل نفسك، "بفرض أنني حسبت عاملي n-1، هل أستطيع حساب عاملي n؟" في هذه الحالة، من الواضح انك تستطيع، بضربه بn.

طبعاً، من الغريب قليلاً أن تفترض أن العملية تعمل بشكل صحيح قبل أن تنتهي من كتابتها حتى، لكن هذا هو السبب وراء تسميتها قفزة الثقة!

6.10 مثال إضافي أخير

ثاني أشهر مثال عن التوابع الرياضية المعرفة تعاودياً بعد factorial، هو fibonacci، المعرف بالشكل التالي:

fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n - 1) + fibonacci(n - 2)
وبترجمته إلى Java يصبح:
public static int fibonacci (int n) {
  if (n == 0 || n == 1) {
     return 1;
  } else {
     return fibonacci (n-1) + fibonacci (n-2);
  }
}
إذا حاولت تتبع مجرى التنفيذ هنا، حتى من أجل قيم صغيرة للمتحول n، فإن رأسك سينفجر. لكن حسب قفزة الثقة، إذا افترضنا أن الاستدعاءين التعاوديين (نعم، يمكنك عمل استدعاءين تعاوديين) يعملان بشكل صحيح، عندئذ سيكون واضحاً أننا سنحصل على الإجابة الصحيحة بجمعهما معاً.

6.11 المصطلحات

نوع الإرجاع: الجزء من التصريح عن العملية الذي يحدد نوع القيمة التي ستعيدها العملية.

القيمة المعادة: القيمة المقدمة على أنها ناتج استدعاء العملية.

الشفرة الميتة: جزء من البرنامج لا يمكن تنفيذه أبداً، غالباً لأنه يظهر بعد تعليمة return.

السقالات: شفرة تستخدم أثناء تطوير البرنامج لكنها ليست جزءاً من الإصدار النهائي.

void: نوع إرجاع خاص يشير إلى عملية عقيمة؛ وهي عملية لا تعيد أي قيمة.

التحميل الزائد: هو وجود أكثر من عملية بنفس الاسم لكن بمعاملات مختلفة. عندما تستدعي عملية محملة بشكل زائد، ستعرف Java أي واحدة تقصد من خلال المتحولات التي تعطيها إياها.

بولياني: نوع متغير يمكن له تخزين إحدى القيمتين فقط: true أو false.

علم (راية): متغير (بولياني عادة) يسجل شرطاً أو معلومات عن حالة.

عامل شرطي: عامل يقارن بين قيمتين وينتج قيمة بوليانية تشير إلى العلاقة بين المعاملات.

عامل منطقي: عامل يركب قيمتين بوليانيتين وينتج قيمة بوليانية.

return type: The part of a method declaration that indicates what type of value the method returns.

return value: The value provided as the result of a method invocation.

dead code: Part of a program that can never be executed, often because it appears after a return statement.

scaffolding: Code that is used during program development but is not part of the final version.

void: A special return type that indicates a void method; that is, one that does not return a value.

overloading: Having more than one method with the same name but different parameters. When you invoke an overloaded method, Java knows which version to use by looking at the arguments you provide.

boolean: A type of variable that can contain only the two values true and false.

flag: A variable (usually boolean) that records a condition or status information.

conditional operator: An operator that compares two values and produces a boolean that indicates the relationship between the operands.

logical operator: An operator that combines boolean values and produces boolean values.

6.12 تمرينات

تمرين 6.1

اكتب عملية اسمها isDivisible تأخذ عددين صحيحين، n وm وتعيد القيمة true إذا كان n قابلاً للقسمة على m وfalse فيما عدا ذلك.

تمرين 6.2 يمكن التعبير عن العديد من العمليات المعقدة بشكل مختصر باستخدام عملية "الضرب-جمع"، التي تأخذ ثلاثة معاملات وتحسب a*b + c. حتى أن بعض المعالجات توفر معدات خاصة لتنفيذ هذه العملية للأعداد العشرية.

  1. أنشئ برنامجاً جديداً باسم Multadd.java.
  2. اكتب عملية اسمها multadd تأخذ ثلاثة معاملات من نوع double وتطبع ناتج ضرب-جمعهم.
  3. اكتب عملية main تختبر multadd باستدعائها باستخدام معاملات بسيطة، مثل 1.0، 2.0، 3.0. أيضاً في main، استعمل multadd لحساب القيم التالية:

  4. اكتب عملية تدعى yikes تأخذ عدداً عشرياً كمعامل وتستخدم multadd لحساب وطباعة

    مساعدة: العملية الرياضية التي ترفع e إلى قوة هي Math.exp.

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

إن أحد أهداف هذا التمرين هو التدرب على "مطابقة النموذج" – "pattern-matching": القدرة على التعرف على مشكلة محددة كحالة خاصة من فئة عامة من المشاكل.

تمرين 6.3

إذا أعطيتَ ثلاثة عيدان، فقد تستطيع ترتيبها بشكل مثلث وقد لا تستطيع. مثلاً، إذا كان طول أحد العيدان 12 بوصة وكان طول كل من العودين الآخرين 1 بوصة، فمن الواضح أنك لن تستطيع جعل العودين القصيرين يلتقيان في المنتصف. بالنسبة لأي ثلاثة أطوال، هناك اختبار بسيط لمعرفة ما إذا كان تشكيل مثلث منها ممكناً:

"إذا كان أي واحد من الأطوال الثلاثة أكبر من مجموع الطولين الآخرين، لا يمكنك تشكيل مثلث منها، وبخلاف ذلك تستطيع"

اكتب عملية اسمها isTriangle تأخذ ثلاثة أعداد صحيحة كمتحولات، وتعيد قيمة إما true وإما false، اعتماداً على إمكانية أو عدم إمكانية تشكيل مثلث من عيدان أطوالها تساوي الأعداد المعطاة.

الهدف من هذا التمرين هو استعمال التعليمات الشرطية لكتابة عملية مثمرة.

تمرين 6.4

ما هو خرج البرنامج التالي؟ إن الغرض وراء هذا التمرين هو التأكد من أنك تفهم العوامل المنطقية ومجرى التنفيذ في العمليات المثمرة.

public static void main (String[] args) {
  boolean flag1 = isHoopy (202);
  boolean flag2 = isFrabjuous (202);
  System.out.println (flag1);
  System.out.println (flag2);
  if (flag1 && flag2) {
    System.out.println ("ping!");
  }
  if (flag1 || flag2) {
    System.out.println ("pong!");
  }
}

public static boolean isHoopy (int x) {
  boolean hoopyFlag;
  if (x%2 == 0) {
    hoopyFlag = true;
  } else {
    hoopyFlag = false;
  }
  return hoopyFlag;
}

public static boolean isFrabjuous (int x) {
  boolean frabjuousFlag;
  if (x > 0) {
    frabjuousFlag = true;
  } else {
    frabjuousFlag = false;
  }
  return frabjuousFlag;
}

تمرين 6.5

المسافة بين نقطتين (x1, y1) و(x2, y2) هي

اكتب عملية اسمها distance تأخذ أربعة معاملات من نوع double –x1, y1, x2, y2- وتطبع المسافة بين النقطتين.

عليك أن تفترض وجود عملية اسمها sumSquares تحسب وتعيد مجموع مربعات معاملاتها، مثلاً:

double x = sumSquares (3.0, 4.0);
ستسند القيمة 25.0 للمتغير x.

الغرض من هذا التمرين هو كتابة عملية جديدة تستخدم عملية موجودة. عليك كتابة عملية واحدة فقط: distance. لا تكتب العملية sumSquares ولا main كما لا تستدعي distance.

تمرين 6.6

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

public class Prod {
  public static void main (String[] args) {
    System.out.println (prod (1, 4));
  }
  public static int prod (int m, int n) {
    if (m == n) {
      return n;
    } else {
      int recurse = prod (m, n-1);
      int result = n * recurse;
      return result;
    }
  }
}
  1. ارسم مخططاً هرمياً يبين حالة البرنامج قبيل اكتمال حالة prod الأخيرة. ما هو خرج هذا البرنامج؟
  2. اشرح باستعمال بضعة كلمات ما تفعله العملية prod.
  3. أعد كتابة prod بدون استخدام المتغيرات المؤقتة recurse وresult.

تمرين 6.7

الغرض من هذا التمرين هو ترجمة تعريف تعاودي إلى عملية مكتوبة بلغة Java. يعرف تابع أكرمان (Ackerman function) بالنسبة للأعداد غير السالبة كما يلي:

اكتب عملية اسمها ack تأخذ عددين صحيحين كمعاملات وتحسب وإعادة قيمة تابع أكرمان.

اختبر عملية أكرمان باستدعائها من main وطباعة القيمة المعادة.

تحذير: القيمة المعادة تكبر كثيراً بسرعة كبيرة. عليك اختبارها مع قيم صغيرة للمتغيرين n وm (ليس أكبر من 2).

تمرين 6.8

  1. أنشئ برنامجاً يدعى Recurse.java واكتب فيه العمليات التالية:
    // first: returns the first character of the given String
    public static char first (String s) {
      return s.charAt (0);
    }
    
    // last: returns a new String that contains all but the
    // first letter of the given String
    public static String rest (String s) {
      return s.substring (1, s.length());
    }
    
    // length: returns the length of the given String
    public static int length (String s) {
      return s.length();
    }
    
  2. اكتب بعض الشفرة في main لاختبار هذه العمليات. تأكد من أنها تعمل، وتأكد من أنك قد فهمت وظائفها.
  3. اكتب عملية باسم printString تأخذ سلسلة محرفية كمعامل وتطبع أحرف تلك السلسلة، واحد على كل سطر. يجب أن تكون عملية فارغة (void method).
  4. اكتب عملية اسمها printBackward تفعل بنفس ما تفعله printString عدا أنها تطبع السلسلة بالمقلوب (الحرف الأخير على السطر الأول).
  5. اكتب عملية اسمها reverseString تأخذ سلسلة محرفية كمعامل وتعيد سلسلة جديدة كقيمة معادة. يجب أن تحتوي السلسلة الجديدة على نفس حروف السلسلة المعطاة، لكن بترتيب معكوس. مثلاً، سيكون خرج الشفرة التالية
    String backwards = reverseString ("Allen Downey");
    System.out.println (backwards);
    
    كما يلي
    yenwoD nellA
    

تمرين 6.9

اكتب عملية تعاودية اسمها power تأخذ عدداً عشرياً x وعدداً صحيحاً n وتعيد القيمة xn.

مساعدة: التعريف التعاودي لهذه العملية هو power (x, n) = x * power (x, n-1). أيضاً، تذكر أن أي شيء مرفوع للقوة صفر يعطي 1.

تحد اختياري: يمكنك جعل هذه العملية أكثر فاعلية، عندما يكون n زوجياً، باستخدام

تمرين 6.10

(هذا التمرين مبني على الصفحة 44 من كتاب Structure and Interpretation of Computer Programs للمؤلفين Ableson and Sussman).

الخوارزمية التالية تعرف باسم خوارزمية إقليدس لأنها مكتوبة في كتاب العناصر لإقليدس (الكتاب 7، 300 ق.م.). قد تكون هذه أقدم خوارزمية معروفة(1).

تبنى الخوارزمية على الملاحظة التالية: إذا كان r باقي قسمة a على b، عندئذ تكون القواسم المشتركة للعدين a وb هي نفس القواسم المشتركة للعدين b وr. وبالتالي يمكننا استخدام المعادلة

gcd(a, b) = gcd(b, r)
لتحويل معضلة حساب ق.م.أ (القاسم المشترك الأكبر) لعددين صحيحين إلى حساب القاسم المشترك الأكبر لعددين أصغر فأصغر. مثلاً،
gcd(36, 20) = gcd(20, 16) = gcd(16, 4) = gcd(4, 0) = 4
ينتج أن القاسم المشترك الأكبر (GCD Great Common Divider) للعددين 36 و20 هو 4. يمكن البرهان على أنه من أجل أي عددين ابتدائيين، فإن تكرار هذه العملية سينتهي إلى حساب ق.م.أ لعددين يكون الثاني منهما يساوي الصفر. ويكون ق.م.أ المطلوب هو العدد الأول منهما.

اكتب عملية اسمها gcd تأخذ عددين صحيحين كمعاملين وتستعمل خوارزمية إقليدس لحساب وإرجاع القاسم المشترك الأكبر للعددين.

(1) لتعرف ما هي "الخوارزمية"، انظر القسم 11.13

السابقالفهرسالتالي