تعلّم البرمجة بلغة كوتلن (49): الدوال ذات المرتبة الأعلى Higher-Order Functions

تعلّم البرمجة بلغة كوتلن (49): الدوال ذات المرتبة الأعلى Higher-Order Functions
أستمع الى المقال

تعرّفنا حتى الآن في هذه الدورة لتعلم لغة البرمجة كوتلن، على عدة أنواع من الدوال، مثل: الدوال المنتمية لصنف معيّن Member Functions، والتي يمكننا كتابتها داخل صنف ونحتاج إلى إنشاء كائن من هذا الصنف لإستخدام هذا النوع من الدوال. ومنها الدوال ذات المستوى الأعلى، والتي يمكننا كتابتها منفصلة في أي ملف في مشروع البرنامج وتعرف بـ Top-Level Functions. وهذا النوع يمكننا الوصول إليه مباشرةً من أي مكان داخل الملف، أو داخل ملفات المشروع الأخرى فقط نحتاج تضمين رابط الحزمة package التي تتواجد بها الدالة.

ثم بداية من هذا القسم والذي يختص بتطبيقات البرمجة الوظيفية في كوتلن، تعرّفنا على كيفية إنشاء تعابير اللامبدا Lambda Expressions، وتعابير مرجع الدوال Functions Reference، وإرسالهما إلى دوال أخرى لديها معامل من نوع بيانات الدوال.

ومن ضمن هذه الدوال التي لديها معامل من نوع بيانات الدالة، استخدمنا دوال مثل: ()filter و ()filterNot و ()map و ()any و ()all …الخ. والتي شرحناها تفصيليًا في الدروس الثلاث السابقة. وكل هذه الدوال تأتي مجهزة ومختبرة من فريق لغة كوتلن، وتم إدراجها في المكتبة القياسية للغة.

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

الدوال ذات المرتبة الأعلى Higher-Order Functions:

تعتبر الدوال في مشروع كوتلن، مكونات من الدرجة الأولى أو First-Class، تمامًا مثل الكائنات. لذلك يمكننا إسنادها لمتغير أو إرسالها إلى دوال أخرى. هذه الدوال التي لديها إمكانية إستقبال تعبير لامبدا أو مرجع دالة كقيمة Argument، هي التي تعرف بالدوال ذات المرتبة الأعلى.

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

الدوال ذات المرتبة الأعلى في المكتبة القياسية للغة كوتلن:

وقبل البدء في إنشاء هذه الدوال، دعونا نتعرف على طريقة فريق كوتلن، في كتابة بعض الدوال التي استخدمناها في الدروس السابقة، مثل دالة ()any. والتي عملها هو فحص عناصر القائمة List التي تستدعيها، والتأكد مما إذا كان أيًا منها، يوافق الشرط أو القاعدة المرسلة لها عبر اللامبدا أو مرجع الدوال. إذا كان هذا صحيحًا، ستعيد لنا القيمة true. أمّا إذا لم يوافق أيًا منها الشرط، ستعيد القيمة false. كما شرحناها في هذه الفقرة من درس سابق.

ويكون أبسط استخدام لها كالتالي:

لدينا قائمة numbers وبها عدة أرقام. نريد أن نتأكد ممّا إذا كانت القائمة تحتوي على أية أرقام موجبة positive أم لا. لذا، استدعينا دالة ()any عبر قائمة numbers. ولأن لدى الدالة معامل من نوع بيانات الدوال، أرسلنا لها تعبير اللامبدا، كقيمة لهذا المعامل.

الشرط في تعبير اللامبدا، هو ما إذا كان هناك أي عنصر في القائمة numbers أكبر من صفر. مثّلنا عناصر القائمة في تعبير اللامبدا، بالمتغير num والذي بالطبع نوع بياناته هو نفس نوع بيانات عناصر القائمة، النوع Int.

إذًا، كيف ستطبق دالة ()any نتيجة شرط اللامبدا، وكيف ستعيد لنا القيمة true إذا كانت هناك أرقام موجبة في القائمة، أو false إذا لم توجد؟ لفهم ذلك، يجب أن نلقي نظرة إلى تعريف الدالة، الذي سنشرحه في الفقرة التالية.

تعريف دالة ()any:

الشكل التالي، هو الشكل التقريبي لدالة ()any الخاصة بتجميعة القوائم Lists في مكتبة كوتلن القياسية. تجاهلنا بعض الإضافات الأخرى في تعريف الدالة، لأنها لا تؤثر في فهم عمل الدالة الأساسي، ولأننا لم ندرس هذه الإضافات بعد:

كما نرى، هي دالة مُعَمَمّة ملحقة بالتجميعة List. لذا، يُمكن استخدامها مع تجميعة List والتي يمكن أن تكون عناصرها من أي نوع بيانات أو أي كائن من صنف خاص. ثم لدى الدالة، معامل اسمه predicate ونوع بياناته هو:

(T) -> Boolean

