Iterable i Sequence Kotlina wyglądają dokładnie tak samo. Dlaczego potrzebne są dwa typy?

86

Oba te interfejsy definiują tylko jedną metodę

public operator fun iterator(): Iterator<T>

Dokumentacja mówi, Sequenceże ma być leniwy. Ale czy nie jest Iterableteż leniwy (chyba że jest poparty przez Collection)?

Venkata Raju
źródło

Odpowiedzi:

136

Kluczowa różnica polega na semantyce i implementacji funkcji rozszerzających stdlib dla Iterable<T>i Sequence<T>.

  • W przypadku Sequence<T>, gdy to możliwe, funkcje rozszerzeń działają leniwie, podobnie jak pośrednie operacje strumieni Java . Na przykład Sequence<T>.map { ... }zwraca inny Sequence<R>i faktycznie nie przetwarza elementów, dopóki nie zostanie wywołana operacja terminala , taka jak toListlub fold.

    Rozważ ten kod:

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

    Drukuje:

    before sum 1 2
    

    Sequence<T>jest przeznaczony do leniwego użytkowania i wydajnego przetwarzania potokowego, gdy chcesz maksymalnie zredukować pracę wykonywaną w operacjach terminalowych , tak samo jak w przypadku strumieni Java. Jednak lenistwo wprowadza pewien narzut, co jest niepożądane w przypadku zwykłych prostych przekształceń mniejszych kolekcji i sprawia, że ​​są mniej wydajne.

    Ogólnie rzecz biorąc, nie ma dobrego sposobu na określenie, kiedy jest to potrzebne, więc w Kotlin stdlib lenistwo jest jawne i wyodrębniane do Sequence<T>interfejsu, aby uniknąć używania go Iterabledomyślnie na wszystkich s.

  • Na Iterable<T>, na Przeciwnie, funkcje Przedłużacz z pośrednich semantyki operacyjnych pracują gorliwie, przetwarzać elementy od razu i powrócić inny Iterable. Na przykład Iterable<T>.map { ... }zwraca a List<R>z wynikami mapowania.

    Odpowiedni kod dla Iterable:

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

    To drukuje:

    1 2 before sum
    

    Jak wspomniano powyżej, Iterable<T>domyślnie nie jest leniwy, a to rozwiązanie pokazuje się dobrze: w większości przypadków ma dobrą lokalizację odniesienia, dzięki czemu wykorzystuje pamięć podręczną procesora, przewidywanie, wstępne pobieranie itp., Więc nawet wielokrotne kopiowanie kolekcji nadal działa dobrze wystarczająco dużo i działa lepiej w prostych przypadkach z małymi zbiorami.

    Jeśli potrzebujesz większej kontroli nad potokiem oceny, istnieje jawna konwersja do leniwej sekwencji z Iterable<T>.asSequence()funkcją function.

Klawisz skrótu
źródło
3
Prawdopodobnie duża niespodzianka dla Java(głównie Guava) fanów
Venkata Raju
@VenkataRaju dla osób funkcjonalnych mogą być zaskoczeni domyślną alternatywą leniwości.
Jayson Minard
9
Leniwy domyślnie jest zwykle mniej wydajny w przypadku mniejszych i częściej używanych kolekcji. Kopia może być szybsza niż leniwa ewaluacja, jeśli korzysta się z pamięci podręcznej procesora i tak dalej. Dlatego w typowych przypadkach lepiej nie być leniwym. I niestety typowe dla funkcji, takich jak kontrakty map, filtera inni nie posiadają wystarczających informacji, aby zdecydować, inne niż z rodzaju Źródło, a ponieważ większość kolekcje są również iterable, że nie jest dobrym markerem „leń”, ponieważ jest powszechnie WSZĘDZIE. leniwy musi być wyraźny, aby był bezpieczny.
Jayson Minard,
1
@naki Jeden z przykładów z niedawnego ogłoszenia Apache Spark. Oni oczywiście się tym martwią, zobacz sekcję „Obliczenia z obsługą pamięci podręcznej” na databricks.com/blog/2015/04/28/ ... ale martwią się miliardy rzeczy się powtarzają, więc muszą dojść do pełnej skrajności.
Jayson Minard
3
Ponadto częstą pułapką związaną z leniwą oceną jest uchwycenie kontekstu i przechowywanie wynikowych leniwych obliczeń w polu wraz ze wszystkimi przechwyconymi lokalnymi mieszkańcami i wszystkim, co posiadają. W związku z tym trudne do debugowania wycieki pamięci.
Ilya Ryzhenkov
49

