Dlaczego wszędzie nie stosuje się leniwej oceny?


32

Właśnie dowiedziałem się, jak działa leniwa ocena i zastanawiałem się: dlaczego leniwa ocena nie jest stosowana w każdym aktualnie produkowanym oprogramowaniu? Dlaczego nadal chętnie korzystasz z oceny?


2
Oto przykład tego, co może się zdarzyć, jeśli zmiksujesz stan zmienny z leniwą oceną. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström

2
@ JonasElfström: Proszę nie mylić stanu zmiennego z jedną z jego możliwych implementacji. Zmienny stan może zostać zaimplementowany przy użyciu nieskończonego, leniwego strumienia wartości. Wtedy nie masz problemu ze zmiennymi zmiennymi.
Giorgio

W imperatywnych językach programowania „leniwa ocena” wymaga od programisty świadomego wysiłku. Programowanie ogólne w językach imperatywnych ułatwiło to, ale nigdy nie będzie przejrzyste. Odpowiedź na drugą stronę pytania rodzi inne pytanie: „Dlaczego wszędzie nie używa się funkcjonalnych języków programowania?”, A obecnie odpowiedź jest po prostu „nie” w związku z bieżącymi sprawami.
rwong

2
Funkcjonalne języki programowania nie są wszędzie stosowane z tego samego powodu, dla którego nie używamy młotów do śrub, nie każdy problem można łatwo wyrazić za pomocą funkcjonalnego wejścia -> sposób wyjścia, na przykład GUI lepiej nadaje się do wyrażenia w trybie rozkazującym .
ALXGTV,

Ponadto istnieją dwie klasy funkcjonalnych języków programowania (lub przynajmniej oba twierdzą, że są funkcjonalne), imperatywne języki funkcjonalne, np. Clojure, Scala i deklaratywne, np. Haskell, OCaml.
ALXGTV,

Odpowiedzi:


38

Leniwa ocena wymaga narzutów księgowych - musisz wiedzieć, czy została jeszcze oceniona i takie rzeczy. Chętna ocena jest zawsze oceniana, więc nie musisz wiedzieć. Jest to szczególnie prawdziwe w równoległych kontekstach.

Po drugie, przekształcenie chętnej oceny w leniwą ocenę jest trywialne poprzez spakowanie jej w obiekt funkcji, który można później wywołać, jeśli chcesz.

Po trzecie, leniwa ocena oznacza utratę kontroli. Co jeśli leniwie oceniłem odczytanie pliku z dysku? A może masz czas? To nie do przyjęcia.

Szybka ewaluacja może być bardziej wydajna i łatwiejsza do kontrolowania, i jest w trywialny sposób przekształcana w leniwą ewaluację. Dlaczego miałbyś chcieć leniwej oceny?


10
Leniwe czytanie pliku z dysku jest naprawdę fajne - dla większości moich prostych programów i skryptów Haskell's readFilejest dokładnie tym , czego potrzebuję. Poza tym przejście z leniwej na chętną ocenę jest równie trywialne.
Tikhon Jelvis,

3
Zgadzam się ze wszystkimi, z wyjątkiem ostatniego akapitu. Leniwa ocena jest bardziej wydajna, gdy występuje operacja łańcuchowa, i może mieć większą kontrolę nad tym, kiedy rzeczywiście potrzebujesz danych
texasbruce

4
Prawa dotyczące funktorów chcieliby porozmawiać z tobą o „utracie kontroli”. Jeśli piszesz czyste funkcje, które działają na niezmiennych typach danych, leniwa ocena jest darem niebios. Języki takie jak haskell opierają się zasadniczo na lenistwie. Jest to uciążliwe w niektórych językach, szczególnie gdy jest mieszane z „niebezpiecznym” kodem, ale sprawiasz, że lenistwo jest domyślnie niebezpieczne lub złe. Jest niebezpieczny tylko w niebezpiecznym kodzie.
sara

1
@DeadMG Nie, jeśli zależy Ci na tym, czy Twój kod się skończy ... Co head [1 ..]daje ci chętnie oceniany czysty język, ponieważ w Haskell daje 1?
średnik

