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

تعبيرات لامدا (Lambda Expressions) في جافا


رضوى العربي

يُخْزَّن البرنامج الفرعي (subroutine) بذاكرة الحاسوب على هيئة سِلسِلة نصية طويلة مُكوَّنة من الرقمين صفر وواحد، ولذلك فإنه لا يَختلف كثيرًا عن البيانات (data)، مثل الأعداد الصحيحة والسَلاسِل النصية والمصفوفات، والتي تُخْزَّن أيضًا بنفس الطريقة. ربما اِعتدت التفكير بكُلًا من البرامج الفرعية والبيانات على أساس أنهما شيئان مختلفان تمامًا، ولكن في الواقع يَتعامَل الحاسوب مع البرنامج الفرعي على أساس أنه مجرد نوع آخر من البيانات. تَسمَح حتى بعض اللغات البرمجية بالتَعامُل مع البرامج الفرعية بنفس الطريقة التي يَتعامَل معها الحاسوب، وهو ما أُضيف إلى الجافا على هيئة تعبيرات لامدا (lambda expressions) منذ الإصدار ٨.

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

الدوال الكيانية (first-class functions)

يُمكِننا كتابة دالة لحِسَاب قيمة مربع العدد، بحيث تَستقبِل ذلك العدد كمُعامِل صُّوري أو وهمي (dummy parameter)، كالتالي:

static double square( double x ) {
    return x*x;
}

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

لامدا (lambda) هو أحد حروف الأبجدية الإغريقية (Greek alphabet)، والذي اِستخدَمه عالم الرياضيات ألونزو تشرتش (Alonzo Church) أثناء دراسته للدوال القابلة للحِسَاب (computable functions). على سبيل المثال، إذا كنا نُريد حِسَاب قيمة مربع عدد، وليكن x، فقد تَظُنّ أن الترميز x2 يُمثِل "دالة" تَحسِب مربع x، لكنه، في الواقع، ليس سوى تعبير (expression) يُمثِل "نتيجة" حِسَاب مربع x. قَدَّمَ ألونزو تشرتش (Alonzo Church) ترميز لامدا (lambda notation) والذي يُمكِن اِستخدَامه لتعريف دالة (function) بدون تَخْصيص اسم لها. بالتحديد اِستخدَم الترميز lambda(x).x2 (في الواقع يُستخدَم الحرف الإغريقي لامدا ذاته وليس الكلمة نفسها) للإشارة إلى "دالة x معطاة بواسطة x2". يُعدّ هذا الترميز دالة مجرَّدة (function literal)، أيّ تُمثِل قيمة من النوع "دالة (function)" بنفس الطريقة التي تُعدّ بها القيمة 42 عددًا صحيحًا مُجرَّدًا (integer literal) يُمثِل قيمة من النوع int.

ينبغي أن تَدْفَعك الدوال المُجرَّدة (function literals) إلى التَفكير بالدوال (function) على أنها مُجرَّد نوع آخر من القيم، وعندما تَصِل إلى تلك المرحلة، فإنك ستَكُون قادرًا على تَطْبِيق نفس تلك الأشياء التي اعتدت إجرائها على القيم الآخرى على الدوال أيضًا، كإِسْناد دالة إلى مُتَغيِّر، أو تمرير دالة كمُعامِل (parameter) إلى برنامج فرعي (subroutine)، أو إعادة دالة كقيمة من برنامج فرعي، أو حتى إنشاء مصفوفة دوال (array of functions). تَسمَح بعض لغات البرمجة بالقيام بكل تلك الأشياء، ويُقال عندها أنها تُدعِّم "الدوال الكيانيَّة (first-class functions)" أو أنها تُعامِل الدوال بعدّها كائنات كيانيَّة (first-class objects).

تُوفِّر لغة الجافا كل تلك الأشياء من خلال تعبيرات لامدا (lambda expressions)، والتي تَختلِف صيغتها عن الترميز الذي اِستخدَمه ألونزو تشرتش (Alonzo Church)، فهي الواقع، لا تَستخدِم حتى كلمة لامدا (lambda) على الرغم من مُسماها. تُكتَب دالة حِسَاب مربع العدد بتعبير لامدا (lambda expression) بلغة الجافا، كالتالي:

x  ->  x*x

يُنشِئ العَامِل ‎->‎ تعبير لامدا، بحيث تَقَع المُعامِلات الوهمية أو الصُّوريّة (dummy parameter) على يسار العَامِل، بينما يَقَع التعبير (expression) المسئول عن حِسَاب قيمة الدالة على يمينه. قد ترى تعبير لامدا بالأعلى مُمرَّرًا كمُعامِل فعليّ (actual parameter) إلى برنامج فرعي، أو مُسنَد إلى مُتَغيِّر، أو مُعاد من دالة.

