我最喜欢 Kotlin 函数式编程的例子
Posted 承香墨影
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我最喜欢 Kotlin 函数式编程的例子相关的知识,希望对你有一定的参考价值。
Hi,大家好,我是承香墨影!
今天给大家带来一篇海外的热文翻译,关于 Kotlin 的函数式编程的。函数式编程确实可以大大简化代码并且提高可读性,而本文就从实际例子出发,来阐述函数式编程带来的好处。
Kotlin 的一大优点就是它支持函数式编程。让我们来看看和讨论一下 Kotlin 写的一些简单而有表现力的功能。
处理集合
Kotlin 对集合的处理有一些非常好的支持。它表现力强,支持很多功能。
看一个例子,假设我们为一所大学建立一个学生系统。我们需要找到最值得获得奖学金的学生。我们有一个 Student
的 Model:
class Student(
val name:String,
val surname:String,
val passing:Boolean,
val averageGrade:Double
)
当我们想要按照标准,获取最佳的 10 名学生的列表,我们可以这样处理:
students.filter { it.passing && it.averageGrade > 4.0 } // 1
.sortedBy { it.averageGrade } // 2
.take(10) // 3
.sortedWith(compareBy({ it.surname }, { it.name })) // 4
流程如下:
1、我们只会获取平均分出超过 4.0 分的学生。
2、我们按平均等级排序。
3、我们只取前 10 名学生。
4、之后再按学生的姓名排序(先比较姓,再比较名)。
如果不按姓名的字母排序,我们就想让他们按照同一个索引排序,要怎么做?我们可以为他们增加一个索引项。
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
1、首先我们为每个元素添加当前的索引。
2、在使用之前,我们需要使用 解构 来声明它。
3、我们按原先的索引进行排序。
4、删除索引,只保留最终学生的数据。
这个例子,显示了 kotlin 中,简单并且直观的集合处理。
Kotlin 解构声明 Doc:
https://kotlinlang.org/docs/reference/multi-declarations.html
Power Set
如果你曾经在大学学习过代数,那么你可能还记得什么是一个 幂集(Power Set)。对于任何集合,它的 幂集 都是它所有子集的集合,包括当前集合和一个空集合。
例如,如果我们有一下集合:
{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})。
我们可以通过取第一个元素来计算 幂集 ,然后再计算其他元素的 幂集,最后将结果们相加取它们之和。
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 为空集的时候会跑出一个错误。在这里,有一个解决方案,当为空集的时候,返回一个 {{}}。
我们继续修复上面的代码。
fun <T> powerset(set: Set<T>): Set<Set<T>> =
if (set.isEmpty()) setOf(setOf())
else {
val powersetOfRest = powerset(set.drop(1).toSet())
powersetOfRest + powersetOfRest.map { it + set.first() }
}
让我们看看它是如何工作的。假设我们需要计算 powerset({1,2,3})。该计算的过程将是:
powerset({1,2,3}) = powerset({2,3}) + powerset({2,3}).map { it + 1 }
powerset({2,3}) = powerset({3}) + powerset({3}).map { it + 2}
powerset({3}) = powerset({}) + powerset({}).map { 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(setOf())
else powerset(set.drop(1).toSet())
.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(setOf())
else drop(1)
.powerset()
.let { it+ it.map { it + first() }
我们还可以继续对它进行改进,就是让 powerset 实现的尾递归。在上面的实现中,powerset 随着每次递归调用,其递归层数都在增加,因此需要将之前迭代的数据保存在内存中。
相反,我们可以使用命令循环或者 tailrec
修饰符来解决这个问题。这里,我们使用 tailrec
修饰符来保持函数的可读性。tailrec
允许在最后一条语句只有一个递归调用。
那么,我们继续改进我们的功能:
fun <T> Collection<T>.powerset(): Set<Set<T>> =
powerset(this, setOf(setOf()))
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 库的一部分,它定义了离散数学中使用的许多其他的函数。
KotlinDiscreteMathToolKit github:
https://github.com/MarcinMoskala/KotlinDiscreteMathToolkit
快速排序
这是我最喜欢的例子,我们将看到如何使用功能性编程风格和工具来简化难题并使其依然保持高度的可读性。
我们将实现 快排(Quicksort)算法。
步骤为:
1、从数列中挑出一个元素,称为"基准"(pivot),
2、重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3、递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
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 | QuickSort |
---|---|---|
100k | 83ms | 163ms |
1000k | 558ms | 859ms |
100000k | 6182ms | 12133ms |
正如我们所看到的,这里的 quickSort 函数,通常慢了 2 倍。但是,在面对更多的列表集合的时候,它依然具有可扩展性。在正常情况下,这的差值一般在 0.1ms 和 0.2ms 之间。
但是请注意,它将更简单、更可读。这就解释了为什么在某些情况下,我们可以使用一个稍微没有那么快速的功能,但是依然可以保持可读性和简单性。
https://medium.freecodecamp.org/my-favorite-examples-of-functional-programming-in-kotlin-e69217b39112
推荐阅读:
听说喜欢留言的人,运气都不会太差
点击『阅读原文』查看更多精彩内容
以上是关于我最喜欢 Kotlin 函数式编程的例子的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin函数式编程 ① ( 函数式编程简介 | 高阶函数 | 函数类别 | Transform 变换函数 | 过滤函数 | 合并函数 | map 变换函数 | flatMap 变换函数 )
Kotlin函数式编程 ② ( 过滤函数 | predicate 谓词函数 | filter 过滤函数 | 合并函数 | zip 函数 | folder 函数 | 函数式编程意义 )
Kotlin函数式编程 ② ( 过滤函数 | predicate 谓词函数 | filter 过滤函数 | 合并函数 | zip 函数 | folder 函数 | 函数式编程意义 )
Kotlin函数式编程 ③ ( 早集合与惰性集合 | 惰性集合-序列 | generateSequence 序列创建函数 | 序列代码示例 | take 扩展函数分析 )