Clojure Lazy Sequences: эквиваленты в Kotlin

Clojure предоставляет средства для ленивого вычисления значений в (бесконечных) последовательностях. При этом значения будут вычисляться только тогда, когда они будут фактически использованы.

Пример бесконечной последовательности одного повторяющегося элемента:

(take 3 (repeat "Hello StackOverflow")) 
//=> ("Hello StackOverflow" "Hello StackOverflow" "Hello StackOverflow")

Использование take помогает потреблять столько элементов из последовательности, сколько мы хотим. Без него OutOfMemoryError быстро убил бы процесс.

Другой пример бесконечной последовательности:

(take 5 (iterate inc 1)) 
//(1 2 3 4 5)

Или более сложная последовательность, обеспечивающая факториальную функцию:

((defn factorial [n]
   (apply * (take n (iterate inc 1)))) 5)

Предоставляет ли Kotlin похожие последовательности? Как они выглядят?

Я сам ответил на этот вопрос, чтобы зафиксировать здесь полученные знания. Это нормально согласно Могу ли я ответить на свой вопрос?


person s1m0nw1    schedule 07.06.2018    source источник


Ответы (1)


В Kotlin мы также можем использовать ленивую оценку, используя Последовательности тоже. Чтобы создать последовательность, мы можем использовать generateSequence < / a> (с предоставлением seed.

fun <T : Any> generateSequence(
    seed: T?,
    nextFunction: (T) -> T?
): Sequence<T> (source)

Возвращает последовательность, определяемую начальным значением seed и функцией nextFunction, которая вызывается для вычисления следующего значения на основе предыдущего на каждой итерации.

Ниже будут показаны некоторые примеры сравнения последовательностей Clojure и Kotlin.

1. Простой take из бесконечной последовательности одного статического значения

Clojure

(take 3 (repeat "Hello StackOverflow")) 

Котлин

generateSequence { "Hello StackOverflow" }.take(3).toList()

Они очень похожи. В Clojure мы можем использовать repeat, а в Kotlin просто generateSequence со статическим значением, которое будет возвращаться вечно. В обоих случаях take используется для определения количества элементов, которые мы хотим вычислить.

Примечание: в Kotlin мы преобразуем полученную последовательность в список с toList()


2. Простой take из бесконечной последовательности динамического значения

Clojure

(take 5 (iterate inc 1))

Котлин

generateSequence(1) { it.inc() }.take(5).toList()

Этот пример немного отличается, потому что последовательности дают бесконечное приращение предыдущего значения. Kotlin generateSequence может быть вызван с начальным значением (здесь: 1) и nextFunction (увеличивая предыдущее значение).


3. Циклическое повторение значений из списка.

Clojure

(take 5 (drop 2 (cycle [:first :second :third ])))
// (:third :first :second :third :first)

Котлин

listOf("first", "second", "third").let { elements ->
    generateSequence(0) {
        (it + 1) % elements.size
    }.map(elements::get)
}.drop(2).take(5).toList()

В этом примере мы циклически повторяем значения списка, отбрасываем первые два элемента, а затем берем 5. Это довольно многословно в Kotlin, потому что повторение элементов из списка непросто. Чтобы исправить это, простая функция расширения делает соответствующий код более читабельным:

fun <T> List<T>.cyclicSequence() = generateSequence(0) {
    (it + 1) % this.size
}.map(::get)

listOf("first", "second", "third").cyclicSequence().drop(2).take(5).toList()

4. Факториал

И последнее, но не менее важное: давайте посмотрим, как факторная проблема может быть решена с помощью последовательности Kotlin. Сначала рассмотрим версию Clojure:

Clojure

(defn factorial [n]
   (apply * (take n (iterate inc 1)))) 

Мы берем n значений из последовательности, которая дает возрастающее число, начиная с 1, и накапливаем их с помощью apply < / а>.

Котлин

fun factorial(n: Int) = generateSequence(1) { it.inc() }.take(n).fold(1) { v1, v2 ->
    v1 * v2
}

Kotlin предлагает fold, который позволяет нам легко накапливать значения .

person s1m0nw1    schedule 07.06.2018
comment
В частности, пример цикла показывает, насколько больше внимания Clojure уделяет функциональному коду. У Kotlin есть своя фортэ: императивная идиома с использованием сопрограмм. Я думаю, что это идиома, которую следует рекомендовать для сложных случаев вместо того, чтобы повсюду заставлять ФП: buildSequence { val items = listOf("first", "second", "third"); var i = 0; while (true) { yield(items[i++]); i %= items.size } } - person Marko Topolnik; 08.06.2018
comment
Дело в том, что императивная идиома очень легко расширяется до любого уровня сложности, не вкладывая каждый раз усилий в поиск только правильного примитива FP для определения, с точки зрения которого ваша текущая логика будет выглядеть проще. Такие примитивы, как правило, используются единожды во всей кодовой базе, но требуют от читателя их понимания и запоминания. - person Marko Topolnik; 08.06.2018