إذًا، هل الدوال بلغة الجافا كيانيَّة (first-class)؟ لا تَتَوفَّر إجابة واضحة على هذا السؤال، لأن لغة الجافا لا تُدعِّم بعض الأشياء الآخرى المُتوفِّرة ضِمْن لغات برمجية آخرى. فمثلًا، في حين تستطيع إِسْناد تعبير لامدا المُعرَّف بالأعلى إلى مُتَغيِّر اسمه sqr، فإنك لن تستطيع بَعْدها اِستخدَام ذلك المُتَغيِّر كما لو كان دالة فعليّة، أيّ لا تستطيع كتابة sqr(42)‎. لغة الجافا عمومًا هي لغة صارمة في تَحْدِيد النوع (strongly typed)، ولذلك لكي تَتَمكَّن من إِنشاء المُتَغيِّر sqr، فعليك التَّصْريح عنه أولًا وهو ما يَتضمَّن تَخْصيص نوعه، فيا تُرى ما هو ذلك النوع الذي يَتناسب مع قيمة هي عبارة عن دالة؟ تُوفِّر الجافا ما يُعرَف باسم واجهة نوع الدالة (functional interface) لهذا الغرض، وسنناقشها بَعْد قليل.

ملاحظة أخيرة: على الرغم من ارتباط مصطلح الدالة (function) بتعبيرات لامدا (lambda expressions) بدلًا من مصطلحات مثل برنامج فرعي (subroutine) أو تابع (method)، فإنها في الواقع لا تَقْتصِر على تمثيل الدوال (functions)، وإنما يُمكِنها تمثيل أي برنامج فرعي (subroutines)، وبصورة عشوائية.

واجهات نوع الدالة (functional interfaces)

كي تَتَمكَّن من اِستخدَام برنامج فرعي (subroutine) معين، فلابُدّ أن تَعرِف كُلًا من اسمه، وعدد المُعامِلات (parameters) التي يَستقبِلها، وأنواعها، بالإضافة إلى نوع القيمة المُعادة (return type) من ذلك البرنامج الفرعي. تُوفِّر لغة الجافا ما يُعرَف باسم واجهات نوع الدالة (functional interface)، وهي أَشْبَه بالأصناف (class)، حيث تُعرَّف بملف امتداده هو ‎.java كأي صَنْف، وتَتضمَّن تلك المعلومات المذكورة بالأعلى عن برنامج فرعي وحيد. انظر المثال التالي:

public interface FunctionR2R {
    double valueAt( double x );
}

تُصرِّح الشيفرة بالأعلى عن واجهة نوع الدالة FunctionR2R، والموجودة بملف يَحمِل اسم FunctionR2R.java. تُصرِّح تلك الواجهة عن الدالة valueAt، والتي تَستقبِل مُعامِل وحيد من النوع double، وتُعيد قيمة من النوع double. تَفرِض قواعد الصيغة تَخْصيص اسم للمُعامِل -x بالأعلى- على الرغم من أنه في الواقع ليس ذا أهمية هنا، وهو ما قد يَكُون مزعجًا نوعًا ما. ها هو مثال آخر:

public interface ArrayProcessor {
    void process( String[] array, int count );
}

تَتضمَّن لغة الجافا الكثير من واجهات نوع الدالة القياسية (standard functional interfaces) بصورة افتراضية. تُعدّ واجهة نوع الدالة Runnable واحدة من أهم تلك الواجهات، وأبسطها، والمُعرَّفة كالتالي:

public interface Runnable {
    public void run();
}

سنَستخدِم واجهات نوع الدالة (functional interfaces) الثلاثة بالأعلى للأمثلة التالية بهذا القسم.

تُوفِّر لغة الجافا أيضًا ما يُعرَف باسم الواجهات (Interfaces)، والتي هي في الواقع أعم وأشمل، وربما أكثر تعقيدًا من واجهات نوع الدالة (functional interfaces). سنتعلَّّم المزيد عنها بالقسم ٥.٧. سنُقصِر حديثنا بهذا القسم على واجهات نوع الدالة (functional interfaces)؛ لارتباطها بموضوعنا الرئيسي: تعبيرات لامدا (lambda expressions).

