Я видел упражнение в главе 3 превосходной книги « Функциональное программирование в Scala», которое посвящено определению функциональных структур данных и использует связанный список в качестве примера того, как приступить к разработке такой структуры данных. Я хотел попробовать этот образец, используя Kotlin, чтобы увидеть, в какой степени я могу воспроизвести образец.
Скелет scala образца доступен в коде компаньона для книги здесь, и моя попытка в Kotlin сильно вдохновлена (скопирована!) Ключом ответа в хранилище.
основной
Вот как выглядит базовое представление List в Kotlin:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
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") }} |
Список был определен как запечатанный класс, это означает, что все подклассы запечатанного класса будут определены в одном файле. Это полезно для сопоставления с образцом в типе экземпляра и будет появляться в большинстве функций.
Есть две реализации этого списка —
1. Минусы непустого списка, состоящего из элемента head и хвостового списка,
2. Ноль пустой список
Это уже очень полезно в его текущей форме, рассмотрим следующее, которое создает список и извлекает из него элементы:
|
1
2
3
4
5
6
|
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:
|
1
2
3
4
5
6
|
fun sum(l: List<Int>): Int { return when(l) { is Cons -> l.head + sum(l.tail) is Nil -> 0 }} |
Компилятор понимает, что Cons и Nil — это единственные два пути для совпадения в экземпляре списка.
Немного более сложная операция: «удалить» некоторое количество элементов из начала списка и «dropWhile», который принимает предикат и удаляет элементы с начала, соответствующие предикату:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
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 .
Небезопасная дисперсия!
Чтобы коснуться складки, посмотрите, как список определен с параметром типа, который объявлен как «out T», это называется «декларация сайта дисперсии», что в этом случае делает список ко-вариантным для типа T. Объявление сайта дисперсия красиво объясняется с документацией Kotlin . Благодаря тому, как List объявлен, он позволяет мне делать что-то вроде этого:
|
1
2
|
val l:List<Int> = Cons(1, Cons(2, Nil))val lAny: List<Any> = l |
Теперь рассмотрим функцию «добавить», которая добавляет другой список:
|
1
2
3
4
5
6
|
fun append(l: List<@UnsafeVariance A>): List<A> { return when (this) { is Cons -> Cons(head, tail.append(l)) is Nil -> l }} |
здесь второй список берется в качестве параметра для функции добавления, однако Kotlin будет отмечать этот параметр — это потому, что можно возвращать ко-вариантный тип, но не принимать его в качестве параметра. Однако, поскольку мы знаем, что список в его текущей форме неизменен, я могу обойти это, пометив параметр типа аннотацией «@UnsafeVariance».
Раскладной
Операции складывания позволяют «свернуть» список в результат, основанный на некоторой агрегации по отдельным элементам в нем.
Рассмотрим foldLeft:
|
01
02
03
04
05
06
07
08
09
10
|
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 (z (2), 3), 5), 8)»
С этой функцией более высокого порядка функция суммы может быть выражена следующим образом:
|
1
2
|
val l = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))assertThat(l.foldLeft(0, {r, e -> r + e})).isEqualTo(10) |
FoldRight выглядит следующим образом в Kotlin:
|
1
2
3
4
5
6
|
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 следующим образом:
|
1
2
3
4
5
6
7
|
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) })} |
карта и плоская карта
map — это функция, которая преобразует элемент этого списка:
|
1
2
3
4
5
6
|
fun <B> map(f: (A) -> B): List<B> { return when (this) { is Cons -> Cons(f(head), tail.map(f)) is Nil -> Nil }} |
Пример использования этой функции:
|
1
2
3
|
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)))) |
Вариант карты, в котором функция преобразования возвращает другой список, а окончательные результаты сглаживают все, что лучше всего продемонстрировать на примере после реализации:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
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 репо для этого образца или как комментарий к этому сообщению.
| См. Оригинальную статью здесь: Kata — реализация функциональной структуры данных List в Kotlin.
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |