Dlaczego „zip” ignoruje zwisający ogon kolekcji?

12

C # , Scala, Haskell, Lisp i Pythonzip zachowują się tak samo : jeśli jedna kolekcja jest dłuższa, ogon jest cicho ignorowany.

Może to być również wyjątek, ale nie słyszałem o żadnym języku używającym takiego podejścia.

To mnie zastanawia. Czy ktoś wie, dlaczego zipjest tak zaprojektowany? Sądzę, że w przypadku nowych języków jest to zrobione, ponieważ inne języki robią to w ten sposób. Ale jaki był główny powód?

Zadaję tu oparte na faktach, historyczne pytanie, nie jeśli komuś się to podoba, czy jest to dobre czy złe podejście.

Aktualizacja : Gdybym został zapytany, co robić, powiedziałbym - rzuć wyjątek, podobnie jak indeksowanie tablicy (pomimo, że „stare” języki robiły wszelkiego rodzaju magię, jak radzić sobie z indeksem poza granicami, UB, rozszerzanie tablicy, itp).

Greenoldman
źródło
10
Gdyby nie zignorował ogona, który ma jeden funkt, użycie nieskończonych sekwencji byłoby bardziej kłopotliwe. Zwłaszcza jeśli uzyskanie długości nieskończonego zakresu było kosztowne / zawiłe / niemożliwe.
Deduplicator
2
Wydaje ci się, że sądzisz, że jest to nieoczekiwane i dziwne. Uważam to za oczywiste i rzeczywiście nieuniknione. Co chciałbyś zrobić , gdy spakujesz kolekcje o nierównej długości?
Kilian Foth,
@KilianFoth, zgłoszony wyjątek.
greenoldman
@Deduplicator, miło. Dzięki cichej kropli ogona możesz naturalnie wyrazić, zipWithIndexgenerując liczby naturalne. Teraz jedyny brakujący fragment informacji - jaki był tego powód? :-) (btw. proszę ponownie opublikować swój komentarz jako odpowiedź, dziękuję).
greenoldman
1
Python ma itertools.izip_longest, który skutecznie autopaduje zakończone dane wejściowe za pomocą Nones. Często wybieram go zamiast zip, kiedy faktycznie używam zip; nie pamiętam już powodów, dla których dokonałem wyboru. Python ma już funkcję enumerate () dla sprawy @ greenoldman, z której często korzystam.
StarWeaver,

Odpowiedzi:

11

Prawie zawsze jest to, czego chcesz, a kiedy nie jest, możesz zrobić to sam.

Główny problem dotyczy leniwej semantyki, której nie znasz długości przy pierwszym uruchomieniu zip, więc nie możesz po prostu rzucić wyjątku na początku. Musisz najpierw zwrócić wszystkie wspólne elementy, a następnie zgłosić wyjątek, który nie byłby bardzo przydatny.

To także kwestia stylu. Niezwykle programiści są przyzwyczajeni do ręcznego sprawdzania warunków brzegowych w dowolnym miejscu. Funkcjonalni programiści preferują konstrukcje, które nie mogą zawieść z założenia. Wyjątki są niezwykle rzadkie. Jeśli istnieje sposób na zwrócenie rozsądnej wartości domyślnej przez funkcję, programiści funkcjonalni ją przyjmą. Kompozycja jest królem.

Karl Bielefeldt
źródło
Pytam o powody historyczne, a nie o to, co mogę zrobić. Drugi akapit - mylisz się, spójrz, jak zipjest obecnie wdrażany. Wyjątkiem od rzucania jest po prostu zmiana „stop fed” na „throw”. Trzeci akapit - zwracanie pustego elementu do przekroczenia granicy nie może zawieść, ale wątpię, aby którykolwiek programista FP głosowałby, że to dobry projekt.
greenoldman
3
Mój drugi akapit nie dotyczy wszystkich implementacji, tylko tych naprawdę leniwych. Jeśli zipdwie nieskończone sekwencje razem, nie znacie rozmiaru na początku. W trzecim akapicie powiedziałem, że jest to uzasadnione naruszenie. Zwrócenie pustego w tym przypadku nie byłoby rozsądne, podczas gdy oczywiście upuszczenie ogona.
Karl Bielefeldt
Ach, wreszcie rozumiem, o co ci chodzi - z rzucaniem wyjątku w leniwym języku nie jest to techniczna wymiana, to całkowicie zmiana zachowania, ponieważ musisz rzucić wyjątek od samego początku, a ty możesz zignorować ogon, kiedy jest to dogodne.
greenoldman
3
+1 jest to również świetna odpowiedź: „Programiści funkcjonalni wolą konstrukcje, które nie mogą zawieść w projekcie”, dlatego elokwentnie stwierdza, jaki jest największy motywator większości decyzji projektowych podejmowanych przez funkcjonalnych programistów. Programiści mają zasadę, która im się podoba: „Mów, nie pytaj”, FP przenosi to do N-tego stopnia, skupiając się na umożliwieniu ciągłego mówienia instrukcji bez konieczności sprawdzania wyników do absolutnej ostatniej chwili, więc staramy się zapewnić pośrednie kroki nie może zawieść, ponieważ Kompozycja jest królem. Bardzo dobrze powiedziane.
Jimmy Hoffa
12

