Iterable и Sequence Kotlin выглядят точно так же. Почему требуются два типа?

86

Оба этих интерфейса определяют только один метод

public operator fun iterator(): Iterator<T>

В документации написано Sequence, что нужно лениться. Но разве не Iterableленив (если не подкреплен Collection)?

Венката Раджу
источник

Ответы:

136

Ключевое различие заключается в семантике и реализации функций расширения stdlib для Iterable<T>и Sequence<T>.

  • Для Sequence<T>функций расширения там, где это возможно, работают лениво, аналогично промежуточным операциям Java Streams . Например, Sequence<T>.map { ... }возвращает другой Sequence<R>и фактически не обрабатывает элементы, пока не будет вызвана операция терминала, такая как toListили fold.

    Рассмотрим этот код:

    val seq = sequenceOf(1, 2)
    val seqMapped: Sequence<Int> = seq.map { print("$it "); it * it } // intermediate
    print("before sum ")
    val sum = seqMapped.sum() // terminal
    

    Он печатает:

    before sum 1 2
    

    Sequence<T>предназначен для ленивого использования и эффективной конвейерной обработки, когда вы хотите максимально сократить объем работы, выполняемой в терминальных операциях, как и Java Streams. Однако лень приводит к некоторым накладным расходам, что нежелательно для обычных простых преобразований небольших коллекций и делает их менее производительными.

    В общем, нет хорошего способа определить, когда это необходимо, поэтому в Kotlin stdlib "лень" делается явной и извлекается в Sequence<T>интерфейс, чтобы Iterableпо умолчанию не использовать ее во всех s.

  • Для Iterable<T>, напротив, функции расширения с промежуточной операцией семантикой работы с нетерпением, обрабатывать детали сразу и вернуть другие Iterable. Например, Iterable<T>.map { ... }возвращает List<R>с результатами сопоставления в нем.

    Эквивалентный код для Iterable:

    val lst = listOf(1, 2)
    val lstMapped: List<Int> = lst.map { print("$it "); it * it }
    print("before sum ")
    val sum = lstMapped.sum()
    

    Это распечатывает:

    1 2 before sum
    

    Как сказано выше, Iterable<T>по умолчанию не ленив, и это решение хорошо себя показывает: в большинстве случаев у него хорошая локальность ссылки, что позволяет использовать кеш-память процессора, прогнозирование, предварительную выборку и т. Д., Так что даже многократное копирование коллекции по-прежнему работает хорошо. достаточно и лучше работает в простых случаях с небольшими коллекциями.

    Если вам нужен больший контроль над конвейером оценки, есть явное преобразование в ленивую последовательность с Iterable<T>.asSequence()функцией.

горячая клавиша
источник
3
Наверное, большой сюрприз для Java(в основном Guava) фанатов
Венката Раджу
@VenkataRaju для функциональных людей, возможно, они будут удивлены альтернативой ленивости по умолчанию.
Джейсон Минард,
9
Ленивый по умолчанию обычно менее эффективен для небольших и более часто используемых коллекций. Копия может быть быстрее, чем ленивый eval, если использовать кеш процессора и так далее. Так что для обычных случаев использования лучше не лениться. И, к сожалению, общие контракты для таких функций, как map, filterи другие, не несут достаточно информации для принятия решения, кроме как из исходного типа коллекции, и, поскольку большинство коллекций также являются Iterable, это не хороший маркер для «лениться», потому что это обычно ВЕЗДЕ. lazy должен быть явным, чтобы быть в безопасности.
Джейсон Минард
1
@naki Один пример из недавнего объявления Apache Spark, они явно обеспокоены этим, см. раздел «Вычисления с учетом кеша » на databricks.com/blog/2015/04/28/… ... но они обеспокоены миллиардами вещи повторяются, поэтому им нужно довести до крайности.
Jayson Minard
3
Кроме того, распространенная ошибка ленивых вычислений - это захват контекста и сохранение результатов ленивых вычислений в поле вместе со всеми захваченными локальными переменными и всем, что они содержат. Следовательно, трудно отлаживать утечки памяти.
Илья Рыженков
49

