Iterable e Sequence de Kotlin são exatamente iguais. Por que dois tipos são necessários?


86

Ambas as interfaces definem apenas um método

public operator fun iterator(): Iterator<T>

A documentação diz que Sequenceé para ser preguiçoso. Mas também não é Iterablepreguiçoso (a menos que seja apoiado por a Collection)?

Respostas:


136

A principal diferença reside na semântica e na implementação das funções de extensão stdlib para Iterable<T>e Sequence<T>.

  • Pois Sequence<T>, as funções de extensão executam lentamente onde possível, de forma semelhante às operações intermediárias do Java Streams . Por exemplo, Sequence<T>.map { ... }retorna outro Sequence<R>e não processa realmente os itens até que uma operação de terminal como toListou foldseja chamada.

    Considere este código:

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

    Ele imprime:

    before sum 1 2
    

    Sequence<T>destina-se a uso lento e pipelining eficiente quando você deseja reduzir o trabalho feito em operações de terminal tanto quanto possível, o mesmo para Java Streams. No entanto, a preguiça introduz alguma sobrecarga, o que é indesejável para transformações simples comuns de coleções menores e as torna menos eficientes.

    Em geral, não há uma boa maneira de determinar quando ele é necessário, portanto, no Kotlin stdlib, a preguiça é explicitada e extraída para a Sequence<T>interface para evitar o uso em todos os programas Iterablepor padrão.

  • Pois Iterable<T>, ao contrário, as funções de extensão com semântica de operação intermediária funcionam avidamente, processam os itens imediatamente e retornam outro Iterable. Por exemplo, Iterable<T>.map { ... }retorna um List<R>com os resultados do mapeamento nele.

    O código equivalente para Iterable:

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

    Isso imprime:

    1 2 before sum
    

    Como dito acima, Iterable<T>não é preguiçoso por padrão, e esta solução mostra-se bem: na maioria dos casos tem boa localidade de referência , tirando proveito do cache da CPU, predição, pré-busca etc. para que mesmo a cópia múltipla de uma coleção ainda funcione o suficiente e tem melhor desempenho em casos simples com pequenas coleções.

    Se você precisar de mais controle sobre o pipeline de avaliação, há uma conversão explícita para uma sequência preguiçosa com Iterable<T>.asSequence()função.


3
Provavelmente uma grande surpresa para os fãs Java(principalmente Guava)
Venkata Raju

@VenkataRaju para pessoas funcionais, eles podem se surpreender com a alternativa de preguiçoso por padrão.
Jayson Minard

9
Por padrão, lento geralmente tem menos desempenho para coleções menores e mais comumente usadas. Uma cópia pode ser mais rápida do que uma avaliação preguiçosa se tirar proveito do cache da CPU e assim por diante. Portanto, para casos de uso comuns, é melhor não ser preguiçoso. E, infelizmente, os contratos comuns para funções como map, filtere outros não carregam informações suficientes para decidir além do tipo de coleção de origem, e uma vez que a maioria das coleções também são Iteráveis, isso não é um bom marcador para "ser preguiçoso" porque é comumente EM TODOS OS LUGARES. preguiçoso deve ser explícito para ser seguro.
Jayson Minard

1
@naki Um exemplo de um anúncio recente do Apache Spark, eles estão preocupados com isso, obviamente, consulte a seção "Cache-aware Computation" em databricks.com/blog/2015/04/28/… ... mas eles estão preocupados com bilhões de coisas iterando, então eles precisam ir ao extremo.
Jayson Minard

3
Além disso, uma armadilha comum com a avaliação preguiçosa é capturar o contexto e armazenar a computação preguiçosa resultante em um campo junto com todos os locais capturados e tudo o que eles contêm. Portanto, é difícil depurar vazamentos de memória.
Ilya Ryzhenkov

49

Concluindo a resposta da tecla de atalho:

É importante observar como Sequence e Iterable itera em todos os seus elementos:

Exemplo de sequência:

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

Resultado do log:

filtro - Mapa - Cada; filtro - Mapa - Cada

Exemplo iterável:

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

filtro - filtro - Mapa - Mapa - Cada - Cada


5
Esse é um excelente exemplo da diferença entre os dois.
Alexey Soshin

Este é um excelente exemplo.
frye3k

2

Iterableé mapeado para a java.lang.Iterableinterface no JVMe é implementado por coleções comumente usadas, como Lista ou Conjunto. As funções de extensão da coleção são avaliadas avidamente, o que significa que todas processam imediatamente todos os elementos em sua entrada e retornam uma nova coleção contendo o resultado.

Aqui está um exemplo simples de como usar as funções de coleção para obter os nomes das primeiras cinco pessoas em uma lista com pelo menos 21 anos:

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

Plataforma de destino: JVMRunning no kotlin v. 1.3.61 Primeiro, a verificação de idade é feita para cada pessoa na lista, com o resultado colocado em uma lista totalmente nova. Em seguida, o mapeamento de seus nomes é feito para cada Pessoa que permaneceu após o operador de filtro, terminando em mais uma nova lista (agora é a List<String>). Finalmente, há uma última nova lista criada para conter os primeiros cinco elementos da lista anterior.

Em contraste, Sequence é um novo conceito em Kotlin para representar uma coleção de valores avaliada preguiçosamente. As mesmas extensões de coleção estão disponíveis para a Sequenceinterface, mas retornam imediatamente instâncias de Sequence que representam um estado processado da data, mas sem realmente processar nenhum elemento. Para iniciar o processamento, o Sequencedeve ser encerrado com um operador de terminal, trata-se basicamente de um pedido à Sequência para materializar os dados que representa de alguma forma concreta. Os exemplos incluem toList, toSete sum, para mencionar apenas alguns. Quando eles são chamados, apenas o número mínimo necessário de elementos será processado para produzir o resultado exigido.

Transformar uma coleção existente em uma sequência é bastante simples, você só precisa usar a asSequenceextensão. Como mencionado acima, você também precisa adicionar um operador de terminal, caso contrário, a Sequência nunca fará nenhum processamento (de novo, preguiçoso!).

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

Plataforma de destino: JVMRunning no kotlin v. 1.3.61 Nesse caso, as instâncias de Person na Sequência são verificadas quanto à idade, se passarem, seus nomes serão extraídos e, em seguida, adicionados à lista de resultados. Isso é repetido para cada pessoa na lista original até que cinco pessoas sejam encontradas. Nesse ponto, a função toList retorna uma lista e o restante das pessoas no Sequencenão são processadas.

Há também algo extra de que uma Sequência é capaz: ela pode conter um número infinito de itens. Com isso em perspectiva, faz sentido que os operadores trabalhem da maneira que o fazem - um operador em uma sequência infinita nunca poderia retornar se fizesse seu trabalho avidamente.

Como exemplo, aqui está uma sequência que irá gerar tantas potências de 2 quantas forem exigidas por seu operador de terminal (ignorando o fato de que isso transbordaria rapidamente):

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

Você pode encontrar mais aqui .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.