الفرق بين المراجعتين ل"Kotlin/lambdas"

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث
(إضافة فقرات للصفحة)
(إضافة فقرات)
سطر 64: سطر 64:
  
 
=== أنواع الدوال (Function Types) ===
 
=== أنواع الدوال (Function Types) ===
 +
يجب تحديد نوع المتحول في الدالة من النوع "دالة" حتى يقبل تمرير الدالة عبره، إذ سيعرَّف التابع <code>max</code> المذكور سابقًا كما يلي:<syntaxhighlight lang="kotlin" line="1">
 +
fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
 +
    var max: T? = null
 +
    for (it in collection)
 +
        if (max == null || less(max, it))
 +
            max = it
 +
    return max
 +
}
 +
</syntaxhighlight>إنّ المتحول <code>less</code> من النوع ‎<code>(T, T) -> Boolean</code> والذي هو دالة بمتحولين من النوع <code>T</code> تعيد قيمةً ثنائيّةً (boolean) بالقيمة <code>true</code> إن كان المتحول الأوّل بقيمةٍ أصغر من قيمة المتحول الثاني.
 +
 +
وقد استُخدِم المتحول <code>less</code> في السطر الرابع كدالةٍ مُستدعَاة عبر تمرير المتحولين من النوع <code>T</code>، إذ يكتب نوع الدالة كما في الشيفرة السابقة، أو قد يُكتب بمتحولاتٍ مُسمَّاة (named parameters) عند الرغبة بتوثيق المعنى لكلِّ متحوّلٍ، كما يلي:<syntaxhighlight lang="kotlin">
 +
val compare: (x: T, y: T) -> Int = ...
 +
</syntaxhighlight>وللتصريح عن متحول nullable من نوع الدالة يجب أن يُحاط كامل نوع الدالة بين قوسين <code>()</code> ويُضاف الرمز <code>?</code> تاليًا بالشكل:<syntaxhighlight lang="kotlin">
 +
var sum: ((Int, Int) -> Int)? = null
 +
</syntaxhighlight>
  
 
=== صيغة تعابير Lambda ===
 
=== صيغة تعابير Lambda ===
 +
تكون الصيغة الكاملة لتعابير lambda (القيم الحرفيّة لأنواع الدوال) بالشكل:<syntaxhighlight lang="kotlin">
 +
val sum = { x: Int, y: Int -> x + y }
 +
</syntaxhighlight>إذ يُحاط تعبير lambda بالأقواس <code>{}</code> دائمًا حيث يُوضَع داخلهما التصريحُ بالصيغة الكاملة، وقد يحتوي على توصيفاتٍ للنوع (type annotations) وهذا أمرٌ اختياريّ، وتأتي بُنية التعبير بعد الرمز <code><-،</code> وإن لم يكن النوع المُعاد في التعبير من النوع <code>Unit</code> فسيُعامَل آخرُ تعبيرٍ واردٍ فيه وكأنه القيمة المُعادة، وبإزالة كافة التوصيفات الاختيارية نحصل على التعبير بالشكل:<syntaxhighlight lang="kotlin">
 +
val sum: (Int, Int) -> Int = { x, y -> x + y }
 +
</syntaxhighlight>ومن الشائع جدًا أن يكون للتعبير متحولٌ وسيطٌ واحدٌ فقط، وعندها لا حاجة إلى التصريح عنه إذ سيُعرَّف ضمنيًا (implicitly) بالتسمية <code>it</code>، كما في الشيفرة:<syntaxhighlight lang="kotlin">
 +
ints.filter { it > 0 } // القيمة الحرفيّة هنا هي من النوع
 +
                      // '(it: Int) -> Boolean'
 +
</syntaxhighlight>ويمكن استخدام أمر [[Kotlin/returns|العودة المُقيّد (qualified return)]] في تعبير lambda لإعادة قيمةٍ ما حيث يتمّ ذكر بشكلٍ صريحٍ (explicitly)، وإن لم يُذكر ذلك فسيُعامَل آخرُ تعبيرٍ واردٍ فيه وكأنه القيمة المُعادة، ولذلك فإن مقطعي الشيفرة الآتيان متكافئان:<syntaxhighlight lang="kotlin">
 +
