Przepraszam za jeszcze jedno pytanie o efekty uboczne FP +, ale nie mogłem znaleźć już istniejącego, który całkiem mi na to odpowiedział.
Moje (ograniczone) rozumienie programowania funkcjonalnego jest takie, że efekty stanu / uboczne powinny być zminimalizowane i oddzielone od logiki bezstanowej.
Zbieram również podejście Haskella do tego, monady IO, osiąga to poprzez zawijanie stanowych akcji w kontenerze, do późniejszego wykonania, rozważanego poza zakresem samego programu.
Próbuję zrozumieć ten wzorzec, ale tak naprawdę ustalić, czy użyć go w projekcie Python, więc jeśli to możliwe, unikaj specyfiki Haskell.
Nadchodzi surowy przykład.
Jeśli mój program konwertuje plik XML na plik JSON:
def main():
xml_data = read_file('input.xml') # impure
json_data = convert(xml_data) # pure
write_file('output.json', json_data) # impure
Czy podejście Monady IO nie jest skuteczne, aby to zrobić:
steps = list(
read_file,
convert,
write_file,
)
następnie zwolnij się z odpowiedzialności, nie dzwoniąc tych kroków, ale pozwalając tłumaczowi to zrobić?
Lub inaczej: pisanie:
def main(): # pure
def inner(): # impure
xml_data = read_file('input.xml')
json_data = convert(xml_data)
write_file('output.json', json_data)
return inner
następnie oczekuję, że ktoś zadzwoni inner()
i powie, że twoja praca jest skończona, ponieważmain()
jest czysta.
Zasadniczo cały program zostanie zawarty w monadzie IO.
Kiedy kod jest faktycznie wykonywany , wszystko po odczytaniu pliku zależy od stanu tego pliku, więc nadal będą cierpieć z powodu tych samych błędów związanych ze stanem, co implementacja imperatywna, więc czy rzeczywiście zyskałeś coś, jako programista, który to utrzyma?
Całkowicie doceniam korzyści płynące z ograniczenia i izolowania zachowań stanowych, dlatego właśnie stworzyłem taką wersję imperatywną: zbieraj dane wejściowe, rób czyste rzeczy, wypluwaj dane wyjściowe. Mamy nadzieję, że convert()
może być całkowicie czysty i czerpać korzyści z możliwości buforowania, bezpieczeństwa wątków itp.
Doceniam również to, że typy monadyczne mogą być użyteczne, szczególnie w potokach działających na porównywalnych typach, ale nie rozumiem, dlaczego IO powinno używać monad, chyba że jest już w takim potoku.
Czy jest jakaś dodatkowa korzyść z radzenia sobie z efektami ubocznymi, które przynosi wzorzec monady IO, którego mi brakuje?
main
programu Haskell jestIO ()
- działanie IO. To wcale nie jest funkcja; to jest wartość . Cały twój program jest czystą wartością zawierającą instrukcje, które informują środowisko wykonawcze języka, co powinien zrobić. Wszystkie nieczyste rzeczy (faktycznie wykonujące operacje IO) są poza zakresem twojego programu.read_file
) i użyjesz go jako argumentu do następnego (write_file
). Gdybyś miał tylko sekwencję niezależnych działań, nie potrzebowałbyś Monady.Odpowiedzi:
W tym momencie myślę, że nie widzisz tego z perspektywy Haskellerów. Mamy więc taki program:
Myślę, że typowe podejście Haskellera byłoby takie
convert
, że czysta część:IO
części;IO
w ogóle.Dlatego nie widzą w tym
convert
„zawartej”IO
, ale raczej jako izolowanej odIO
. Od tego typu, cokolwiekconvert
nie może nigdy nie zależeć od niczego, co dzieje się wIO
akcji.Powiedziałbym, że dzieli się to na dwie rzeczy:
convert
zależy od stanu pliku.convert
funkcja , nie zależy od stanu pliku. jest zawsze tą samą funkcją , nawet jeśli jest wywoływana z różnymi argumentami w różnych punktach.convert
Jest to nieco abstrakcyjny punkt, ale naprawdę jest kluczem do tego, co Haskellers mają na myśli, kiedy o tym mówią. Chcesz pisać
convert
w taki sposób, że podając dowolny poprawny argument, wygeneruje poprawny wynik dla tego argumentu. Patrząc na to w ten sposób, fakt, że odczytanie pliku jest operacją stanową, nie wchodzi w zakres równania; liczy się tylko to, że niezależnie od tego, jaki argument zostanie mu przekazany i skądkolwiek pochodził,convert
musi poprawnie go rozpatrywać. A fakt, że czystość ogranicza to, coconvert
można zrobić z jej wkładem, upraszcza to rozumowanie.Jeśli więc
convert
generuje nieprawidłowe wyniki z niektórych argumentów ireadFile
podaje taki argument, nie uważamy tego za błąd wprowadzony przez stan . To błąd w czystej funkcji!źródło
Trudno jest dokładnie ustalić, co rozumiesz przez „czysto akademicki”, ale myślę, że odpowiedź brzmi „nie”.
Jak wyjaśniono w Tackling the Awkward Squad autorstwa Simona Peytona Jonesa ( zdecydowanie zalecane czytanie!), Monadyczne We / Wy miało rozwiązać prawdziwe problemy ze sposobem, w jaki Haskell obsługiwał We / Wy. Przeczytaj przykład serwera z żądaniami i odpowiedziami, którego tutaj nie skopiuję; to bardzo pouczające.
Haskell, w przeciwieństwie do Pythona, promuje styl „czystych” obliczeń, które mogą być wymuszone przez jego system typów. Oczywiście możesz zastosować samodyscyplinę podczas programowania w Pythonie, aby zachować zgodność z tym stylem, ale co z modułami, których nie napisałeś? Bez dużej pomocy ze strony systemu typów (i popularnych bibliotek) monadyczne We / Wy jest prawdopodobnie mniej przydatne w Pythonie. Filozofia języka nie ma na celu wymuszania ścisłej separacji od czystej i nieczystości.
Zauważ, że mówi to więcej o różnych filozofiach Haskella i Pythona niż o tym, jak akademickim monadycznym We / Wy jest. Nie użyłbym tego do Pythona.
Jeszcze jedna rzecz. Mówisz:
To prawda, że
main
funkcja Haskell „żyje”IO
, ale prawdziwe programy Haskell są zachęcane, aby nie używać,IO
gdy nie jest to potrzebne. Prawie każda pisana funkcja, która nie musi wykonywać operacji wejścia / wyjścia, nie powinna mieć typuIO
.Tak więc powiedziałbym w ostatnim przykładzie, że masz to wstecz:
main
jest nieczyste (ponieważ czyta i zapisuje pliki), ale podstawowe funkcje, takie jak,convert
są czyste.źródło
Dlaczego IO jest nieczyste? Ponieważ może zwracać różne wartości w różnych momentach. Istnieje zależność od czasu, którą należy uwzględnić w ten czy inny sposób. Jest to tym bardziej istotne w przypadku leniwej oceny. Rozważ następujący program:
Bez monady we / wy, dlaczego pierwszy monit miałby kiedykolwiek otrzymać wyjście? Nie ma w tym nic, więc leniwa ocena oznacza, że nigdy nie będzie wymagana. Nie ma też nic, co mogłoby zmusić do wyświetlenia przed odczytem danych wejściowych. Jeśli chodzi o komputer, bez monady we / wy, te dwa pierwsze wyrażenia są całkowicie od siebie niezależne. Na szczęście
name
narzuca dwa kolejne.Istnieją inne sposoby rozwiązania problemu zależności od zamówienia, ale użycie monady we / wy jest prawdopodobnie najprostszym sposobem (przynajmniej z punktu widzenia języka), aby pozwolić wszystkim pozostać w leniwej sferze funkcjonalnej, bez małych sekcji imperatywnego kodu. Jest również najbardziej elastyczny. Na przykład można stosunkowo łatwo zbudować potok we / wy dynamicznie w czasie wykonywania na podstawie danych wprowadzonych przez użytkownika.
źródło
To nie tylko programowanie funkcjonalne; to zwykle dobry pomysł w każdym języku. Jeśli zrobić testów jednostkowych, sposób rozpadł
read_file()
,convert()
awrite_file()
przychodzi naturalnie, ponieważ doskonale, mimoconvert()
bycia zdecydowanie najbardziej złożonym i największej części kodu, pisanie testów dla niego jest stosunkowo proste: wystarczy założyć to parametr wejściowy . Pisanie testów dlaread_file()
iwrite_file()
jest nieco trudniejsze (nawet jeśli same funkcje są prawie trywialne), ponieważ musisz tworzyć i / lub czytać rzeczy w systemie plików przed i po wywołaniu funkcji. Idealnie byłoby, gdyby takie funkcje były tak proste, że nie będziesz ich testować, a tym samym zaoszczędzisz sobie wiele kłopotów.Różnica między Pythonem a Haskell polega na tym, że Haskell ma moduł sprawdzania typu, który może udowodnić, że funkcje nie mają skutków ubocznych. W Pythonie musisz mieć nadzieję, że nikt przypadkowo nie upuścił funkcji odczytu lub zapisu plików do
convert()
(powiedzmyread_config_file()
). W Haskell, kiedy deklarujeszconvert :: String -> String
lub podobnie, bezIO
monady, moduł sprawdzania typu zagwarantuje, że jest to czysta funkcja, która opiera się tylko na parametrze wejściowym i niczym innym. Jeśli ktoś spróbuje zmodyfikować wconvert
celu odczytania pliku konfiguracyjnego, szybko zobaczy błędy kompilatora wskazujące, że naruszałoby czystość funkcji. (Mam nadzieję, że byliby na tyle rozsądni, aby sięread_config_file
wyprowadzićconvert
i przekazać wynikiconvert
, zachowując czystość.)źródło