يُعدّ اسم واجهة نوع الدالة نوعًا (type)، تمامًا كالأنواع String و double، وكأيّ نوع، فإنه يُمكِن اِستخدَامه للتَّصْريح عن المُتَغيِّرات، والمُعامِلات، وكذلك لتَخْصيص نوع القيمة المُعادة (return type) من الدوال، ويُمكِن إِسْناد تعبيرات لامدا (lambda expression) كقيم للمُتَغيِّرات من ذلك النوع. في الواقع، تَتضمَّن واجهة نوع الدالة (functional interface) قالبًا (template) لبرنامج فرعي وحيد، وينبغي لتعبير لامدا (lambda expression) المُسنَد أن يَكُون مُتطابِقًا مع ذلك القالب.

تعبيرات لامدا (lambda Expressions)

يُعدّ أي تعبير لامدا (lambda expression) تمثيلًا لبرنامج فرعي مجهول الاسم (anonymous subroutine)، أيّ لا يَمتلك اسمًا، ولكنه في المقابل، وكأيّ برنامج فرعي، لديه قائمة من المُعامِلات الصُّوريّة (formal parameter list)، بالإضافة إلى التعريف (definition) نفسه، ويُكتَب عمومًا بالصيغة (syntax) التالية:

( <parameter-list> )  ->  { <statements> }

كما هو الحال مع البرامج الفرعية العادية، يُمكِن لقائمة المُعامِلات -بالأعلى- أن تَكُون فارغة، أو قد تتكوَّن من تَّصْريح عن مُعامِل (parameter declaration) وحيد أو أكثر، بحيث يُفصَل بين كل تَّصْريح والذي يليه بفاصلة (comma)، وبحيث يَتكوَّن كل تَّصْريح من نوع المُعامِل متبوعًا باسمه. في الواقع، تُبسَّط عادة تلك الصيغة وفقًا لمجموعة من القواعد: أولًا: يُمكِن حَذْف أنواع المُعامِلات إذا كان استنباطها من السِّياق مُمكِنًا. على سبيل المثال، إذا كان هناك تعبير لامدا مُعرَّف على أنه من النوع FunctionR2R، فإن مُعامِل التعبير لابُدّ وأن يَكُون من النوع double، ولذلك فإنه، في تلك الحالة، ليس من الضروري تَخْصيص نوع المُعامِل ضِمْن تعبير اللامدا. ثانيًا، يُمكِن حَذْف الأقواس () حول قائمة المُعامِلات (parameter list) إذا كانت مُكوَّنة من مُعامِل وحيد غَيْر مُحدَّد النوع. ثالثًا، يُمكِن حَذْف الأقواس {} الموجودة على الجانب الأيمن من العَامِل ‎->‎ إذا كانت تَحتوِي على تَعْليمَة استدعاء برنامج فرعي (subroutine call) وحيدة. أخيرًا، إذا كان الجانب الأيمن من العَامِل ‎->‎ مكتوبًا بالصيغة { return <expression>;‎ }، فيُمكِن تبسيطها إلى التعبير وحَذْف أي شيء آخر.

بفَرْض أننا نريد كتابة دالة لحِسَاب مربع القيم من النوع double، فإن نوع تلك الدالة هو من نفس نوع واجهة نوع الدالة FunctionR2R المُعرَّفة بالأعلى. الآن، إذا كان sqr مُتَغيِّرًا من النوع FunctionR2R، فإننا نستطيع أن نُسنِد تعبير لامدا (lambda expression) كقيمة لذلك المُتَغيِّر، ويُعدّ عندها ذلك التعبير تمثيلًا للدالة المطلوبة. يُمكِن القيام بذلك بأي من الصِيَغ التالية:

sqr = (double x) -> { return x*x; };
sqr = (x) -> { return x*x; };
sqr = x -> { return x*x; };
sqr = x -> x*x;
sqr = (double fred) -> fred*fred;
sqr = (z) -> z*z;

تُعرِّف تعبيرات لامدا (lambda expressions) الستة بالأعلى نفس الدالة بالضبط، وأُضيفت آخر تَعْليمَتين (statements) خصيصًا؛ للتأكيد على أن أسماء المُعامِلات غير مهمة، فهى بالنهاية مجرد مُعامِلات وهمية أو صُّوريّة (dummy parameters). لمّا كان المُصرِّف (compiler) على علم بكون المُتَغيِّر sqr من النوع FunctionR2R، ولأن النوع FunctionR2R يتطلَّب مُعامِلًا من النوع double، فقد تَمَكَّنا من حَذْف نوع المُعامِل double. يُمكِن اِستخدَام تعبيرات لامدا فقط ضِمْن السِّياقات التي يستطيع فيها المُصرِّف (compiler) استنباط نوعها، ولذلك لابُدّ من كتابة نوع المُعامِل إذا كان حَذْفه سيَتسبَّب بغموض نوع التعبير.

