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?
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?
Odpowiedzi:
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?
readFile
jest dokładnie tym , czego potrzebuję. Poza tym przejście z leniwej na chętną ocenę jest równie trywialne.
head [1 ..]
daje ci chętnie oceniany czysty język, ponieważ w Haskell daje 1
?
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)
set!
w leniwym tłumaczu Scheme. > :(
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ć.
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.
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.
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.