واحدة من الميزات الرائعة لـ Kotlin هي أنه يدعم البرمجة الوظيفية. دعونا نلقي نظرة ونناقش بعض الوظائف البسيطة ولكن المعبرة المكتوبة في Kotlin.

العمل مع المجموعات
يدعم Kotlin العمل المريح مع المجموعات. هناك العديد من الميزات المختلفة. لنفترض أننا أنشأنا بعض الأنظمة للجامعة. نحن بحاجة إلى العثور على أفضل الطلاب الذين يستحقون منحة دراسية. لدينا نموذج Student
التالي:
class Student( val name: String, val surname: String, val passing: Boolean, val averageGrade: Double )
الآن يمكننا استدعاء الوظيفة التالية للحصول على قائمة بالطلاب العشرة الأوائل الذين يستوفون جميع المعايير:
students.filter { it.passing && it.averageGrade > 4.0 } // 1 .sortedBy { it.averageGrade } // 2 .take(10) // 3 .sortedWith(compareBy({ it.surname }, { it.name })) // 4
- نترك فقط الطلاب الذين اجتازوا الاختبار ، ومتوسط درجاته أكثر من 4.0.
- قم بفرزها حسب متوسط الدرجة.
- نترك الطلاب العشرة الأوائل.
- رتبهم أبجديًا. يقارن المقارنة أولاً الأسماء الأخيرة ، وإذا كانت متساوية ، فإنها تقارن الأسماء.
ماذا لو احتجنا ، بدلاً من الترتيب الأبجدي ، إلى الحفاظ على الترتيب الأصلي للطلاب؟ يمكننا القيام بذلك باستخدام الفهارس:
students.filter { it.passing && it.averageGrade > 4.0 } .withIndex() // 1 .sortedBy { (i, s) -> s.averageGrade } // 2 .take(10) .sortedBy { (i, s) -> i } // 3 .map { (i, s) -> s } // 4
- اربط مؤشر التكرار الحالي بكل عنصر.
- قم بالفرز حسب متوسط الدرجات وترك الطلاب العشرة الأوائل.
- فرز مرة أخرى ، ولكن الآن حسب الفهرس.
- نحذف الفهارس ونترك الطلاب فقط.
يوضح هذا المثال مدى سهولة وبساطة العمل مع المجموعات في Kotlin.

إذا درست الجبر في إحدى الجامعات ، يمكنك أن تتذكر ماهية المجموعة الفائقة. بالنسبة إلى أي مجموعة ، فإن مجموعتها العليا هي مجموعة كل مجموعاتها الفرعية ، بما في ذلك المجموعة الأصلية نفسها والمجموعة الفارغة. على سبيل المثال ، إذا كان لدينا المجموعة التالية:
{1،2،3}
هذه هي مجموعته الشاملة:
{{} و {1} و {2} و {3} و {1،2} و {1،3} و {2،3} و {1،2،3}}
في الجبر ، هذه الوظيفة مفيدة جدًا. كيف ننفذها؟
إذا كنت ترغب في تحدي نفسك ، فتوقف عن القراءة الآن وحاول حل هذه المشكلة بنفسك.
لنبدأ تحليلنا بملاحظة بسيطة. إذا أخذنا بعض عناصر المجموعة (على سبيل المثال ، 1) ، فسيكون للمجموعة الفائقة عدد متساوٍ من المجموعات مع هذا العنصر ({1} و {1،2} و {1،3} و {1،2،3}) و بدونها ({} ، {2} ، {3} ، {2،3}).
لاحظ أن المجموعة الثانية هي مجموعة فائقة ({2،3}) ، والأولى هي مجموعة فائقة ({2،3}) مع العنصر المضاف (1) لكل مجموعة. وبالتالي ، يمكننا حساب المجموعة الفائقة عن طريق أخذ العنصر الأول ، وحساب المجموعة الفائقة لجميع العناصر الأخرى وإرجاع مجموع النتيجة والنتيجة مع إضافة العنصر الأول إلى كل مجموعة:
fun <T> powerset(set: Set<T>): Set<Set<T>> { val first = set.first() val powersetOfRest = powerset(set.drop(1)) return powersetOfRest.map { it + first } + powersetOfRest }
لكن هذه الطريقة لن تنجح. المشكلة هي مجموعة فارغة: first
سيلقي خطأ عندما تكون المجموعة فارغة. هنا يأتي تعريف مجموعة شاملة للإنقاذ - مجموعة شاملة لمجموعة فارغة هي مجموعة فارغة: powerset ({}) = {{}}. إليك ما تبدو عليه الخوارزمية المصححة:
fun <T> powerset(set: Set<T>): Set<Set<T>> = if (set.isEmpty()) setOf(emptySet()) else { val powersetOfRest = powerset(set.drop(1)) powersetOfRest + powersetOfRest.map { it + set.first() } }