وهو من نوع بيانات الدوال الذي شرحناه في درس اللامبدا. إذًا، المعامل predicate هو نفسه عبارة عن دالة، لديها معامل عام من النوع T، وتعيد قيمة منطقية Boolean، إما true أو false. وهو نفس المعامل الذي أرسلنا له تعبير اللامبدا السابق كقيمة.

ودالة ()any نفسها أيضًا تعيد قيمة من النوع Boolean. لذا، داخل أقواس الدالة المعقوفة، تم استخدام الكلمة return، لإعادة إمّا true أو false.

ولدينا في أول سطر، كتلة if الشرطية، والتي مهمتها هي إذا كانت this فارغة isEmpty، أوقف عمل الدالة، وأعد return القيمة false. أمّا غير ذلك، فنفذ السطر التالي. والكلمة this هنا تعني قائمة List التي تستدعي دالة ()any في هذه اللحظة.

في السطر التالي، لدينا حلقة for التكرارية. ستدور الحلقة على عناصر القائمة عنصرًا بعد عنصر، وفي كل دورة سيُمثل المتغير element العنصر الحالي. هذا العنصر سيتم إرساله إلى معامل دالة predicate. فإذا كانت القيمة العائدة من predicate هي true، سيتم إعادة true وإيقاف عمل الدالة كلها. أمّا إذا أعادت دالة predicate القيمة false، سيتم تنفيذ السطر الأخير في الدالة، والذي سيعيد القيمة false وينتهي عمل الدالة.

ولكن، كيف تستخدم دالة predicate العنصر element الذي نرسله إليها في كل دورة للحلقة، لتعيد لنا القيمة true أو false؟ هذا هو الجزء الذي يجعل من دالة ()any، دالة ذات مرتبة أعلى. لو فهمنا هذا الجزء، سيصبح من السهل علينا إنشاء دوال ذات مرتبة أعلى خاصة بنا. لذلك، دعونا نشرحها تفصيليًا، في الفقرة التالية.

استخدام المعامل predicate كدالة:

عند استدعاء دالة ()any عبر القائمة numbers في دالة ()main، أرسلنا تعبير لامبدا ليكون قيمة لمعاملها الذي يحمل اسم predicate. هذا يعني، كأننا أسندنا إلى هذا المعامل تعبير اللامبدا مباشرةً كالتالي:

هذا بالضبط، هو ما فعلناه عند إرسال اللامبدا لهذا المعامل. سيتم أيضًا، تعويض مكان النوع (T) في نوع بيانات predicate، بالنوع Int. لأن عناصر numbers، هي من هذا النوع.

وكما نعرف من درس اللامبدا، أنه يمكننا أن نستخدم هذا المتغير (المعامل) من نوع بيانات الدوال، كالتالي:

predicate(14)

سيأخذ تعبير اللامبدا، الرقم 14 المُرسل عبر predicate، ويسنده لمعامله num. ثم ينفذ التعبير المنطقي:

num > 0

وبما أن num يساوي 14، ستكون نتيجة التعبير هي true.

وبما أن المعامل predicate لديه معامل واحد فقط، يمكن أن نستبدل الكلمة num بالكلمة it:

predicate: (Int) -> Boolean = { it > 0 }

استدعاء المعامل في حلقة for:

وبعد أن تعرّفنا على كيفية عمل المعامل predicate، دعونا ننظر مرة أخرى لحلقة for في دالة ()any:

for (element in this) {
    if (predicate(element))
        return true
}

كما قلنا سابقًا، أن متغير الحلقة element سيُمثِّل عنصر من القائمة numbers في كل دورة للحلقة. ففي أول دورة ستكون قيمته هي قيمة العنصر الأول في القائمة، وفي الدورة الثانية ستكون قيمته هي قيمة العنصر الثاني في القائمة …وهكذا حتى آخر عنصر في القائمة numbers.

ولأن أول عنصر في قائمة numbers هو الرقم 13، سيتم تعويض قيمة element به، ثم يتم إرسال قيمة element إلى دالة predicate. وكما رأينا في الفقرة السابقة، أن دالة predicate هي في الأصل عبارة عن معامل تم إسناد تعبير لامبدا إليه، لذا سيتم تعويض معامل اللامبدا num بالرقم 13 وتطبيق الشرط num > 0 عليه. 

ولأن الرقم 13 أكبر من الصفر، ستكون نتيجة التعبير هي true. وهي القيمة التي ستعيدها دالة predicate. وبما أن التعبير بين قوسي كتلة if المتواجدة في حلقة for نتيجته true، ستنفذ كتلة if السطر الذي يليها وهو إعادة القيمة true. سيتم إنهاء عمل الدالة عبر return ولن يتم تنفيذ أي شيء آخر بعد هذه الكلمة. (المزيد عن الكلمة return في درس أوامر القفز والعودة).

وهذا بالضبط هو ما تقوم به دالة ()any الخاصة بكوتلن. أي عندما تجد أن هناك عنصر في القائمة يوافق الشرط، ستعيد القيمة true وتتجاهل باقي العناصر. أمّا إذا كانت القائمة التي تستدعيها فارغة أو لم فيها تجد عنصر يوافق الشرط، ستعيد false.