ints.filter {
 +
    val shouldFilter = it > 0
 +
    shouldFilter
 +
}
 +
 +
ints.filter {
 +
    val shouldFilter = it > 0
 +
    return@filter shouldFilter
 +
}
 +
</syntaxhighlight>أمّا عند وجود دالةٍ في المتحوّل الأخير للدالة فيُمكن تمرير تعبير lambda خارج القوسين <code>()</code>.
  
 
=== الدوال المجهولة (Anonymous Function) ===
 
=== الدوال المجهولة (Anonymous Function) ===
  
=== النطاق المغلق (Closures) ===
+
=== النطاق المُغلق (Closures) ===
 +
تستطيع كلٌّ من تعابير lambda أو الدوال المجهولة (anonymous functions) أو [[Kotlin/functions|الدوال المحليّة (local functions)]] أو [[Kotlin/object declarations|تعابير الكائنات (object expressions)]] الوصول إلى العناصر الموجودة ضمن نطاقها المغلق أي لكافّة المتحولات الموجودة في المجال الخارجيّ (outer scope)، وعلى عكس لغة Java فإنّه من الممكن تعديل المتحولات الموجودة ضمن هذا النطاق، كما في الشيفرة:<syntaxhighlight lang="kotlin">
 +
var sum = 0
 +
ints.filter { it > 0 }.forEach {
 +
    sum += it
 +
}
 +
print(sum)
 +
</syntaxhighlight>
  
 
=== القيم الحرفيّة للدوال مع المستقبِل (Function Literals with Receiver) ===
 
=== القيم الحرفيّة للدوال مع المستقبِل (Function Literals with Receiver) ===

مراجعة 13:34، 24 مارس 2018

الدوال من المرتبة الأعلى (Higher-Order Functions)

وهي الدوال التي تقبل دوالًا أخرى كمتحولاتٍ وسيطةٍ لها، أو تلك التي تُعيد (return) دوالًا أخرى كنتيجة لها، وكمثالٍ عنها لنأخذ الدالة lock()‎، وهي الدالة التي تقبل كائنًا lock ودالةً أخرى، حيث ستحصلُ الدالة على الكائن lock وتُنفِّذُ الدالةَ الوسيطةَ ثم تُحرِّر القفل في النهاية، كما في الشيفرة:

fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
        return body()
    }
    finally {
        lock.unlock()
    }
}

إنّ body هو من نوع دالة (وهو ‎() -> T) ومن الواضح أنّه دالةٌ خاليةٌ من المتحولات تعيد النوع T، وتُستدعَى في الجزء try حيث تكون محميةً (protected) عبر lock، وتُعاد قيمتها عبر الدالة lock()‎. ولدى استدعاء الدالة lock()‎ يمكن تمرير دالةٍ أخرى كمتحولٍ لها (راجع مرجعيّات الدوال [function references] )، مثل:

fun toBeSynchronized() = sharedResource.operation()

val result = lock(lock, ::toBeSynchronized)

ومن الأفضل بمثل هذه الحالة تمريرُ تعبير lambda بالشكل:

val result = lock(lock, { sharedResource.operation() })

إذ تتصف تعابير lambda بما يلي:

  • يٌحاط التعبير بالقوسين {} دائمًا.
  • تُعرَّف متحولاته (إن وُجدَت) قبل المعامل ‎->‎ (وقد يُحذَف نوع المتحولات).
  • تتوضع البُنية (body) (إن وُجدَت) بعد المعامل ‎->‎ .

ويُصطلَح في Kotlin بأنّه إن كانت الدالة تقبل في متحولها الأخير دالةً أخرى ويُمرَّر تعبير lambda كمتحولٍ لها، فيجب أن يوضع خارج القوسين () كما في الشيفرة:

lock (lock) {
    sharedResource.operation()
}

وتُعدُّ الدالة map()‎ مثالًا آخر عن الدوال من المرتبة الأعلى وهي بالشكل:

fun <T, R> List<T>.map(transform: (T) -> R): List<R> {
    val result = arrayListOf<R>()
    for (item in this)
        result.add(transform(item))
    return result
}

ويكون استدعاؤها بالشكل:

val doubled = ints.map { value -> value * 2 }