لا يُعدّ المُتَغيِّر sqr -كما هو مُعرَّف بالأعلى- دالة (function) تمامًا، وإنما هو قيمة من النوع FunctionR2R، وبحسب ما هو مُخصَّص بتعريف الواجهة (interface definition)‏ FunctionR2R، فإن ذلك المُتَغيِّر لابُدّ وأن يَحتوِي على دالة تَحمِل اسم valueAt. يمكن استدعاء تلك الدالة من خلال اسمها الكامل sqr.valueAt، فمثلًا يُمكِن كتابة sqr.valueAt(42)‎ أو sqr.valueAt(x) + sqr.valueAt(y)‎.

إذا كانت قائمة مُعامِلات تعبير لامدا مُكوَّنة من أكثر من مُعامِل واحد، فإن الأقواس () المُحيطة بقائمة المُعامِلات (parameters list) لم تَعُدْ اختيارية. اُنظر المثال التالي، والذي يَستخدِم واجهة نوع الدالة ArrayProcessor، كما يُبيِّن طريقة كتابة تعبير لامدا عندما يكون تعريفها مُتعدِّد الأسطر (multiline definition):

ArrayProcessor concat;
concat = (A,n) -> { // الأقواس هنا مطلوبة
    String str;
    str = "";
    for (int i = 0; i < n; i++)
        str += A[i];
    System.out.println(str);
};  // الفاصلة المنقوطة هنا ليست جزءًا من تعبير لامدا
    // وإنما تشير إلى انتهاء تعليمة الإسناد

String[] nums;
nums = new String[4];
nums[0] = "One";
nums[1] = "Two";
nums[2] = "Three";
nums[3] = "Four";
for (int i = 1; i < nums.length; i++) {
   concat.process( nums, i );
}

مما سينتج عنه الخَرْج التالي:

One
OneTwo
OneTwoThree
OneTwoThreeFour

تُصبِح الأمور أكثر تشويقًا عند تمرير تعبير لامدا كمُعامِل فعليّ (actual parameter)، وهو في الواقع الاِستخدَام الأكثر شيوعًا لتعبيرات لامدا. افترض أن لدينا الدالة التالية مثلًا:

/** 
 * احسب قيمة التعبير‫ f(start) + f(start+1) + ... + f(end)
 * حيث‫ f عبارة عن دالة تُستقبل كمعامل
 * ‫قيمة المعامل end لابد أن تكون أكبر من أو تساوي start
 */
static double sum( FunctionR2R f, int start, int end ) {
    double total = 0;
    for (int n = start; n <= end; n++) {
        total = total + f.valueAt( n );
    }
    return total;
}

لمّا كانت f من النوع FunctionR2R، فإن قيمة f عند n تُكتَب على الصورة f.valueAt(n)‎. يُمكِن تمرير تعبير لامدا (lambda expression) كقيمة للمُعامِل الأول عند استدعاء الدالة sum، كالتالي:

System.out.print("The sum of n squared for n from 1 to 100 is ");
System.out.println( sum( x -> x*x, 1, 100 ) );
System.out.print("The sum of 2 raised to the power n, for n from 1 to 10 is ");
System.out.println( sum( num -> Math.pow(2,num), 1, 10 ) );

كمثال آخر، افترض أن لدينا برنامجًا فرعيًا (subroutine) ينبغي أن يُنفِّذ مُهِمّة (task) مُعطاة عدة مرات، يُمكِننا ببساطة تمرير تلك المُهِمّة كمُعامِل من النوع Runnable، كالتالي:

static void doSeveralTimes( Runnable task, int repCount ) {
    for (int i = 0; i < repCount; i++) {
        task.run();  // نفذ المهمة
    }
}

نستطيع الآن طباعة السِلسِلة النصية "Hello World" عشر مرات باستدعاء التالي:

doSeveralTimes( () -> System.out.println("Hello World"), 10 );

تَتَكوَّن قائمة مُعامِلات (parameter list) تعبيرات لامدا من النوع Runnable من زوج فارغ من الأقواس ()؛ وذلك لتتماشى مع البرنامج الفرعي المُصرَّح عنه ضِمْن واجهة نوع الدالة Runnable.

اُنظر المثال التالي:

doSeveralTimes( () -> { 
    int count = 5 + (int)(21*Math.random()); 
    for (int i = 1; i <= count; i++) { 
        System.out.print(i + " ");
    }
    System.out.println();
}, 100);

