Jak ważne są zaawansowane koncepcje Haskell, takie jak Monady i Funkcje aplikacyjne dla większości rutynowych zadań programistycznych?

16

Przeczytałem książkę Learn You a Haskell do momentu, w którym przedstawiają Monady i takie rzeczy jak Just a. Są dla mnie tak sprzeczne z intuicją, że po prostu mam ochotę się poddać, próbując się tego nauczyć.

Ale naprawdę chcę spróbować.

Zastanawiam się, czy mogę chociaż na jakiś czas uniknąć zaawansowanych koncepcji i po prostu zaczynam używać innych części języka do wykonywania wielu praktycznych zadań z bardziej podstawowymi częściami Haskell, tj. Funkcjami, standardowym IO, obsługą plików, interfejs do bazy danych i tym podobne.

Czy to rozsądne podejście do nauki korzystania z Haskell?

dan
źródło
2
Nie wiem jak daleko zajedziesz. Bez monad nie można zrobić nic przydatnego w Haskell, ponieważ robienie czegokolwiek przydatnego w programowaniu (np. Pisanie poważnego programu, z którego ktoś naprawdę chciałby skorzystać) wymaga We / Wy i stanu, z których oba można zarządzać tylko w Haskell poprzez użycie monad.
Mason Wheeler,
1
@dan - w końcu zrozumiałem monady Haskella po przeczytaniu dwóch dokumentów - „Mógłbyś wymyślić monady” i „Tackling the Niezgward Squad”. Nie mogę powiedzieć, czy są lepsze niż „Learn You a Haskell”, którego nie przeczytałem, ale działały dla mnie. Aby użyć notacji do z monadą IO, możesz sobie poradzić z bardzo małym zrozumieniem, czym są monady - zrobisz kilka rzeczy, które rozśmieszą lub płaczą ekspertów, ale przynajmniej powierzchownie nie różni się to zbytnio od używania konwencjonalny język imperatywny. Ale to jest warto się trochę głębsze zrozumienie.
Steve314,
2
@dan, zajęło mi prawie dekadę znudzenie się OOP i zacząłem doceniać / być ciekawym funkcjonalnych języków. Nadal będę uczył się F # i Clojure, zanim dam Haskellowi poważną szansę. Podczas gdy osobiste wyzwania są miłe, jest coś do powiedzenia na temat czucia jelit i ścieżki najmniejszego oporu. Wiele zależy od tego, kim jesteś. Jeśli jesteś typem bardzo matematycznym, prawdopodobnie od samego początku chciałbyś Haskell / Schem. Jeśli jesteś bardziej facetem „zrób to”, to też jest w porządku. Nie zbliżaj się do Haskella z postawą „zrób albo zgiń”. Ucz się innych rzeczy, a gdy będziesz się nudzić, wróć.
Job
Dzięki za radę @Job. Próbowałem już trochę Clojure, ale składnia Lisp naprawdę nie działa dla mnie, zarówno podczas czytania, jak i pisania. Uważam, że składnia Haskella jest bardziej naturalna. Tymczasem cieszę się, że używam Ruby do projektów, ale mam nadzieję, że zacznę robić praktyczne projekty w Haskell.
dan
@dan, Zabawne, uważam, że składnia Clojure jest raczej przyjazna, ale to prawdopodobnie dlatego, że wcześniej byłem narażony na Scheme. Może po F # przyzwyczaję się do sposobu, w jaki Haskell robi rzeczy.
Job

Odpowiedzi:

16

Najpierw rozróżnijmy uczenie się abstrakcyjnych pojęć i uczenie się ich konkretnych przykładów .

Nie zajdziesz daleko, ignorując wszystkie konkretne przykłady, z tego prostego powodu, że są one wszechobecne. W rzeczywistości abstrakcje istnieją w dużej części, ponieważ jednoczą rzeczy, które i tak byś robił z konkretnymi przykładami.