Ponieważ nie ma oczywistego sposobu na ukończenie ogona. Jakikolwiek wybór, jak to zrobić, spowodowałoby nieoczywisty ogon.

Sztuką jest wyraźne wydłużenie najkrótszej listy, aby dopasować długość najdłuższej do oczekiwanych wartości.

Jeśli zip zrobił to za Ciebie, nie mogłeś wiedzieć, jakie wartości wypełniał intuicyjnie. Czy przeglądał listę? Czy to powtórzyło wartość pustą? Jaka jest wartość pusta dla twojego typu?

Nie ma żadnego wpływu na to, co robi zip, którego można by użyć do intuicyjnego sposobu wydłużania ogona, więc jedyną rozsądną rzeczą do zrobienia jest praca z dostępnymi wartościami, a nie wymyślanie czegoś, czego konsument może się nie spodziewać.


Pamiętaj też, że masz na myśli bardzo konkretną dobrze znaną funkcję z określoną dobrze znaną semantyką. Ale to nie znaczy, że nie możesz wykonać podobnej, ale nieco innej funkcji . Tylko dlatego, że jest to wspólna funkcja robi x, nie znaczy, że nie może decydować za dany cel, który chcesz zrobić xa y.

Pamiętaj jednak, że ta i wiele innych powszechnych funkcji w stylu FP są wspólne, ponieważ są one proste i uogólnione, dzięki czemu możesz dostosować kod, aby z nich korzystać i uzyskać pożądane zachowanie. Na przykład w języku C # można po prostu

IEnumerable<Tuple<T, U>> ZipDefaults(IEnumerable<T> first, IEnumerable<U> second)
{
    return first.Count() < second.Count()
        ? first.Concat(Enumerable.Repeat(default(T), second.Count() - first.Count())).Zip(second)
        : first.Zip(second.Concat(Enumerable.Repeat(default(U), first.Count() - second.count())))
}

Lub inne proste rzeczy. Podejścia FP sprawiają, że modyfikacje są tak łatwe, ponieważ można ponownie wykorzystywać elementy, a także mieć tak małe implementacje, że tworzenie własnych zmodyfikowanych wersji rzeczy jest niezwykle proste.

Jimmy Hoffa
źródło
Ok, ale dzieje się tak tylko wtedy, gdy zmusisz kolekcje do zrobienia czegoś pasującego do innego - porównaj to z indeksowaniem kolekcji (tablicy). Możesz zacząć myśleć, czy powinienem rozwinąć i tablicować, jeśli mam indeks poza granicami? A może po cichu zignoruj ​​prośbę. Ale od pewnego czasu istnieje powszechne pojęcie wyjątku. To samo tutaj - jeśli nie masz pasującej kolekcji, wyrzuć wyjątek. Dlaczego nie zastosowano tego podejścia?
greenoldman
2
zipmoże wypełnić wartości zerowe, co jest często intuicyjnym rozwiązaniem. Rozważ typ zip :: [a] -> [b] -> [(Maybe a, Maybe b)]. To prawda, że ​​typ wyniku jest nieco ^ H ^ H dość niepraktyczny, ale pozwoliłby łatwo zaimplementować na nim jakiekolwiek inne zachowanie (skrót, wyjątek).
amon
1
@amon: To wcale nie jest intuicyjne, to głupie. Wymagałoby to po prostu sprawdzenia wartości zerowej każdego argumentu.
DeadMG
4
@amon nie każdy typ ma wartość null, to właśnie mam na myśli mempty, że obiekty mają wartość null, aby wypełnić przestrzeń, ale chcesz, aby wymyśliła coś takiego dla int i innych typów? Jasne, C # ma, default(T)ale nie wszystkie języki, a nawet dla C # czy to naprawdę oczywiste zachowanie? Nie sądzę
Jimmy Hoffa
1
@amon Prawdopodobnie bardziej przydatne byłoby zwrócenie nieużywanej części dłuższej listy. Możesz użyć tego, aby sprawdzić, czy po fakcie były równej długości, jeśli to konieczne, i nadal możesz ponownie rozpakować zamek błyskawiczny lub zrobić coś z nieprzytomnym ogonem bez ponownego przeglądania listy.
Doval