Clojure Lazy Sequences: Equivalents in Kotlin
Asked Answered
A

1

5

Clojure provides means for lazy evaluation of values in (infinite) sequences. With this, values will only be computed when they get actually consumed.

An example of an infinite sequence of one repeated element:

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

Using take helps to only consume as many elements from the sequence as we want. Without it, an OutOfMemoryError would kill the process quickly.

Another example of an infinite sequence is the following:

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

Or a more advanced sequence providing the factorial function:

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

Does Kotlin provide similar sequences? How do they look like?

I answered the question myself in order to document the knowledge here. This is fine according to Can I answer my own question?

Avarice answered 7/6, 2018 at 19:10 Comment(0)
A
6

In Kotlin, we can also make use of lazy evaluation using Sequences, too. In order to create a sequence, we may use generateSequence (with or without providing a seed.

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

Returns a sequence defined by the starting value seed and the function nextFunction, which is invoked to calculate the next value based on the previous one on each iteration.

The following will show some examples comparing Clojure with Kotlin sequences.

1. A simple take from an infinite sequence of one static value

Clojure

(take 3 (repeat "Hello StackOverflow")) 

Kotlin

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

These are pretty similar. In Clojure we can use repeat and in Kotlin it's simply generateSequence with a static value that will be yielded for ever. In both cases, take is being used in order to define the number of elements we want to compute.

Note: In Kotlin, we transform the resulting sequence into a list with toList()


2. A simple take from an infinite sequence of an dynamic value

Clojure

(take 5 (iterate inc 1))

Kotlin

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

This example is a bit different because the sequences yield the increment of the previous value infinitely. The Kotlin generateSequence can be invoked with a seed (here: 1) and a nextFunction (incrementing the previous value).


3. A cyclic repetition of values from a list

Clojure

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

Kotlin

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

In this example, we repeat the values of a list cyclically, drop the first two elements and then take 5. It happens to be quite verbose in Kotlin because repeating elements from the list isn't straightforward. In order to fix it, a simple extension function makes the relevant code more readable:

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

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

4. Factorial

Last but not least, let's see how the factorial problem can be solved with a Kotlin sequence. First, let's review the Clojure version:

Clojure

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

We take n values from a sequence that yields an incrementing number starting with 1 and accumulate them with the help of apply.

Kotlin

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

Kotlin offers fold which let's us accumulate the values easily.

Avarice answered 7/6, 2018 at 19:10 Comment(2)
Especially the cycling example shows how much more focus Clojure has on functional code. Kotlin has its own forté: imperative idiom using coroutines. I think that's the idiom to recommend for complex cases instead of forcing FP everywhere: buildSequence { val items = listOf("first", "second", "third"); var i = 0; while (true) { yield(items[i++]); i %= items.size } }Devitalize
The point is that the imperative idiom is very easy to extend to any kind of complexity, without investing effort every time into finding just the right FP primitive to define, in terms of which your current logic will look simpler. Such primitives tend to have a single use in the whole codebase, but require the reader to understand and memorize themDevitalize

© 2022 - 2024 — McMap. All rights reserved.