Same abstrakcje są z pewnością przydatne , ale nie są natychmiast potrzebne. Dość daleko można całkowicie zignorować abstrakcje i bezpośrednio użyć różnych typów. W końcu będziesz chciał je zrozumieć, ale zawsze możesz do nich wrócić później. W rzeczywistości mogę prawie zagwarantować, że jeśli to zrobisz, po powrocie do niego uderzysz się w czoło i zastanawiasz się, dlaczego spędziłeś tyle czasu na robieniu rzeczy zamiast korzystania z wygodnych narzędzi ogólnego zastosowania.

Weź Maybe ajako przykład. To tylko typ danych:

data Maybe a = Just a | Nothing

To wszystko samo z siebie; jest to wartość opcjonalna. Albo masz „tylko” coś w rodzaju a, albo nie masz nic. Załóżmy, że masz jakąś funkcję wyszukiwania, która powraca Maybe Stringdo reprezentowania szukania Stringwartości, która może nie być obecna. Więc dopasuj wzór do wartości, aby zobaczyć, która to jest:

case lookupFunc key of
    Just val -> ...
    Nothing  -> ...

To wszystko!

Naprawdę, nic więcej nie potrzebujesz. Żadnych Functors, Monadani nic innego. Wyrażają one typowe sposoby używania Maybe awartości ... ale są to tylko idiomy, „wzorce projektowe”, jakkolwiek chcesz to nazwać.

Jedynym miejscem, którego tak naprawdę nie można całkowicie uniknąć, jest IOtajemnicza czarna skrzynka, więc nie warto próbować zrozumieć, co to znaczy jako coś takiego Monad.

W rzeczywistości oto ściągawka na wszystko, o czym naprawdę musisz wiedzieć IOna razie:

  • Jeśli coś ma typ IO a, oznacza to, że jest to procedura , która coś robi i wyrzuca awartość.

  • Gdy masz blok kodu za pomocą donotacji, napisz coś takiego:

    do -- ...
       inp <- getLine
       -- etc...
    

    ... oznacza wykonanie procedury po prawej stronie <-i przypisanie wyniku do nazwy po lewej stronie.

  • Natomiast jeśli masz coś takiego:

    do -- ...
       let x = [foo, bar]
       -- etc...
    

    ... oznacza przypisanie wartości wyrażenia zwykłego (nie procedury) po prawej =stronie nazwy po lewej stronie.

  • Jeśli umieścisz tam coś bez przypisania wartości, na przykład:

    do putStrLn "blah blah, fishcakes"
    

    ... oznacza wykonanie procedury i zignorowanie wszystkiego, co zwróci. Niektóre procedury mają ten typ IO ()- ()typ jest rodzajem symbolu zastępczego, który nic nie mówi, więc oznacza to, że procedura coś robi i nie zwraca wartości. Coś jak voidfunkcja w innych językach.

  • Wykonanie tej samej procedury więcej niż raz może dać różne wyniki; taki jest pomysł. Dlatego nie ma możliwości „usunięcia” IOwartości, ponieważ coś w niej IOnie jest wartością, jest to procedura uzyskania wartości.

  • Ostatni wiersz w dobloku musi być prostą procedurą bez przypisania, gdzie wartość zwracana tej procedury staje się wartością zwracaną dla całego bloku. Jeśli chcesz, aby wartość zwracana używała już przypisanej wartości, returnfunkcja przyjmuje zwykłą wartość i daje ci procedurę braku działania, która zwraca tę wartość.

  • Poza tym nie ma w tym nic specjalnego IO; procedury te same w sobie są zwykłymi wartościami i można je przekazywać i łączyć na różne sposoby. Tylko wtedy, gdy są wykonywane w dobloku wywołanym przez maincoś, co robią.

A więc w czymś takim, jak ten całkowicie nudny, stereotypowy przykładowy program:

hello = do putStrLn "What's your name?"
           name <- getLine
           let msg = "Hi, " ++ name ++ "!"
           putStrLn msg
           return name