على الرغم من أن الشيفرة بالأعلى قد تبدو معقدة نوعًا ما، إلا أنها، في الواقع، مُكوَّنة من تَعْليمَة وحيدة هي تَعْليمَة استدعاء البرنامج الفرعي doSeveralTimes. يَتَكوَّن مُعامِل البرنامج الفرعي الأول من تعبير لامدا (lambda expression) قد امتد تعريفه إلى عدة أسطر، أما مُعامِله الثاني فهو القيمة ١٠٠، وتُنهِي الفاصلة المنقوطة (semicolon) بنهاية آخر سطر تَعْليمَة استدعاء البرنامج الفرعي (subroutine call).

اطلعنا على عدة أمثلة تُسنَد فيها تعبيرات لامدا (lambda expression) إلى مُتَغيِّرات، وآخرى تُمرَّر فيها تعبيرات لامدا كمُعامِلات فعليّة (actual parameter). تَبَقَّى الآن أن نراها مُستخدَمة كقيمة مُعادة (return value) من دالة. اُنظر المثال التالي:

static FunctionR2R makePowerFunction( int n ) {
   return x -> Math.pow(x,n);
}

وبالتالي، ستُعيد makePowerFunction(2)‎ قيمة من النوع FunctionR2R تَحسِب مربع قيمة المُعامِل المُمرَّر، بينما ستُعيد makePowerFunction(10)‎ قيمة من النوع FunctionR2R تَحسِب قيمة المُعامِل المُمرَّر مرفوعًا للأس ١٠. يُوضِح هذا المثال أيضًا أن تعبيرات لامدا (lambda expression) تستطيع اِستخدَام مُتَغيِّرات آخرى إلى جانب مُعامِلاتها، مثل n في المثال بالأعلى. (في الواقع هناك بعض القيود لتَحْدِيد متى يُمكِن القيام بذلك).

مراجع التوابع (Method References)

لنفْترِض أننا نريد كتابة تعبير لامدا (lambda expression) من النوع FunctionR2R، بحيث يُمثِل دالة تَحسِب الجذر التربيعي. نستطيع كتابته على الصورة x -> Math.sqrt(x)‎، ولكننا إذا دققنا النظر، سنَجِدْ أن تعبير لامدا، في تلك الحالة تحديدًا، ليس سوى مجرد مُغلِّف (wrapper) بسيط للدالة Math.sqrt الموجودة بالفعل. ولهذا تُوفِّر الجافا ما يُعرَف باسم مَراجِع التوابع (method reference)؛ لاِستخدَامها في الحالات المشابهة (تَذَكَر أن "التابع [method]" هو مجرد كلمة آخرى للإشارة إلى "البرنامج الفرعي [subroutine]" بلغة الجافا). نستطيع مثلًا اِستخدَام المَرجِع التابعي Math::sqrt، والذي يُعدّ اختصارًا لتعبير اللامدا المذكور سلفًا، أينما أَمْكَن اِستخدَام تعبير اللامدا المناظر، كتمريره مثلًا كمُعامِل للدالة sum المُعرَّفة بالأعلى، كالتالي:

System.out.print("The sum of the square root of n for n from 1 to 100 is ");
System.out.println( sum( Math::sqrt, 1, 100 ) );

فقط لو أَمْكَننا اِستخدَام الاسم Math.sqrt هنا بدلًا من الاستعانة بترميز (notation) جديد ::! في الواقع، الترميز Math.sqrt مُعرَّف بالفعل ضِمْن الصَنْف Math لكن للإشارة إلى مُتَغيِّر اسمه sqrt. يُمكِن عمومًا اِستخدَام مَراجِع التوابع (method reference) بدلًا من تعبيرات لامدا (lambda expression) التي يَقْتصِر دورها على اِستدعاء أحد التوابع الساكنة (static method) الموجودة مُسْبَّقًا. تُكتَب مَراجِع التوابع على الصورة التالية:

<classname>::<method-name>

لا يَقْتصِر هذا الترميز (notation) على التوابع الساكنة، أي تلك المُنتمية للأصناف (classes)، وإنما يَمْتَدّ إلى تلكم المُنتمية للكائنات (objects) كذلك. على سبيل المثال، إذا كان str متغير من النوع String، فإن str بطبيعة الحال سيَحتوِي على التابع str.length()‎، وفي هذه الحالة، يُمكِن اِستخدَام المَرجِع التابعي str::length كتعبير لامدا من واجهة نوع الدالة SupplyInt المُعرَّفة كالتالي:

public interface SupplyInt {
    int get( );
}

ترجمة -بتصرّف- للقسم Section 5: Lambda Expressions من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب 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.


×
×
  • أضف...