Uzupełnianie odpowiedzi klawisza skrótu:

Ważne jest, aby zauważyć, jak sekwencja i iteracja iterują w twoich elementach:

Przykład sekwencji:

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

Wynik dziennika:

filtr - Mapa - Każdy; filtr - Mapa - Każdy

Przykład iterowalny:

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

filtr - filtr - Mapa - Mapa - Każda - Każda

Leandro Borges Ferreira
źródło
5
To doskonały przykład różnicy między nimi.
Alexey Soshin
To świetny przykład.
frye3k
2

Iterablejest mapowany na java.lang.Iterableinterfejs w JVMi jest implementowany przez powszechnie używane kolekcje, takie jak Lista lub Zestaw. Funkcje rozszerzające kolekcję na nich są oceniane chętnie, co oznacza, że ​​wszystkie natychmiast przetwarzają wszystkie elementy w swoich danych wejściowych i zwracają nową kolekcję zawierającą wynik.

Oto prosty przykład użycia funkcji kolekcji w celu uzyskania nazwisk pierwszych pięciu osób z listy w wieku co najmniej 21 lat:

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

Platforma docelowa: JVM Running on kotlin v. 1.3.61 Najpierw sprawdzanie wieku jest wykonywane dla każdej osoby na liście, a wynik umieszczany jest na zupełnie nowej liście. Następnie mapowanie na ich imiona jest wykonywane dla każdej osoby, która pozostała po operatorze filtru, kończąc na kolejnej nowej liście (to jest teraz a List<String>). Na koniec utworzono ostatnią nową listę zawierającą pięć pierwszych elementów poprzedniej listy.

Natomiast Sequence to nowa koncepcja Kotlina, która przedstawia leniwie oceniany zbiór wartości. Te same rozszerzenia kolekcji są dostępne dla Sequenceinterfejsu, ale natychmiast zwracają wystąpienia Sequence, które reprezentują przetworzony stan daty, ale bez faktycznego przetwarzania żadnych elementów. Aby rozpocząć przetwarzanie, Sequencenależy zakończyć je z operatorem terminala, są to w zasadzie żądanie do Sekwencji o zmaterializowanie danych, które reprezentuje, w jakiejś konkretnej formie. Przykłady obejmują toList, toSeti sum, żeby wymienić tylko kilka. Gdy zostaną one wywołane, tylko minimalna wymagana liczba elementów zostanie przetworzona w celu uzyskania żądanego wyniku.

Przekształcenie istniejącej kolekcji w sekwencję jest całkiem proste, wystarczy użyć asSequencerozszerzenia. Jak wspomniano powyżej, musisz również dodać operatora terminala, w przeciwnym razie Sekwencja nigdy nie będzie przetwarzać (znowu, leniwy!).

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

Platforma docelowa: JVMRunning na kotlin v. 1.3.61 W tym przypadku instancje Person w Sekwencji są sprawdzane pod kątem ich wieku, jeśli zdają, mają wyodrębnione nazwisko, a następnie dodane do listy wyników. Powtarza się to dla każdej osoby z oryginalnej listy, aż zostanie znalezionych pięć osób. W tym momencie funkcja toList zwraca listę, a reszta osób w elemencie Sequencenie jest przetwarzana.

Jest też coś więcej, do czego zdolna jest Sekwencja: może zawierać nieskończoną liczbę elementów. Patrząc z tego punktu widzenia, sensowne jest, aby operatorzy pracowali tak, jak robią - operator na nieskończonej sekwencji nigdy nie mógłby wrócić, gdyby wykonał swoją pracę chętnie.

Jako przykład, oto sekwencja, która wygeneruje tyle potęg 2, ile wymaga operator terminalu (ignorując fakt, że szybko się to przepełni):

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

Więcej znajdziesz tutaj .

Sazzad Hissain Khan
źródło