... możesz to odczytać jak imperatywny program. Definiujemy procedurę o nazwie hello. Po wykonaniu najpierw wykonuje procedurę drukowania wiadomości z pytaniem o twoje imię; następnie wykonuje procedurę, która odczytuje wiersz danych wejściowych i przypisuje wynik do name; następnie przypisuje wyrażenie do nazwy msg; następnie drukuje wiadomość; następnie zwraca nazwę użytkownika jako wynik całego bloku. Ponieważ namejest to String, oznacza helloto, że procedura zwraca a String, więc ma typ IO String. A teraz możesz wykonać tę procedurę w innym miejscu, tak jak to się dzieje getLine.

Pfff, monady. Kto ich potrzebuje?

CA McCann
źródło
2
@dan: Nie ma za co! Jeśli po drodze masz jakieś konkretne pytania, nie wahaj się zadać pytania na temat przepełnienia stosu. Tag [haskell] tam jest zazwyczaj dość aktywny, a jeśli wyjaśnisz, skąd pochodzisz, jesteś całkiem dobry w pomaganiu w zrozumieniu, a nie tylko w rzucaniu tajemniczych odpowiedzi.
CA McCann,
9

Jak ważne są zaawansowane koncepcje Haskell, takie jak Monady i Funkcje aplikacyjne dla większości rutynowych zadań programistycznych?

Są niezbędne. Bez nich zbyt łatwo jest używać Haskell jako kolejnego imperatywnego języka i chociaż Simon Peyton Jones nazwał go kiedyś Haskell, „najlepszym imperatywnym językiem programowania na świecie”, nadal brakuje ci najlepszej strony.

Monady, transformaty monad, monoidy, funktory, funktory aplikacyjne, funktory kontrastowe, strzałki, kategorie itp. Są wzorami projektowymi. Po dopasowaniu konkretnej rzeczy, którą tworzysz do jednej z nich, możesz czerpać z ogromnego bogactwa funkcji napisanych abstrakcyjnie. Te klasy nie są po prostu po to, by cię mylić, są po to, aby ułatwić ci życie i zwiększyć produktywność. Są one, moim zdaniem, największym powodem zwięzłości dobrego kodu Haskell.

dan_waterworth
źródło
1
+1: Dziękujemy za wskazanie, że „Monady, transformaty monad, monoidy, funktory, funktory aplikacyjne, funktory przeciwstawne, strzałki, kategorie itp. Są wzorami projektowymi”. !
Giorgio,
7

Czy jest to absolutnie konieczne, jeśli chcesz po prostu coś uruchomić? Nie.

Czy jest to konieczne, jeśli chcesz pisać piękne idiomatyczne programy Haskell? Absolutnie.

Programując w Haskell, warto pamiętać o dwóch rzeczach:

  1. System typów jest w stanie Ci pomóc. Jeśli skomponujesz swój program z wysoce polimorficznego kodu ciężkiego na typeclass, bardzo często możesz znaleźć się w cudownej sytuacji, w której rodzaj funkcji, którą chcesz, poprowadzi cię do (jednej!) Poprawnej implementacji .

  2. Różne dostępne biblioteki zostały opracowane zgodnie z zasadą nr 1.

Pisząc program w Haskell, zaczynam od pisania typów. Potem zaczynam od nowa i zapisuję kilka bardziej ogólnych typów (i jeśli mogą to być wystąpienia istniejących klas, tym lepiej). Następnie piszę niektóre funkcje, które dają mi wartości typów, które chcę. (Potem gram z nimi ghcii może napiszę kilka testów QuickCheck.) I na koniec przyklejam to wszystko razem main.

Można napisać brzydkiego Haskella, który po prostu coś działa. Ale kiedy osiągniesz ten etap, musisz się jeszcze wiele nauczyć.

Powodzenia!

Lambdageek
źródło
1
Dziękuję Ci! Wahałem się między Clojure i Haskellem, ale teraz pochylam się w kierunku Haskell z powodu tego, jak pomocni byli ludzie tacy jak ty.
dan
1
Społeczność Haskell wydaje się bardzo miła i pomocna. I wydaje się, że pochodzi stąd wiele interesujących pomysłów
Zachary K