Czy korzyść z wzorca monad IO w radzeniu sobie z efektami ubocznymi ma charakter wyłącznie naukowy?

17

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?

Stu Cox
źródło
1
Powinieneś obejrzeć ten film . Cuda monad są wreszcie ujawnione bez uciekania się do teorii kategorii lub Haskella. Okazuje się, że monady są trywialnie wyrażone w JavaScript i są jednym z kluczowych czynników wspomagających Ajax. Monady są niesamowite. Są to proste rzeczy, prawie trywialnie realizowane, z ogromną mocą zarządzania złożonością. Ale ich zrozumienie jest zaskakująco trudne, a większość ludzi, kiedy mają już ten ah-ha moment, wydaje się tracić zdolność wyjaśniania ich innym.
Robert Harvey,
Dobre wideo, dzięki. Właściwie dowiedziałem się o tych rzeczach od wstępu JS do programowania funkcjonalnego (potem przeczytałem milion więcej…). Chociaż to obejrzałem, jestem całkiem pewien, że moje pytanie dotyczy monady IO, której Crock nie omawia w tym filmie.
Stu Cox,
Hmm ... Czy AJAX nie jest uważany za formę I / O?
Robert Harvey,
1
Zauważ, że typem mainprogramu Haskell jest IO ()- 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.
Wyzard
W twoim przykładzie część monadyczna ma miejsce, gdy weźmiesz wynik jednego obliczenia ( 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.
lortabac,

Odpowiedzi:

14

Zasadniczo cały program zostanie zawarty w monadzie IO.

W tym momencie myślę, że nie widzisz tego z perspektywy Haskellerów. Mamy więc taki program:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Myślę, że typowe podejście Haskellera byłoby takie convert, że czysta część:

  1. Jest prawdopodobnie większą częścią tego programu i jest o wiele bardziej skomplikowany niż IOczęści;
  2. Może być uzasadniony i przetestowany bez konieczności zajmowania się IOw ogóle.

Dlatego nie widzą w tym convert„zawartej” IO, ale raczej jako izolowanej od IO. Od tego typu, cokolwiek convertnie może nigdy nie zależeć od niczego, co dzieje się w IOakcji.

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?

Powiedziałbym, że dzieli się to na dwie rzeczy:

  1. Kiedy program jest uruchamiany, gdy wartość argumentu dla convertzależy od stanu pliku.
  2. Ale to, co robiconvert 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ć convertw 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ł, convertmusi poprawnie go rozpatrywać. A fakt, że czystość ogranicza to, co convertmożna zrobić z jej wkładem, upraszcza to rozumowanie.

Jeśli więc convertgeneruje nieprawidłowe wyniki z niektórych argumentów i readFilepodaje taki argument, nie uważamy tego za błąd wprowadzony przez stan . To błąd w czystej funkcji!

sacundim
źródło
Myślę, że to najlepszy opis (chociaż inni też pomogli mi wyjaśnić), dzięki.
Stu Cox,
czy warto zauważyć, że używanie monad w pythonie może mieć mniejszą zaletę, ponieważ python ma tylko jeden (statyczny) typ, a zatem nie daje żadnych gwarancji na nic?
jk.
7

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:

Zasadniczo cały program zostanie zawarty w monadzie IO.

To prawda, że mainfunkcja Haskell „żyje” IO, ale prawdziwe programy Haskell są zachęcane, aby nie używać, IOgdy nie jest to potrzebne. Prawie każda pisana funkcja, która nie musi wykonywać operacji wejścia / wyjścia, nie powinna mieć typu IO.

Tak więc powiedziałbym w ostatnim przykładzie, że masz to wstecz: mainjest nieczyste (ponieważ czyta i zapisuje pliki), ale podstawowe funkcje, takie jak, convertsą czyste.

Andres F.
źródło
3

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:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

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 namenarzuca 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.

Karl Bielefeldt
źródło
2

Moje (ograniczone) rozumienie programowania funkcjonalnego jest takie, że efekty stanu / uboczne powinny być zminimalizowane i oddzielone od logiki bezstanowej.

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()a write_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 dla read_file()i write_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()(powiedzmy read_config_file()). W Haskell, kiedy deklarujesz convert :: String -> Stringlub podobnie, bez IOmonady, 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ć w convertcelu 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_filewyprowadzić converti przekazać wyniki convert, zachowując czystość.)

Curt J. Sampson
źródło