Завершение ответа горячей клавиши:

Важно заметить, как Sequence и Iterable повторяются в ваших элементах:

Пример последовательности:

list.asSequence().filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

Результат журнала:

фильтр - Карта - Каждый; фильтр - Карта - Каждый

Итерационный пример:

list.filter { field ->
    Log.d("Filter", "filter")
    field.value > 0
}.map {
    Log.d("Map", "Map")
}.forEach {
    Log.d("Each", "Each")
}

фильтр - фильтр - Карта - Карта - Каждый - Каждый

Леандро Борхес Феррейра
источник
5
Это отличный пример разницы между ними.
Алексей Сошин
Это отличный пример.
frye3k
2

Iterableсопоставляется с java.lang.Iterableинтерфейсом в JVMи реализуется часто используемыми коллекциями, такими как List или Set. Функции расширения коллекции на них быстро оцениваются, что означает, что все они немедленно обрабатывают все элементы во входных данных и возвращают новую коллекцию, содержащую результат.

Вот простой пример использования функций сбора данных для получения имен первых пяти человек в списке, возраст которых составляет не менее 21 года:

val people: List<Person> = getPeople()
val allowedEntrance = people
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)

Целевая платформа: JVMRunning on kotlin v. 1.3.61 Во-первых, проверка возраста выполняется для каждого отдельного человека в списке, а результат помещается в новый список. Затем выполняется сопоставление с их именами для каждого человека, который остался после оператора фильтра, и попадает в еще один новый список (теперь это a List<String>). Наконец, создается последний новый список, содержащий первые пять элементов предыдущего списка.

Напротив, Sequence - это новая концепция в Kotlin, представляющая лениво оцениваемую коллекцию значений. Те же расширения коллекции доступны для Sequenceинтерфейса, но они немедленно возвращают экземпляры Sequence, которые представляют обработанное состояние даты, но без фактической обработки каких-либо элементов. Чтобы начать обработку, Sequenceнеобходимо завершить обработку оператором терминала, это в основном запрос к последовательности для материализации данных, которые она представляет в некоторой конкретной форме. Примеры включают toList, toSetи sum, если упомянуть лишь некоторые из них. Когда они вызываются, будет обработано только минимально необходимое количество элементов для получения требуемого результата.

Преобразовать существующую коллекцию в последовательность довольно просто, вам просто нужно использовать asSequenceрасширение. Как упоминалось выше, вам также необходимо добавить оператор терминала, иначе Sequence никогда не будет выполнять никакой обработки (опять же, ленивый!).

val people: List<Person> = getPeople()
val allowedEntrance = people.asSequence()
    .filter { it.age >= 21 }
    .map { it.name }
    .take(5)
    .toList()

Целевая платформа: JVMRunning on kotlin v. 1.3.61 В этом случае каждый экземпляр Person в последовательности проверяется на свой возраст, если они проходят, их имена извлекаются, а затем добавляются в список результатов. Это повторяется для каждого человека в исходном списке, пока не будет найдено пять человек. На этом этапе функция toList возвращает список, а остальные участники Sequenceне обрабатываются.

Есть еще кое-что, на что способна Последовательность: она может содержать бесконечное количество элементов. С этой точки зрения логично, что операторы работают так же, как и они - оператор бесконечной последовательности никогда не сможет вернуться, если он будет выполнять свою работу с энтузиазмом.

В качестве примера, вот последовательность, которая будет генерировать столько степеней двойки, сколько требуется оператору терминала (игнорируя тот факт, что это может быстро переполниться):

generateSequence(1) { n -> n * 2 }
    .take(20)
    .forEach(::println)

Вы можете найти больше здесь .

Сазад Хисейн Хан
источник