Uważam, że skutki uboczne są zjawiskiem naturalnym. Ale jest to coś w rodzaju tabu w językach funkcjonalnych. Jakie są powody?
Moje pytanie jest specyficzne dla funkcjonalnego stylu programowania. Nie wszystkie języki programowania / paradygmaty.
Uważam, że skutki uboczne są zjawiskiem naturalnym. Ale jest to coś w rodzaju tabu w językach funkcjonalnych. Jakie są powody?
Moje pytanie jest specyficzne dla funkcjonalnego stylu programowania. Nie wszystkie języki programowania / paradygmaty.
Odpowiedzi:
Pisanie swoich funkcji / metod bez skutków ubocznych - więc są one czystymi funkcjami - ułatwia uzasadnienie poprawności twojego programu.
Ułatwia także komponowanie tych funkcji w celu tworzenia nowych zachowań.
Umożliwia to również pewne optymalizacje, w których kompilator może na przykład powtórzyć wyniki funkcji lub użyć funkcji Common Subexpression Elimination.
Edycja: na żądanie Benjola: Ponieważ duża część twojego stanu jest przechowywana na stosie (przepływ danych, a nie przepływ kontrolny, jak to tutaj nazwał Jonas ), możesz zrównoleglić lub inaczej zmienić kolejność wykonywania tych części obliczeń, które są niezależne od wzajemnie. Możesz łatwo znaleźć te niezależne części, ponieważ jedna część nie zapewnia danych wejściowych dla drugiej.
W środowiskach z debuggerami, które pozwalają wycofać stos i wznowić przetwarzanie (takie jak Smalltalk), posiadanie czystych funkcji oznacza, że bardzo łatwo można zobaczyć, jak zmienia się wartość, ponieważ poprzednie stany są dostępne do wglądu. W obliczeniach obciążonych mutacjami, chyba że wyraźnie dodasz akcje do / cofnij do swojej struktury lub algorytmu, nie zobaczysz historii obliczeń. (To wiąże się z pierwszym akapitem: pisanie czystych funkcji ułatwia sprawdzenie poprawności programu.)
Z artykułu o programowaniu funkcjonalnym :
W praktyce aplikacje muszą mieć pewne skutki uboczne. Simon Peyton-Jones, główny współpracownik funkcjonalnego języka programowania Haskell, powiedział: „Ostatecznie każdy program musi manipulować stanem. Program, który nie ma żadnych skutków ubocznych, jest rodzajem czarnej skrzynki. Wszystko, co możesz powiedzieć, to że pudełko robi się cieplejsze ”. ( http://oscon.blip.tv/file/324976 ) Kluczem jest ograniczenie skutków ubocznych, wyraźna ich identyfikacja i uniknięcie rozproszenia ich w całym kodzie.
Pomyliłeś się, programowanie funkcjonalne promuje ograniczanie skutków ubocznych, aby programy były łatwe do zrozumienia i optymalizacji. Nawet Haskell pozwala pisać do plików.
Zasadniczo to, co mówię, to to, że programiści funkcjonalni nie uważają, że skutki uboczne są złe, po prostu uważają, że ograniczenie stosowania efektów ubocznych jest dobre. Wiem, że to może wydawać się tak prostym rozróżnieniem, ale robi różnicę.
readFile
wykonują, to zdefiniowanie sekwencji działań. ta sekwencja jest funkcjonalnie czysta i przypomina trochę abstrakcyjne drzewo opisujące CO należy zrobić. środowisko wykonawcze przeprowadza następnie rzeczywiste brudne skutki uboczne.
Kilka uwag:
Funkcje bez efektów ubocznych mogą być trywialnie wykonywane równolegle, podczas gdy funkcje z efektami ubocznymi zwykle wymagają pewnego rodzaju synchronizacji.
Funkcje bez efektów ubocznych pozwalają na bardziej agresywną optymalizację (np. Przez przezroczyste użycie pamięci podręcznej wyników), ponieważ dopóki otrzymamy właściwy wynik, nie ma nawet znaczenia, czy funkcja została naprawdę wykonana
deterministic
klauzulę dla funkcji bez skutków ubocznych, więc nie są one wykonywane częściej niż to konieczne.
deterministic
Klauzula jest tylko słowo kluczowe, które informuje kompilator, że jest to funkcja deterministyczny, porównywalny do tego, jak final
kluczowe w Javie informuje kompilator, że zmienna nie może się zmienić.
Pracuję teraz przede wszystkim w kodzie funkcjonalnym i z tej perspektywy wydaje się to oślepiająco oczywiste. Skutki uboczne stanowią ogromne obciążenie psychiczne dla programistów próbujących czytać i rozumieć kod. Nie zauważysz tego obciążenia, dopóki nie uwolnisz się od niego przez jakiś czas, a potem nagle będziesz musiał ponownie przeczytać kod ze skutkami ubocznymi.
Rozważ ten prosty przykład:
val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.
// Code you are troubleshooting
// What's the expected value of foo here?
W funkcjonalnym języku wiem, że foo
to wciąż 42. Nie muszę nawet patrzeć na kod pomiędzy, a tym bardziej go rozumieć, ani na implementację wywoływanych funkcji.
Wszystkie te rzeczy na temat współbieżności, paralelizacji i optymalizacji są fajne, ale właśnie to informatycy umieścili w broszurze. Nie muszę się zastanawiać, kto mutuje Twoją zmienną i kiedy naprawdę lubię codzienną praktykę.
Niewiele języków uniemożliwia wywoływanie skutków ubocznych. Języki, które byłyby całkowicie wolne od skutków ubocznych, byłyby zbyt trudne (prawie niemożliwe) w użyciu, z wyjątkiem bardzo ograniczonej pojemności.
Dlaczego skutki uboczne są uważane za złe?
Ponieważ znacznie utrudniają uzasadnienie dokładnie tego, co robi program i udowodnienie, że robi to, czego się spodziewasz.
Na bardzo wysokim poziomie wyobraź sobie testowanie całej 3-poziomowej witryny z testami tylko w czarnej skrzynce. Jasne, jest to wykonalne, w zależności od skali. Ale z pewnością dzieje się dużo powielania. A jeśli nie jest to błąd (co jest związane z efektem ubocznym), a następnie można potencjalnie złamać cały system do dalszych badań, aż problem został zdiagnozowany i stałe, a poprawka jest wdrożony w środowisku testowym.
Korzyści
Teraz zmniejsz to. Gdybyś był dość dobry w pisaniu kodu wolnego od efektów ubocznych, o ile szybciej byłbyś w rozumowaniu tego, co zrobił jakiś istniejący kod? O ile szybciej mógłbyś pisać testy jednostkowe? Jak byś się czuł, przekonani, że kod bez żadnych skutków ubocznych była gwarantowana wolna od błędów, a użytkownicy mogą ograniczyć ekspozycję na wszelkie błędy go nie mają?
Jeśli kod nie ma skutków ubocznych, kompilator może również mieć dodatkowe optymalizacje, które mógłby wykonać. Implementacja tych optymalizacji może być znacznie łatwiejsza. Znacznie łatwiej jest nawet opracować koncepcję optymalizacji kodu wolnego od skutków ubocznych, co oznacza, że producent kompilatora może wdrożyć optymalizacje trudne w kodzie z efektami ubocznymi.
Współbieżność jest również znacznie prostsza we wdrażaniu, automatycznym generowaniu i optymalizacji, gdy kod nie ma skutków ubocznych. Jest tak, ponieważ wszystkie elementy można bezpiecznie ocenić w dowolnej kolejności. Umożliwienie programistom pisania wysoce współbieżnego kodu jest powszechnie uważane za kolejne duże wyzwanie, z którym musi zmierzyć się informatyka, i jedno z niewielu pozostałych zabezpieczeń przed prawem Moore'a .
Skutki uboczne są jak „wycieki” w kodzie, które będą musiały zostać usunięte później przez ciebie lub przez niczego niepodejrzewającego współpracownika.
Języki funkcjonalne omijają zmienne stanu i zmienne dane jako sposób na uczynienie kodu mniej zależnym od kontekstu i bardziej modułowym. Modułowość zapewnia, że praca jednego programisty nie wpłynie na pracę innego.
Skalowanie tempa rozwoju wraz z rozmiarem zespołu to dziś „święty graal” rozwoju oprogramowania. Podczas pracy z innymi programistami niewiele rzeczy jest tak samo ważnych jak modułowość. Nawet najprostsze logiczne skutki uboczne sprawiają, że współpraca jest niezwykle trudna.
Cóż, IMHO, to jest dość obłudne. Nikt nie lubi skutków ubocznych, ale wszyscy ich potrzebują.
To, co jest tak niebezpieczne w skutkach ubocznych, polega na tym, że wywołanie funkcji może mieć wpływ nie tylko na sposób, w jaki zachowuje się funkcja przy następnym wywołaniu, ale może mieć również wpływ na inne funkcje. Zatem skutki uboczne wprowadzają nieprzewidywalne zachowanie i nietrywialne zależności.
Zarówno paradygmaty programowania jak OO i funkcjonalne rozwiązują ten problem. OO zmniejsza problem poprzez nałożenie separacji obaw. Oznacza to, że stan aplikacji, który składa się z wielu zmiennych danych, jest enkapsulowany w obiekty, z których każdy jest odpowiedzialny tylko za utrzymanie własnego stanu. W ten sposób zmniejsza się ryzyko zależności, a problemy są znacznie bardziej odizolowane i łatwiejsze do śledzenia.
Programowanie funkcjonalne przyjmuje znacznie bardziej radykalne podejście, w którym stan aplikacji jest po prostu niezmienny z perspektywy programisty. To fajny pomysł, ale sam czyni język bezużytecznym. Dlaczego? Ponieważ DOWOLNA operacja We / Wy ma skutki uboczne. Natychmiast po odczytaniu dowolnego strumienia wejściowego stan aplikacji prawdopodobnie się zmieni, ponieważ przy następnym wywołaniu tej samej funkcji wynik może być inny. Być może odczytujesz różne dane lub - również możliwe - operacja może się nie powieść. To samo dotyczy danych wyjściowych. Nawet wyjście jest operacją z efektami ubocznymi. W dzisiejszych czasach nie zdajesz sobie z tego często sprawy, ale wyobraź sobie, że masz tylko 20 KB na wyjście, a jeśli je wypiszesz, aplikacja zawiesza się, ponieważ brakuje miejsca na dysku lub cokolwiek innego.
Więc tak, skutki uboczne są paskudne i niebezpieczne z perspektywy programisty. Większość błędów wynika ze sposobu, w jaki niektóre części stanu aplikacji są blokowane w prawie niejasny sposób, poprzez nierozważne i często niepotrzebne skutki uboczne. Z punktu widzenia użytkownika skutki uboczne są potrzebne do korzystania z komputera. Nie dbają o to, co dzieje się w środku i jak jest zorganizowane. Robią coś i oczekują, że komputer odpowiednio ZMIENI.
Każdy efekt uboczny wprowadza dodatkowe parametry wejściowe / wyjściowe, które należy wziąć pod uwagę podczas testowania.
To sprawia, że sprawdzanie poprawności kodu jest znacznie bardziej złożone, ponieważ środowisko nie może być ograniczone tylko do sprawdzania poprawności kodu, ale musi obejmować część lub całość otaczającego środowiska (globalny, który jest aktualizowany, mieszka tam w tym kodzie, co z kolei zależy od tego kod, który z kolei zależy od życia w pełnym serwerze Java EE ....)
Starając się unikać skutków ubocznych, ograniczasz ilość zewnętrznych potrzebnych do uruchomienia kodu.
Z mojego doświadczenia wynika, że dobry projekt w programowaniu obiektowym nakazuje korzystanie z funkcji, które mają skutki uboczne.
Na przykład weź podstawową aplikację komputerową interfejsu użytkownika. Mogę mieć działający program, który ma na swoim stosie graf obiektowy reprezentujący bieżący stan modelu domeny mojego programu. Wiadomości docierają do obiektów na tym wykresie (na przykład za pośrednictwem wywołań metod wywoływanych z kontrolera warstwy interfejsu użytkownika). Wykres obiektowy (model domeny) na stercie jest modyfikowany w odpowiedzi na komunikaty. Obserwatorzy modelu są informowani o wszelkich zmianach, interfejs użytkownika i być może inne zasoby są modyfikowane.
Dalekie od bycia złym, prawidłowe rozmieszczenie tych efektów ubocznych modyfikujących stos i modyfikujących ekran jest podstawą projektowania OO (w tym przypadku wzorca MVC).
Oczywiście nie oznacza to, że twoje metody powinny mieć arbitralne skutki uboczne. A funkcje wolne od skutków ubocznych mają miejsce w poprawie czytelności, a czasem i wydajności kodu.
Jak wskazano powyżej, języki funkcjonalne nie tyle zapobiegają skutkom ubocznym kodu, ale dostarczają narzędzi do zarządzania, jakie skutki uboczne mogą wystąpić w danym fragmencie kodu i kiedy.
To okazuje się mieć bardzo interesujące konsekwencje. Po pierwsze i najbardziej oczywiste, istnieje wiele rzeczy, które można zrobić z kodem wolnym od skutków ubocznych, które zostały już opisane. Ale są też inne rzeczy, które możemy zrobić, nawet podczas pracy z kodem, który ma skutki uboczne:
W złożonych bazach kodu najtrudniejszą rzeczą, o której myślę, jest złożona interakcja efektów ubocznych. Mogę mówić tylko osobiście, biorąc pod uwagę sposób działania mojego mózgu. Efekty uboczne i uporczywe stany oraz mutowanie danych wejściowych i tak dalej zmuszają mnie do myślenia o tym, „kiedy” i „gdzie” zdarzają się przyczyny prawidłowości, a nie tylko „co” dzieje się w każdej funkcji.
Nie mogę się skupić na „czym”. Po dokładnym przetestowaniu funkcji, która powoduje skutki uboczne, nie mogę stwierdzić, że spowoduje to zwiększenie niezawodności w całym kodzie przy jej użyciu, ponieważ osoby dzwoniące mogą nadal nadużywać, wywołując ją w niewłaściwym czasie, z niewłaściwego wątku, w niewłaściwym zamówienie. Tymczasem funkcja, która nie powoduje żadnych skutków ubocznych i po prostu zwraca nowy wynik na podstawie danych wejściowych (bez dotykania danych wejściowych), jest prawie niemożliwa do niewłaściwego użycia w ten sposób.
Ale myślę, że jestem typem pragmatycznym, a przynajmniej staram się być, i nie sądzę, że musimy koniecznie wyeliminować wszystkie skutki uboczne do jak najmniejszego minimum, aby uzasadnić poprawność naszego kodu (przynajmniej Bardzo trudno byłoby mi to zrobić w takich językach jak C). Bardzo trudno jest mi uzasadnić poprawność, gdy mamy kombinację złożonych przepływów kontrolnych i skutków ubocznych.
Skomplikowane przepływy kontroli do mnie to te, które mają charakter podobny do grafu, często rekurencyjne lub rekurencyjne (kolejki zdarzeń, np. Które nie wywołują bezpośrednio zdarzeń rekurencyjnych, ale mają charakter „rekurencyjny”), być może robią rzeczy w trakcie przemierzania rzeczywistej połączonej struktury grafu lub przetwarzania niejednorodnej kolejki zdarzeń, która zawiera eklektyczną mieszaninę zdarzeń do przetworzenia, prowadząc nas do wszelkiego rodzaju różnych części bazy kodu i wywołując różne skutki uboczne. Jeśli spróbujesz narysować wszystkie miejsca, w których ostatecznie znajdziesz się w kodzie, będzie to przypominało złożony wykres i potencjalnie z węzłami na wykresie, których nigdy się nie spodziewałbyś, byłyby tam w danym momencie, biorąc pod uwagę, że wszystkie one są wszystkie powodując skutki uboczne,
Języki funkcjonalne mogą mieć niezwykle złożone i rekurencyjne przepływy kontroli, ale wynik jest tak łatwy do zrozumienia pod względem poprawności, ponieważ nie występują przy tym różnego rodzaju eklektyczne skutki uboczne. Dopiero gdy złożone przepływy kontrolne napotykają eklektyczne skutki uboczne, uznaję to za bóle głowy, aby spróbować zrozumieć całość tego, co się dzieje i czy zawsze zrobi to dobrze.
Kiedy więc mam takie przypadki, często bardzo trudno mi, jeśli nie niemożliwie, czuć się bardzo pewnie co do poprawności takiego kodu, nie mówiąc już o bardzo pewności, że mogę wprowadzić zmiany w takim kodzie bez potknięcia się o coś nieoczekiwanego. Dla mnie rozwiązaniem jest albo uproszczenie przepływu kontroli, albo zminimalizowanie / ujednolicenie efektów ubocznych (przez ujednolicenie, mam na myśli, jak powodowanie tylko jednego rodzaju efektu ubocznego do wielu rzeczy podczas określonej fazy w systemie, a nie dwóch lub trzech lub jednego tuzin). Potrzebuję jednej z tych dwóch rzeczy, aby mój prosty mózg mógł mieć pewność co do poprawności istniejącego kodu i poprawności wprowadzanych przeze mnie zmian. Łatwo jest mieć pewność co do poprawności kodu wprowadzającego efekty uboczne, jeśli efekty uboczne są jednolite i proste wraz z przepływem kontrolnym, tak jak:
for each pixel in an image:
make it red
Łatwo jest uzasadnić poprawność takiego kodu, ale głównie dlatego, że skutki uboczne są tak jednolite, a kontrola jest tak prosta. Ale powiedzmy, że mieliśmy taki kod:
for each vertex to remove in a mesh:
start removing vertex from connected edges():
start removing connected edges from connected faces():
rebuild connected faces excluding edges to remove():
if face has less than 3 edges:
remove face
remove edge
remove vertex
Jest to absurdalnie uproszczony pseudokod, który zazwyczaj wymaga znacznie większej liczby funkcji i zagnieżdżonych pętli oraz znacznie więcej rzeczy, które musiałyby trwać (aktualizacja wielu map tekstur, masy kości, stanów selekcji itp.), Ale nawet pseudokod tak utrudnia powód poprawności z powodu interakcji złożonego, podobnego do grafu przepływu kontroli i trwających efektów ubocznych. Tak więc jedną strategią upraszczającą jest odroczenie przetwarzania i skupienie się na jednym rodzaju efektów ubocznych na raz:
for each vertex to remove:
mark connected edges
for each marked edge:
mark connected faces
for each marked face:
remove marked edges from face
if num_edges < 3:
remove face
for each marked edge:
remove edge
for each vertex to remove:
remove vertex
... coś w tym celu jako jedna iteracja uproszczenia. Oznacza to, że wielokrotnie przepuszczamy dane, co z pewnością wiąże się z kosztami obliczeniowymi, ale często okazuje się, że łatwiej jest wielowątkowić taki wynikowy kod, teraz, gdy efekty uboczne i przepływy kontrolne nabrały tej jednolitej i prostszej natury. Ponadto każdą pętlę można uczynić bardziej przyjazną dla pamięci podręcznej niż przechodzenie przez podłączony wykres i wywoływanie efektów ubocznych w miarę upływu czasu (np. Użyj równoległego zestawu bitów, aby zaznaczyć, co należy przejść, abyśmy mogli następnie wykonać odroczone przejścia w posortowanej kolejności sekwencyjnej używając masek bitowych i FFS). Ale co najważniejsze, uważam drugą wersję za o wiele łatwiejszą do uzasadnienia pod względem poprawności, a także zmiany bez powodowania błędów. Po to aby'
W końcu potrzebujemy efektów ubocznych, w przeciwnym razie mielibyśmy po prostu funkcje, które wysyłają dane bez celu. Często musimy nagrać coś do pliku, wyświetlić coś na ekranie, przesłać dane przez gniazdo, coś w tym rodzaju, a wszystkie te rzeczy są efektami ubocznymi. Ale możemy zdecydowanie zmniejszyć liczbę zbędnych efektów ubocznych, które się utrzymują, a także zmniejszyć liczbę efektów ubocznych, które występują, gdy przepływy kontrolne są bardzo skomplikowane, i myślę, że znacznie łatwiej byłoby uniknąć błędów, gdybyśmy tak zrobili.
To nie jest złe. Moim zdaniem konieczne jest rozróżnienie dwóch typów funkcji - z efektami ubocznymi i bez. Funkcja bez skutków ubocznych: - zwraca zawsze to samo z tymi samymi argumentami, więc na przykład taka funkcja bez żadnych argumentów nie ma sensu. - Oznacza to również, że kolejność, w jakiej nazywane są niektóre takie funkcje, nie odgrywa żadnej roli - musi być w stanie działać i może być debugowana tylko sama (!), Bez żadnego innego kodu. A teraz lol, zobacz, co robi JUnit. Funkcja z efektami ubocznymi: - ma coś w rodzaju „przecieków”, które można automatycznie zaznaczyć - jest to bardzo ważne przez debugowanie i wyszukiwanie błędów, które generalnie są powodowane przez skutki uboczne. - Każda funkcja z efektami ubocznymi ma również swoją „część” bez skutków ubocznych, co można również rozdzielić automatycznie. Więc zło to te skutki uboczne,