我最喜欢的Kotlin函数式编程示例

Kotlin的一大特色是它支持函数式编程。 让我们看一下,并讨论一些用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中的集合工作多么简单直观。


在Kotlin中处理收藏


SuperSet(布尔值)


如果您在大学学习过代数,您可能会想起什么是超集。 对于任何集合,其超集都是其所有子集的集合,包括原始集合本身和空集合。 例如,如果我们有以下设置:


{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})。 该算法的作用如下:


幂集({1,2,3})=幂集({2,3})+幂集({2,3})。映射{it + 1}


功率集({2,3})=功率集({3})+功率集({3})。映射{it + 2}


powerset({3})= powerset({})+ powerset({})。map {it + 3}


powerset({})= {{}}


powerset({3})= {{},{3}}


幂集({2,3})= {{},{3}} + {{2},{2,3}} = {{},{2},{3},{2,3}}


幂集({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的扩展函数,以便我们可以像使用SetsetOf(1,2,3).powerset() powerset(setOf(1,2,3))而不是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() } // Usage listOf(2,5,1).quickSort() // [1,2,5] 

看起来很美吧? 这就是函数式编程的美。


功能编程


这种功能的第一个问题是其执行时间。 她完全没有被优化。 但这是简短易读的。


如果需要优化的功能,则可以使用标准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() }}") } 

这是我们得到的结果:


Java stdlib排序100000个元素花费了83
quickSort排序100000个元素花了163
Java stdlib排序1,000,000个元素花了558
quickSort排序1,000,000个元素花费了859
Java stdlib排序10,000,000元素花费了6182
quickSort排序10,000,000个元素花费了12133


如您所见, quickSort函数的速度几乎慢了2倍。 即使是巨大的清单。 在正常情况下,差异通常为0.1毫秒至0.2毫秒。 这就解释了为什么在某些情况下我们可以使用优化程度略低但易于理解且简单的函数。

Source: https://habr.com/ru/post/zh-CN420299/


All Articles