حيث أمكن حذف القوسين () لأنّ lambda هي المتحول الوحيد المُستخدَم في هذا الاستدعاء.

it: الاسم الضمني (implicit name) للمتحول الوحيد

ليس من الضروريّ وجود تصريح (declaration) الدالة إن كانت القيمة الحرفيّة (literal) لها بمتحولٍ وحيدٍ حيث ستُستخدَم التسمية it للتعبير عنها، بالشكل:

ints.map { it * 2 }

ويسمح هذا بكتابة شيفرةٍ بالنمط LINQ كما يلي:

strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }

استخدام الشرطة السفليّة (_) للمتحولات غير المستخدمة (بدءًا من الإصدار 1.1)

إن لم تكن هناك حاجةٌ لأحد المتحولات الوسيطة في lambda فيمكن استخدام الرمز _ بدلًا من الاسم، بالشكل:

map.forEach { _, value -> println("$value!") }

التفكيك (Destructuring) في Lambdas (بدءًا من الإصدار 1.1)

راجع التصريح بالتفكيك (destructuring declarations) حيث ستجد شرحًا عن التفكيك في lambda.

الدوال السطريّة (Inline Functions)

قد تُستخدَم الدوال السطريّة لتحسين الأداء في الدوال من المرتبة الأعلى.

تعابير Lambda والدوال المجهولة (Anonymous Functions)

يُعدُّ كلٌّ من تعبير lambda والدالة المجهولة قيمةً دالةً حرفيّةً (function literal) وهي دالةٌ لا يُصرَّح عنها بل تُمرَّر كتعبيرٍ مباشرةً، كما في المثال الآتي:

max(strings, { a, b -> a.length < b.length })

إذ إنّ الدالة max هي دالةٌ من مرتبةٍ أعلى (higher order) لأنها تقبل دالةً في متحولها الثاني والذي هو "تعبيرٌ داليّ" أو بكلامٍ آخر هو "قيمة حرفيّة للدالة"، وهذا مكافئٌ للشيفرة:

fun compare(a: String, b: String): Boolean = a.length < b.length

أنواع الدوال (Function Types)

يجب تحديد نوع المتحول في الدالة من النوع "دالة" حتى يقبل تمرير الدالة عبره، إذ سيعرَّف التابع max المذكور سابقًا كما يلي:

1 fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
2     var max: T? = null
3     for (it in collection)
4         if (max == null || less(max, it))
5             max = it
6     return max
7 }

إنّ المتحول less من النوع ‎(T, T) -> Boolean والذي هو دالة بمتحولين من النوع T تعيد قيمةً ثنائيّةً (boolean) بالقيمة true إن كان المتحول الأوّل بقيمةٍ أصغر من قيمة المتحول الثاني. وقد استُخدِم المتحول less في السطر الرابع كدالةٍ مُستدعَاة عبر تمرير المتحولين من النوع T، إذ يكتب نوع الدالة كما في الشيفرة السابقة، أو قد يُكتب بمتحولاتٍ مُسمَّاة (named parameters) عند الرغبة بتوثيق المعنى لكلِّ متحوّلٍ، كما يلي:

val compare: (x: T, y: T) -> Int = ...

وللتصريح عن متحول nullable من نوع الدالة يجب أن يُحاط كامل نوع الدالة بين قوسين () ويُضاف الرمز ? تاليًا بالشكل:

var sum: ((Int, Int) -> Int)? = null

صيغة تعابير Lambda

تكون الصيغة الكاملة لتعابير lambda (القيم الحرفيّة لأنواع الدوال) بالشكل:

val sum = { x: Int, y: Int -> x + y }

إذ يُحاط تعبير lambda بالأقواس {} دائمًا حيث يُوضَع داخلهما التصريحُ بالصيغة الكاملة، وقد يحتوي على توصيفاتٍ للنوع (type annotations) وهذا أمرٌ اختياريّ، وتأتي بُنية التعبير بعد الرمز <-، وإن لم يكن النوع المُعاد في التعبير من النوع Unit فسيُعامَل آخرُ تعبيرٍ واردٍ فيه وكأنه القيمة المُعادة، وبإزالة كافة التوصيفات الاختيارية نحصل على التعبير بالشكل:

val sum: (Int, Int) -> Int = { x, y -> x + y }