1
W przypadku wielu języków wdrożenie leniwej oceny co najmniej wprowadzi złożoność. Czasami taka złożoność jest potrzebna, a leniwa ocena poprawia ogólną wydajność - szczególnie jeśli to, co jest oceniane, jest tylko warunkowo potrzebne. Jednak źle wykonane może wprowadzić subtelne błędy lub trudne do wyjaśnienia problemy z wydajnością z powodu złych założeń podczas pisania kodu. Jest kompromis.
Berin Loritsch

17

Głównie dlatego, że leniwy kod i stan mogą się źle mieszać i powodować trudności w znalezieniu błędów. Jeśli stan obiektu zależnego zmienia się, wartość twojego leniwego obiektu może być niepoprawna podczas oceny. O wiele lepiej jest, aby programista jawnie kodował obiekt, aby był leniwy, gdy wie, że sytuacja jest odpowiednia.

Na marginesie Haskell do wszystkiego używa oceny Lazy. Jest to możliwe, ponieważ jest to język funkcjonalny i nie używa stanu (z wyjątkiem kilku wyjątkowych okoliczności, w których są wyraźnie oznaczone)


Tak, zmienny stan + leniwa ocena = śmierć. Myślę, że jedyne punkty, które straciłem podczas finału SICP, dotyczyły wykorzystania set!w leniwym tłumaczu Scheme. > :(
Tikhon Jelvis

3
„leniwy kod i stan mogą się źle mieszać”: To naprawdę zależy od sposobu implementacji stanu. Jeśli zaimplementujesz go przy użyciu współdzielonych zmiennych zmiennych i zależysz od kolejności oceny, aby twój stan był spójny, to masz rację.
Giorgio

14

Leniwe oceny nie zawsze są lepsze.

Korzyści płynące z leniwej oceny mogą być świetne, ale nie jest trudno uniknąć większości niepotrzebnych ocen w chętnych środowiskach - z pewnością leniwy sprawia, że ​​jest to łatwe i kompletne, ale rzadko niepotrzebna ocena w kodzie jest poważnym problemem.

Zaletą leniwej oceny jest to, że pozwala ona pisać wyraźniejszy kod; zdobycie dziesiątej liczby pierwszej przez filtrowanie listy nieskończonych liczb naturalnych i przyjęcie dziesiątego elementu tej listy jest jednym z najbardziej zwięzłych i jasnych sposobów postępowania: (pseudokod)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Sądzę, że tak zwięźle byłoby wyrażać rzeczy bez lenistwa.

Ale lenistwo nie jest odpowiedzią na wszystko. Po pierwsze, lenistwo nie może być stosowane w sposób przejrzysty w obecności stanu i uważam, że stanowość nie może zostać automatycznie wykryta (chyba że pracujesz np. Haskell, kiedy stan jest dość wyraźny). Tak więc w większości języków lenistwo musi być wykonywane ręcznie, co sprawia, że ​​rzeczy stają się mniej wyraźne, a tym samym usuwa jedną z wielkich zalet leniwej ewaluacji.

Ponadto lenistwo ma wady wydajności, ponieważ pociąga za sobą znaczny narzut związany z utrzymywaniem nieocenionych wyrażeń; zużywają pamięć i pracują wolniej niż proste wartości. Nierzadko zdarza się, że musisz kodować z entuzjazmem, ponieważ leniwa wersja jest wolna od psów i czasami trudno jest uzasadnić wydajność.

Jak to zwykle bywa, nie ma absolutnie najlepszej strategii. Leniwy jest świetny, jeśli możesz napisać lepszy kod, korzystając z nieskończonych struktur danych lub innych strategii, z których możesz korzystać, ale chętny może być łatwiej zoptymalizować.


Czy naprawdę sprytny kompilator byłby w stanie znacznie zmniejszyć koszty ogólne? a nawet skorzystać z lenistwa dla dodatkowych optymalizacji?
Tikhon Jelvis,

3

Oto krótkie porównanie zalet i wad chętnej i leniwej oceny:

  • Chętna ocena:

    • Potencjalny koszt niepotrzebnej oceny rzeczy.

    • Szybka ocena bez przeszkód.

  • Leniwa ocena:

    • Brak zbędnej oceny.

    • Koszty księgowości przy każdym użyciu wartości.

Tak więc, jeśli masz wiele wyrażeń, których nigdy nie trzeba oceniać, leniwy jest lepszy; ale jeśli nigdy nie masz wyrażenia, którego nie trzeba oceniać, leniwy to czysty narzut.

Spójrzmy teraz na oprogramowanie świata rzeczywistego: ile funkcji, które piszesz, nie wymaga oceny wszystkich ich argumentów? Zwłaszcza w przypadku nowoczesnych krótkich funkcji, które wykonują tylko jedną rzecz, odsetek funkcji należących do tej kategorii jest bardzo niski. Tak więc leniwa ocena po prostu wprowadziłaby koszty księgowości przez większość czasu, bez szansy na faktyczne ocalenie.

W rezultacie leniwa ocena po prostu nie opłaca się średnio, chętna ocena lepiej pasuje do nowoczesnego kodu.


1
„Narzut księgowości przy każdym użyciu wartości.”: Nie sądzę, że narzut księgowości jest większy niż, powiedzmy, sprawdzanie zerowych referencji w języku takim jak Java. W obu przypadkach musisz sprawdzić jeden bit informacji (obliczony / oczekujący w porównaniu z zerowym / innym niż pusty) i musisz to zrobić za każdym razem, gdy użyjesz wartości. Tak, tak, jest narzut, ale jest minimalny.
Giorgio

1
„Ile funkcji, które piszesz, nie wymaga oceny wszystkich ich argumentów?”: To tylko jedna przykładowa aplikacja. Co z rekurencyjnymi, nieskończonymi strukturami danych? Czy potrafisz je wdrożyć z niecierpliwością? Możesz używać iteratorów, ale rozwiązanie nie zawsze jest tak zwięzłe. Oczywiście prawdopodobnie nie przegapisz czegoś, z czego nigdy nie miałeś okazji skorzystać.
Giorgio

2
„W konsekwencji leniwa ewaluacja po prostu nie płaci średnio, chętna ewaluacja lepiej pasuje do nowoczesnego kodu.”: To stwierdzenie nie ma zastosowania: tak naprawdę zależy od tego, co próbujesz wdrożyć.
Giorgio

1
@Giorgio Koszty ogólne mogą ci się nie wydawać dużo, ale warunkowe to jedna z rzeczy, do których przyssają się współczesne procesory: błędnie przewidywana gałąź zwykle wymusza całkowite przepłukanie rurociągu, odrzucając pracę ponad dziesięciu cykli procesora. Nie chcesz niepotrzebnych warunków w swojej wewnętrznej pętli. Płacenie dodatkowych dziesięciu cykli za argument funkcji jest prawie tak samo niedopuszczalne dla kodu wrażliwego na wydajność, jak kodowanie rzeczy w java. Masz rację, że leniwa ocena pozwala ci wykonać kilka sztuczek, których nie da się łatwo zrobić z niecierpliwością. Ale ogromna większość kodu nie potrzebuje tych sztuczek.
cmaster

2
To wydaje się być odpowiedzią na brak doświadczenia w językach z leniwą oceną. Na przykład, co z nieskończonymi strukturami danych?
Andres F.,

3

Jak zauważył @DeadMG, Leniwa ocena wymaga narzutów księgowych. Może to być kosztowne w porównaniu z chętną oceną. Rozważ to oświadczenie:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

To zajmie trochę obliczeń. Jeśli używam leniwej oceny, muszę sprawdzać, czy została ona oceniona za każdym razem, gdy go używam. Jeśli znajduje się on w mocno używanej ciasnej pętli, wówczas narzut znacznie wzrasta, ale nie ma korzyści.

Dzięki chętnej ocenie i przyzwoitemu kompilatorowi formuła jest obliczana w czasie kompilacji. Większość optymalizatorów przeniesie przypisanie poza pętle, w których się pojawi, jeśli to konieczne.

Leniwa ocena najlepiej nadaje się do wczytywania danych, do których dostęp będzie rzadko uzyskiwany i wymaga dużego pobierania narzutu. Dlatego bardziej odpowiednie jest zbijanie skrzynek niż podstawowa funkcjonalność.

Zasadniczo dobrą praktyką jest ocena rzeczy, do których często uzyskuje się dostęp tak wcześnie, jak to możliwe. Leniwa ocena nie działa z tą praktyką. Jeśli zawsze będziesz mieć dostęp do czegoś, wszystko, co leniwa ocena zrobi, to narzut. Koszt / korzyść korzystania z leniwej oceny maleje, gdy dostęp do elementu staje się mniej prawdopodobny.

Zawsze stosowanie leniwej oceny oznacza również wczesną optymalizację. Jest to zła praktyka, która często powoduje, że kod jest o wiele bardziej złożony i kosztowny, w przeciwnym razie może tak być. Niestety, przedwczesna optymalizacja często powoduje, że kod działa wolniej niż kod prostszy. Dopóki nie będziesz w stanie zmierzyć efektu optymalizacji, optymalizacja kodu jest złym pomysłem.

Unikanie przedwczesnej optymalizacji nie jest sprzeczne z dobrymi praktykami kodowania. Jeśli nie zastosowano dobrych praktyk, wstępne optymalizacje mogą polegać na zastosowaniu dobrych praktyk kodowania, takich jak przenoszenie obliczeń poza pętle.


1
Wydaje się, że kłócisz się z braku doświadczenia. Proponuję przeczytać artykuł Wadlera „Po co programować funkcjonalnie”. Poświęca znaczną część wyjaśniającą powód leniwej oceny (wskazówka: ma niewiele wspólnego z wydajnością, wczesną optymalizacją lub „ładowaniem danych, do których rzadko uzyskiwano dostęp”, a wszystko z modułowością).
Andres F.,

@AndresF Przeczytałem artykuł, do którego się odwołujesz. Zgadzam się z użyciem leniwej oceny w takich przypadkach. Wczesna ocena może nie być odpowiednia, ale argumentowałbym, że powrót sub-drzewa dla wybranego ruchu może przynieść znaczącą korzyść, jeśli można łatwo dodać dodatkowe ruchy. Jednak zbudowanie tej funkcjonalności może być przedwczesną optymalizacją. Poza programowaniem funkcjonalnym mam poważne problemy z używaniem leniwej oceny i nieużywaniem leniwej oceny. Istnieją doniesienia o znacznych kosztach wydajności wynikających z leniwej oceny w programowaniu funkcjonalnym.
BillThor

2
Jak na przykład? Istnieją również doniesienia o znacznych kosztach wydajności przy korzystaniu z szybkiej oceny (koszty w postaci albo niepotrzebnej oceny, jak i braku zakończenia programu). Pomyśl o tym są koszty związane z prawie każdą inną (niewłaściwie) używaną funkcją. Sama modułowość może kosztować; problemem jest to, czy warto.
Andres F.,

3

Jeśli potencjalnie będziemy musieli w pełni ocenić wyrażenie, aby określić jego wartość, leniwa ocena może być wadą. Załóżmy, że mamy długą listę wartości boolowskich i chcemy się dowiedzieć, czy wszystkie z nich są prawdziwe:

[True, True, True, ... False]

Aby to zrobić, musimy spojrzeć na każdy element na liście, bez względu na wszystko, więc nie ma możliwości leniwego odcięcia oceny. Możemy użyć fold, aby ustalić, czy wszystkie wartości boolowskie na liście są prawdziwe. Jeśli użyjemy opcji fold right, która korzysta z leniwej oceny, nie uzyskamy żadnych korzyści z leniwej oceny, ponieważ musimy spojrzeć na każdy element na liście:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Składanie w prawo będzie w tym przypadku znacznie wolniejsze niż ścisłe składanie w lewo, które nie korzysta z leniwej oceny:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

Powodem jest ścisłe złożenie po lewej stronie używa rekurencji ogona, co oznacza, że ​​gromadzi on wartość zwracaną i nie gromadzi i nie przechowuje w pamięci dużego łańcucha operacji. Jest to o wiele szybsze niż leniwe składanie w prawo, ponieważ obie funkcje i tak muszą przeglądać całą listę, a składanie w prawo nie może wykorzystywać rekurencji ogona. Chodzi o to, że powinieneś użyć wszystkiego, co jest najlepsze do danego zadania.


„Chodzi o to, że powinieneś użyć wszystkiego, co jest najlepsze do danego zadania”. +1
Giorgio
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.