إنشاء دوال ذات مرتبة أعلى:

كمثال، إذا أردنا إنشاء دالة لجمع عددين Int، وأردنا أن نرسل إليها تعبير لامبدا أو تعبير مرجع دالة، فيمكننا إنشائها كالتالي:

اخترنا للدالة اسم ()sumOfNumbers. وهي كما نلاحظ دالة ذات مستوى أعلى Top-Level Function. لدى الدالة ثلاث معاملات، المعاملين الأولين (a, b) من النوع Int لإستقبال الرقمين المُراد جمعهما، والثالث action هو من نوع بيانات الدوال، أي يمكننا أن نرسل إليه  تعبير لامبدا أو تعبير مرجع دالة. ولهذا السبب، تعتبر دالة ()sumOfNumbers ذات مرتبة أعلى.

ويمكننا اعتبار أن المعامل action، هو دالة لديها معاملين من النوع Int، وتعيد قيمة من النوع Int أيضًا. أمّا نوع بيانات القيمة التي ترجعها الدالة ()sumOfNumbers، هي Int. وبما انه هو نفس نوع إرجاع دالة action، يمكننا إرجاع دالة action نفسها، باستخدام الكلمة return، بعد أن نرسل إليها المعاملين a و b التابعين لدالة ()sumOfNumbers.

أمّا كيف ستقوم دالة action باستخدام المعاملين a و b، هو ما سنحدده نحن عند استدعاء دالة ()sumOfNumbers:

نستدعي دالة ()sumOfNumbers، ونرسل لها رقمين وتعبير لامبدا. ستمرر دالة ()sumOfNumbers الرقمين إلى دالة action. ثم ستمرر الدالة action نفس قيمة المعاملين، إلى تعبير اللامبدا. وتعبير اللامبدا، سيعمل على جمع الرقمين، وإعادة قيمة الجمع. القيمة المُعادة هذه، هي التي سيتم إسنادها للمتغير twoNumsSum. بالتالي، عند طباعة قيمة هذا المتغير، ستكون النتيجة هي العدد 3.

هذه هي أبسط صورة للدوال ذات المرتبة الأعلى. وبالطبع يمكننا جعل دالة ()sumOfNumbers ملحقة بقائمة List مثلًا، ثم استخدام حلقة for لجمع كل عناصر القائمة من الأعداد الصحيحة.

دالة ذات مرتبة أعلى ملحقة:

إذا كنا نريد من دالة ()sumOfNumbers جمع كل عناصر قائمة تحتوي على الأعداد الصحيحة، يمكننا إضافتها كدالة ملحقة، بالقائمة List:

غيرنا في توقيع الدالة، لأنه بعد إلحاقها بالقائمة List، لم نعد بحاجة للمعاملين السابقين، لأنها ستعمل على جمع عناصر القائمة وليس رقمين فقط. أما معامل action، فغيرنا في توقيعه هو الآخر، لنجعله يستقبل قيمة عامة باستخدام النوع العام T. ولم نغيّر نوعي الإرجاع للدالة ومعامل action، وهما من النوع Int كالسابق.

وعند استدعاء هذه الدالة مع قائمة من النوع Int، ستنشئ الدالة متغير محلي result قابل لتغيير قيمته لأنه var. ثم عبر الحلقة for، ستدور على عناصر القائمة وترسلها للمعامل action. ستطبق action الشرط الذي سنضعه لها لاحقًا، على كل عنصر على حدى، وتعيد نتيجة من النوع Int. ثم يتم إضافة النتيجة العائدة من action، إلى المتغير result، عبر عامل الإضافة والإسناد ( =+ ).

وبعد الإنتهاء من كل عناصر القائمة، سينتهي عمل الحلقة، ويتم تنفيذ السطر الذي يأتي بعدها، وهو إعادة قيمة result.

ويمكننا استدعاء دالة ()sumOfNumbers، كالتالي:

لدينا قائمة intsNumbers تحتوي على خمسة عناصر فقط، وهي الأعداد من 1 – 5. داخل دالة ()println، استخدمنا طريقة القوالب النصية، لطباعة قيمة استدعاء دالة ()sumOfNumbers عبر القائمة. مع وضع شرط مختلف في تعبير اللامبدا، في كل استدعاء.

وعند تنفيذ هذه الشفرة، ستعيد لنا دالة ()sumOfNumbers، نتيجة مختلفة حسب كل شرط:

الخلاصة:

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

لذلك في الدرس القادم، سنشرح المزيد من هذه الدوال المهمة، وكيفية استغلالها في التعامل مع القوائم.

هذا الدرس هو جزء من سلسلة تعليم مبادئ البرمجة بلغة كوتلن. لمُتابعة الدروس منذ البداية ومُشاهدة فهرس المحتويات يمكنك الانتقال إلى الدرس الأول من هنا.

هل أعجبك المحتوى وتريد المزيد منه يصل إلى صندوق بريدك الإلكتروني بشكلٍ دوري؟
انضم إلى قائمة من يقدّرون محتوى إكسڤار واشترك بنشرتنا البريدية.