دعونا نرى كيف يعمل. لنفترض أننا بحاجة إلى حساب مجموعة الطاقة ({1،2،3}). تعمل الخوارزمية على النحو التالي:
powerset ({1،2،3}) = powerset ({2،3}) + powerset ({2،3}). map {it + 1}
powerset ({2،3}) = powerset ({3}) + powerset ({3}). خريطة {it + 2}
powerset ({3}) = powerset ({}) + powerset ({}). خريطة {it + 3}
powerset ({}) = {{}}
powerset ({3}) = {{} ، {3}}
powerset ({2،3}) = {{}، {3}} + {{2}، {2، 3}} = {{}، {2}، {3}، {2، 3}}
powerset ({1،2،3}) = {{}، {2}، {3}، {2، 3}} + {{1}، {1، 2}، {1، 3}، {1، 2 ، 3}} = {{} ، {1} ، {2} ، {3} ، {1،2} ، {1،3} ، {2،3} ، {1،2،3}}
ولكن يمكننا تحسين وظيفتنا بشكل أكبر. دعنا نستخدم وظيفة let لجعل الترميز أقصر وأكثر إحكاما:
fun <T> powerset(set: Set<T>): Set<Set<T>> = if (set.isEmpty()) setOf(emptySet()) else powerset(set.drop(1)) .let { it+ it.map { it + set.first() }
يمكننا أيضًا تعريف هذه الوظيفة كدالة ملحق Collection
، حتى نتمكن من استخدام هذه الوظيفة كما لو كانت طريقة Set
( setOf(1,2,3).powerset()
بدلاً من powerset(setOf(1,2,3))
):
fun <T> Collection<T>.powerset(): Set<Set<T>> = if (isEmpty()) setOf(emptySet()) else drop(1) .powerset() .let { it+ it.map { it + first() }
يمكننا أيضًا تقليل الآثار السلبية للعودة المُنشأة. في التطبيق أعلاه ، تنمو حالة المجموعة الفائقة مع كل تكرار (مع كل مكالمة متكررة) ، لأنه يجب تخزين حالة التكرار السابق في الذاكرة.
بدلاً من ذلك ، يمكننا استخدام tailrec
وظائف حلقة أو tailrec
. سنستخدم الخيار الثاني للحفاظ على سهولة قراءة الوظيفة. يسمح معدِّل tailrec
باستدعاء عودي واحد فقط في سطر الوظيفة الأخير الذي تم تنفيذه. إليك كيفية تغيير وظيفتنا لاستخدامها بشكل أكثر كفاءة:
fun <T> Collection<T>.powerset(): Set<Set<T>> = powerset(this, setOf(emptySet())) private tailrec fun <T> powerset(left: Collection<T>, acc: Set<Set<T>>): Set<Set<T>> = if (left.isEmpty()) acc else powerset(left.drop(1), acc + acc.map { it + left.first() })
يعد هذا التطبيق جزءًا من مكتبة KotlinDiscreteMathToolkit ، التي تحدد العديد من الوظائف الأخرى المستخدمة في الرياضيات المنفصلة.
حان الوقت للحصول على المثال الأكثر إثارة للاهتمام. سترى كيف يمكن تبسيط مشكلة معقدة وجعلها قابلة للقراءة باستخدام النمط وأدوات البرمجة الوظيفية.
ننفذ خوارزمية فرز سريع. الخوارزمية بسيطة: نختار بعض العناصر (المحور ( الشريط الروسي)) ونوزع جميع العناصر الأخرى في قائمتين: قائمة بعناصر أكبر من الشريط وأصغر. ثم نقوم بترتيب هذه المصفوفات الفرعية بشكل متكرر. أخيرًا ، نقوم بدمج قائمة مرتبة بالعناصر الأصغر وشريط وقائمة مرتبة بالعناصر الأكبر. للتبسيط ، خذ العنصر الأول كشريط. هنا التنفيذ الكامل:
fun <T : Comparable<T>> List<T>.quickSort(): List<T> = if(size < 2) this else { val pivot = first() val (smaller, greater) = drop(1).partition { it <= pivot} smaller.quickSort() + pivot + greater.quickSort() }
تبدو رائعة ، أليس كذلك؟ هذا هو جمال البرمجة الوظيفية.

المشكلة الأولى في هذه الوظيفة هي وقت تنفيذها. إنها غير محسنة على الإطلاق. لكنها قصيرة وسهلة القراءة.
إذا كنت بحاجة إلى وظيفة محسنة ، يمكنك استخدام الوظيفة من مكتبة Java القياسية. وهو يعتمد على خوارزميات مختلفة وفقًا لظروف معينة ، ويتم كتابته بشكل أصلي. يجب أن يكون هذا أكثر كفاءة. ولكن كم بالضبط؟ دعونا نقارن بين هاتين الوظيفتين. دعنا نفرز عدة صفائف مختلفة بعناصر عشوائية ونقارن وقت التشغيل:
val r = Random() listOf(100_000, 1_000_000, 10_000_000) .asSequence() .map { (1..it).map { r.nextInt(1000000000) } } .forEach { list: List<Int> -> println("Java stdlib sorting of ${list.size} elements took ${measureTimeMillis { list.sorted() }}") println("quickSort sorting of ${list.size} elements took ${measureTimeMillis { list.quickSort() }}") }
فيما يلي النتائج التي حصلنا عليها:
استغرق تصنيف جافا stdlib من 100000 عنصر 83
استغرق فرز QuickSort من 100000 عنصر 163
استغرق فرز جافا stdlib من 1000000 عنصر 558
استغرق الفرز السريع للـ 1،000،000 عنصر 859
استغرق تصنيف Java stdlib لـ 10،000،000 عنصر 6182
استغرق الفرز السريع للـ 10،000،000 عنصر 12133
كما ترى ، وظيفة quickSort
أبطأ مرتين تقريبًا. حتى بالنسبة للقوائم الضخمة. في الحالات العادية ، سيكون الفرق عادةً من 0.1 مللي ثانية إلى 0.2 مللي ثانية. وهذا يفسر لماذا يمكننا في بعض الحالات استخدام وظيفة أقل تحسينًا إلى حد ما ، ولكنها سهلة القراءة وبسيطة.