我在出色的Scala 《 函数式编程》一书的第3章中看到了一个练习,该练习处理了定义函数数据结构的问题,并以链表为例说明了如何开发这样的数据结构。 我想使用Kotlin尝试该示例,以了解我可以在多大程度上复制该示例。
样品的Scala的骨架可在同伴码书在这里和我在Kotlin尝试通过信息库中的大量answerkey启发(复制!)。
基本的
这是Kotlin中基本的List表示形式:
sealed class List<out A> {
abstract val head: A
abstract val tail: List<A>
}
data class Cons<out T>(override val head: T, override val tail: List<T>) : List<T>()
object Nil : List<Nothing>() {
override val head: Nothing
get() {
throw NoSuchElementException("head of an empty list")
}
override val tail: List<Nothing>
get() {
throw NoSuchElementException("tail of an empty list")
}
}
List已定义为密封类,这意味着密封类的所有子类都将在同一文件中定义。 这对于实例类型的模式匹配很有用,并且在大多数功能中都会反复出现。
此列表有两种实现方式–
1.包含一个由头部元素和尾部列表组成的非空列表,
2.空列表
就目前的形式而言,这已经非常有用,请考虑以下内容以构造List并从中检索元素:
val l1:List<Int> = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
assertThat(l1.head).isEqualTo(1)
assertThat(l1.tail).isEqualTo(Cons(2, Cons(3, Cons(4, Nil))))
val l2:List<String> = Nil
模式匹配“何时”表达
现在跳到实现List的一些方法。 由于List是一个密封的类,因此它允许一些良好的模式匹配,例如,获取List中元素的总和:
fun sum(l: List<Int>): Int {
return when(l) {
is Cons -> l.head + sum(l.tail)
is Nil -> 0
}
}
编译器知道Cons和Nil是列表实例上进行匹配的仅有两条路径。
稍微复杂一点的操作是,从列表的开头“删除”一些元素,然后“ dropWhile”接收一个谓词,然后从与该谓词匹配的开头删除元素:
fun drop(n: Int): List<A> {
return if (n <= 0)
this
else when (this) {
is Cons -> tail.drop(n - 1)
is Nil -> Nil
}
}
val l = list(4, 3, 2, 1)
assertThat(l.drop(2)).isEqualTo(list(2, 1))
fun dropWhile(p: (A) -> Boolean): List<A> {
return when(this) {
is Cons -> if (p(this.head)) this.tail.dropWhile(p) else this
is Nil -> Nil
}
}
val l = list(1, 2, 3, 5, 8, 13, 21, 34, 55, 89)
assertThat(l.dropWhile({e -> e < 20})).isEqualTo(list(21, 34, 55, 89))
这些展示了模式匹配和Kotlin中“ when”表达的强大功能。
不安全的差异!
要抚平皱纹,请查看如何使用声明为“ out T”的类型参数定义List,这称为“声明位点差异”,在这种情况下,它使List在类型T上协变。 Kotlin文档对它进行了精美的解释。 通过声明List的方式,它使我可以执行以下操作:
val l:List<Int> = Cons(1, Cons(2, Nil))
val lAny: List<Any> = l
现在,考虑一个“附加”功能,该功能附加另一个列表:
fun append(l: List<@UnsafeVariance A>): List<A> {
return when (this) {
is Cons -> Cons(head, tail.append(l))
is Nil -> l
}
}
此处将第二个列表作为附加函数的参数,但是Kotlin会标记该参数–这是因为可以返回协变量类型,但不能将其作为参数。 但是,由于我们知道List的当前形式是不可变的,因此可以通过在类型参数上标记“ @UnsafeVariance”注释来克服这一问题。
折叠式
折叠操作允许基于列表中各个元素的某种聚合将列表“折叠”为结果。
考虑foldLeft:
fun <B> foldLeft(z: B, f: (B, A) -> B): B {
tailrec fun foldLeft(l: List<A>, z: B, f: (B, A) -> B): B {
return when (l) {
is Nil -> z
is Cons -> foldLeft(l.tail, f(z, l.head), f)
}
}
return foldLeft(this, z, f)
}
如果列表由元素(2、3、5、8)组成,则foldLeft等效于“ f(f(f(f(f(z,2),3),5),8)”
有了这个高阶函数,求和函数可以这样表示:
val l = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
assertThat(l.foldLeft(0, {r, e -> r + e})).isEqualTo(10)
foldRight在Kotlin中如下所示:
fun <B> foldRight(z: B, f: (A, B) -> B): B {
return when(this) {
is Cons -> f(this.head, tail.foldRight(z, f))
is Nil -> z
}
}
如果列表由元素(2、3、5、8)组成,则foldRight等效于“ f(2,f(3,f(5,f(8,z))))”
这个版本的foldRight尽管看上去不是很酷的尾部递归,但是可以通过使用先前定义的尾部递归foldLeft来实现更友好的堆栈版本,方法是简单地反转List并以下列方式在内部调用foldLeft:
fun reverse(): List<A> {
return foldLeft(Nil as List<A>, { b, a -> Cons(a, b) })
}
fun <B> foldRightViaFoldLeft(z: B, f: (A, B) -> B): B {
return reverse().foldLeft(z, { b, a -> f(a, b) })
}
地图和flatMap
map是转换此列表元素的函数:
fun <B> map(f: (A) -> B): List<B> {
return when (this) {
is Cons -> Cons(f(head), tail.map(f))
is Nil -> Nil
}
}
以下是使用此功能的一个示例:
val l = Cons(1, Cons(2, Cons(3, Nil)))
val l2 = l.map { e -> e.toString() }
assertThat(l2).isEqualTo(Cons("1", Cons("2", Cons("3", Nil))))
map的一种变体,其中转换函数返回另一个列表,最终结果使所有内容变平,最好在实现后使用示例进行演示:
fun <B> flatMap(f: (a: A) -> List<@UnsafeVariance B>): List<B> {
return flatten(map { a -> f(a) })
}
companion object {
fun <A> flatten(l: List<List<A>>): List<A> {
return l.foldRight(Nil as List<A>, { a, b -> a.append(b) })
}
}
val l = Cons(1, Cons(2, Cons(3, Nil)))
val l2 = l.flatMap { e -> list(e.toString(), e.toString()) }
assertThat(l2)
.isEqualTo(
Cons("1", Cons("1", Cons("2", Cons("2", Cons("3", Cons("3", Nil)))))))
这涵盖了使用Kotlin实现功能列表数据结构所涉及的基础知识,与scala版本相比,存在一些粗糙之处,但我认为它大部分都可以工作。 诚然,可以对示例进行大幅改进,如果您对如何改进代码有任何意见,请给我发送PR,
github回购此样本或作为对此文章的评论。
翻译自: https://www.javacodegeeks.com/2017/10/kata-implementing-functional-list-data-structure-kotlin.html