ومن الشائع جدًا أن يكون للتعبير متحولٌ وسيطٌ واحدٌ فقط، وعندها لا حاجة إلى التصريح عنه إذ سيُعرَّف ضمنيًا (implicitly) بالتسمية it، كما في الشيفرة:

ints.filter { it > 0 } // القيمة الحرفيّة هنا هي من النوع
                       // '(it: Int) -> Boolean'

ويمكن استخدام أمر العودة المُقيّد (qualified return) في تعبير lambda لإعادة قيمةٍ ما حيث يتمّ ذكر بشكلٍ صريحٍ (explicitly)، وإن لم يُذكر ذلك فسيُعامَل آخرُ تعبيرٍ واردٍ فيه وكأنه القيمة المُعادة، ولذلك فإن مقطعي الشيفرة الآتيان متكافئان:

ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}

أمّا عند وجود دالةٍ في المتحوّل الأخير للدالة فيُمكن تمرير تعبير lambda خارج القوسين ().

الدوال المجهولة (Anonymous Function)

النطاق المُغلق (Closures)

تستطيع كلٌّ من تعابير lambda أو الدوال المجهولة (anonymous functions) أو الدوال المحليّة (local functions) أو تعابير الكائنات (object expressions) الوصول إلى العناصر الموجودة ضمن نطاقها المغلق أي لكافّة المتحولات الموجودة في المجال الخارجيّ (outer scope)، وعلى عكس لغة Java فإنّه من الممكن تعديل المتحولات الموجودة ضمن هذا النطاق، كما في الشيفرة:

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

القيم الحرفيّة للدوال مع المستقبِل (Function Literals with Receiver)

تتيح لغة Kotlin إمكانيّة استدعاء قيمة حرفيّة للدالة بكائنٍ مستقبِل مُحدَّدٍ، إذ يمكن -داخل بُنية قيمة الدالة الحرفيّة- استدعاء التوابع عبر الكائن المُستقبِل دون استخدام أيّ مقيّدات (qualifiers) إضافيّة، وهذا يماثل الدوال الإضافيّة (extension functions) التي تسمح بالوصول (accessing) إلى عناصر الكائن المستقبِل داخل بُنية الدالة، ومن أشهر الأمثلة عنها المُنشِئ الحافظ للنوع (type-safe builders).

نوع هذه القيم الحرفيّة للدالة (function literals) هو نوع دالةٍ مع المستقبِل، أي:

sum : Int.(other: Int) -> Int

إذ من الممكن استدعاؤها وكأنها تابعٌ للكائن المستقبِل بالشكل:

1.sum(2)

وتسمح صيغة الدالة المجهولة (anonymous function) بتحديد نوع المستقبِل للقيمة الحرفيّة للدالة مباشرةً، وهذا يفيد عند الحاجة إلى التصريح عن متحولٍ من نوع الدالة مع المستقبِل لاستخدامه فيما بعد، مثل:

val sum = fun Int.(other: Int): Int = this + other

كما يمكن إسناد القيمة غير الحرفيّة (non-literal) من نوع الدالة بالمستقبِل أو تمريرها كمتحولٍ بالموقع الذي تُتوقَّع فيه الدالة العاديّة، والتي تحتوي على متحول أولٍ إضافيِّ من نوع المستقبِل وبالعكس، فمثلًا: يكون النوعان String.(Int) -> Boolean و (String, Int) -> Boolean متوافقين في الشيفرة:

val represents: String.(Int) -> Boolean = { other -> toIntOrNull() == other }
println("123".represents(123)) // true

fun testOperation(op: (String, Int) -> Boolean, a: String, b: Int, c: Boolean) =
    assert(op(a, b) == c)
    
testOperation(represents, "100", 100, true) // OK

وعندما يكون من الممكن تحديد نوع المستقبِل من السياق العامّ فتستخدم حينئذٍ تعابير lambda كقيمٍ حرفيّةٍ للدوال مع المستقبِل، مثل:

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // إنشاء الكائن المستقبِل
    html.init()        // تمرير الكائن المستقبِل إلى lambda
    return html
}


html {       // بداية lambda 
             // مع المستقبِل
    body()   // استدعاء تابع عبر الكائن المستقبِل
}

مصادر