Jako praktykujący, dlaczego miałbym przejmować się Haskellem? Co to jest monada i dlaczego jej potrzebuję? [Zamknięte]


9

Po prostu nie rozumiem, jaki problem rozwiązują.



2
Myślę, że ta edycja jest nieco ekstremalna. Myślę, że twoje pytanie było zasadniczo dobre. Tyle tylko, że niektóre jego fragmenty były trochę ... kłótliwe. Jest to prawdopodobnie wynik frustracji związanej z próbą nauczenia się czegoś, o czym nie miałeś pojęcia.
Jason Baker

@SnOrfus, to ja przekupiłem to pytanie. Byłem zbyt leniwy, aby poprawnie go edytować.
Job

Odpowiedzi:


34

Monady nie są ani dobre, ani złe. Po prostu są. Są to narzędzia służące do rozwiązywania problemów, takich jak wiele innych konstrukcji języków programowania. Jednym z ich bardzo ważnych zastosowań jest ułatwienie życia programistom pracującym w czysto funkcjonalnym języku. Ale są przydatne w językach niefunkcjonalnych; po prostu ludzie rzadko zdają sobie sprawę, że używają Monady.

Co to jest monada? Najlepszym sposobem myślenia o monadzie jest wzorzec projektowy. W przypadku I / O prawdopodobnie można by pomyśleć, że to niewiele więcej niż chwalebny rurociąg, w którym stan globalny przechodzi między etapami.

Weźmy na przykład kod, który piszesz:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

Tutaj dzieje się o wiele więcej niż na pierwszy rzut oka. Na przykład, można zauważyć, że putStrLnma następujący podpis: putStrLn :: String -> IO (). Dlaczego to?

Pomyśl o tym w ten sposób: udawajmy (dla uproszczenia), że stdout i stdin są jedynymi plikami, które możemy czytać i pisać. W imperatywnym języku nie stanowi to problemu. Ale w funkcjonalnym języku nie można mutować stanu globalnego. Funkcja to po prostu coś, co przyjmuje wartość (lub wartości) i zwraca wartość (lub wartości). Jednym ze sposobów obejścia tego jest użycie stanu globalnego jako wartości, która jest przekazywana do każdej funkcji. Więc możesz przetłumaczyć pierwszy wiersz kodu na coś takiego:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

... a kompilator wiedziałby, aby wydrukować wszystko, co zostało dodane do drugiego elementu global_state. Teraz nie wiem o tobie, ale nie chciałbym tak programować. Ułatwiono to za pomocą Monad. W Monadzie przekazujesz wartość, która reprezentuje pewien stan od jednej akcji do drugiej. Dlatego putStrLnma typ zwracany IO (): zwraca nowy stan globalny.

Więc dlaczego cię to obchodzi? Cóż, zalety programowania funkcjonalnego nad programem imperatywnym były dyskutowane na śmierć w kilku miejscach, więc nie zamierzam ogólnie odpowiadać na to pytanie (ale przeczytaj ten artykuł, jeśli chcesz usłyszeć argumenty dotyczące programowania funkcjonalnego). W tym konkretnym przypadku pomocne może być zrozumienie, co Haskell stara się osiągnąć.

Wielu programistów uważa, że ​​Haskell próbuje uniemożliwić im pisanie kodu imperatywnego lub stosowanie efektów ubocznych. To nie do końca prawda. Pomyśl o tym w ten sposób: język imperatywny to taki, który domyślnie dopuszcza skutki uboczne, ale pozwala ci pisać kod funkcjonalny, jeśli naprawdę chcesz (i chcesz poradzić sobie z niektórymi wymuszeniami). Haskell jest domyślnie czysto funkcjonalny, ale pozwala ci pisać kod imperatywny, jeśli naprawdę chcesz (co robisz, jeśli twój program ma być przydatny). Nie chodzi o to, aby utrudnić pisanie kodu, który ma skutki uboczne. Ma to na celu upewnienie się, że masz wyraźne skutki uboczne (wymuszając to systemem typów).


6
Ten ostatni akapit to złoto. Aby go nieco wyodrębnić i sparafrazować: „Język imperatywny to taki, który domyślnie dopuszcza skutki uboczne, ale pozwala na pisanie kodu funkcjonalnego, jeśli naprawdę tego chce. Język funkcjonalny jest domyślnie czysto funkcjonalny, ale umożliwia pisanie kodu imperatywnego jeśli naprawdę chcesz. ”
Frank Shearar

Warto zauważyć, że artykuł, do którego się odłączyłeś, odrzuca na początku ideę „niezmienności jako cnoty programowania funkcjonalnego”.
Mason Wheeler

@MasonWheeler: Czytam te akapity, nie odrzucając znaczenia niezmienności, ale odrzucając je jako przekonujący argument za wykazaniem wyższości programowania funkcjonalnego. W rzeczywistości mówi to samo o eliminacji goto(jako argumentu dla programowania strukturalnego) nieco później w artykule, charakteryzując takie argumenty jako „bezowocne”. A jednak nikt z nas potajemnie nie życzy sobie gotopowrotu. Po prostu nie można argumentować, że gotonie jest to konieczne dla osób, które intensywnie z niego korzystają.
Robert Harvey

7

Ugryzę!!! Same monady nie są tak naprawdę racją bytu dla Haskell (wczesne wersje Haskell nawet ich nie miały).

Twoje pytanie przypomina trochę „C ++, kiedy patrzę na składnię, nudzę się. Ale szablony są bardzo reklamowaną funkcją C ++, więc spojrzałem na implementację w innym języku”.

Ewolucja programisty Haskell to żart, nie należy go traktować poważnie.

Monada na potrzeby programu w Haskell jest instancją klasy typu Monad, to znaczy typem, który obsługuje pewien niewielki zestaw operacji. Haskell ma specjalne wsparcie dla typów, które implementują klasę typów Monad, w szczególności wsparcie syntaktyczne. Praktycznie skutkuje to tym, co nazywane jest „programowalnym średnikiem”. Kiedy połączysz tę funkcjonalność z niektórymi innymi funkcjami Haskell (funkcje pierwszej klasy, domyślnie lenistwo), uzyskasz możliwość implementacji pewnych rzeczy jako bibliotek, które tradycyjnie były uważane za funkcje językowe. Możesz na przykład wdrożyć mechanizm wyjątku. Możesz wdrożyć obsługę kontynuacji i coroutines jako bibliotekę. Haskell, język nie obsługuje zmiennych zmiennych:

Pytasz o „Monady Może / Identity / Safe Division ???”. Może monada jest przykładem tego, jak można zaimplementować (bardzo prosty, tylko jeden wyjątek) obsługę wyjątków jako bibliotekę.

Masz rację, pisanie wiadomości i czytanie danych wprowadzanych przez użytkowników nie jest wyjątkowe. IO jest kiepskim przykładem „monad jako cechy”.

Ale, aby powtórzyć, jedna „funkcja” sama w sobie (np. Monady) w oderwaniu od reszty języka niekoniecznie natychmiast wydaje się przydatna (świetna nowa funkcja C ++ 0x to odwołania do wartości, nie oznacza, że ​​możesz wziąć poza kontekstowym C ++, ponieważ jego składnia Cię nudzi i koniecznie widzisz narzędzie). Język programowania nie jest czymś, co dostajesz, rzucając kilka funkcji do wiadra.


W rzeczywistości haskell obsługuje zmienne zmienne za pośrednictwem monady ST (jedna z niewielu dziwnych, nieczystych, magicznych części języka, która gra według własnych zasad).
sara,

4

Wszyscy programiści piszą programy, ale na tym kończą się podobieństwa. Myślę, że programiści różnią się znacznie bardziej niż większość programistów może to sobie wyobrazić. Podejmij dowolną od dawna „bitwę”, taką jak typowanie zmiennych statycznych w porównaniu z typami wykonawczymi, skrypty w porównaniu z kompilacją, styl C w porównaniu z obiektami. Nie można racjonalnie argumentować, że jeden obóz jest gorszy, ponieważ niektóre z nich wywołują doskonały kod w jakimś systemie programowania, który wydaje mi się bezcelowy lub wręcz wręcz bezużyteczny.

Myślę, że różni ludzie po prostu myślą inaczej, a jeśli nie kusi cię cukier składniowy lub szczególnie abstrakcje, które istnieją tylko dla wygody i faktycznie mają znaczne koszty wykonania, to z całą pewnością trzymaj się z dala od takich języków.

Poleciłbym jednak przynajmniej spróbować zapoznać się z koncepcjami, które rezygnujesz. Nie mam nic przeciwko komuś, kto jest zdecydowanie pro-czysty-C, o ile faktycznie rozumieją, o co chodzi w wyrażeniach lambda. Podejrzewam, że większość nie od razu zostanie fanem, ale przynajmniej znajdzie się w ich głowach, gdy znajdą idealny problem, który byłby o wiele łatwiejszy do rozwiązania z lambdas.

A przede wszystkim staraj się nie denerwować mówieniem fanów, zwłaszcza przez ludzi, którzy tak naprawdę nie wiedzą, o czym mówią.


4

Haskell wymusza przejrzystość referencyjną : przy tych samych parametrach każda funkcja zawsze zwraca ten sam wynik, bez względu na to, ile razy wywołujesz tę funkcję.

Oznacza to na przykład, że w Haskell (i bez Monad) nie można zaimplementować generatora liczb losowych. W C ++ lub Javie możesz to zrobić za pomocą zmiennych globalnych, przechowując pośrednią wartość „seed” generatora losowego.

W Haskell odpowiednikiem zmiennych globalnych są Monady.


Więc ... co jeśli chcesz generator liczb losowych? Czy to też nie jest funkcja? Nawet jeśli nie, w jaki sposób mogę uzyskać generator liczb losowych?
Job

@Job Możesz stworzyć generator liczb losowych w monadzie (jest to w zasadzie narzędzie do śledzenia stanu) lub możesz użyć niebezpiecznegoPerformIO, diabła z Haskell, którego nigdy nie należy używać (i prawdopodobnie zepsuje Twój program, jeśli użyjesz losowości w środku!)
alternatywnie

W Haskell albo omijasz „RandGen”, który jest w zasadzie obecnym stanem RNG. Tak więc funkcja, która generuje nową liczbę losową, pobiera RandGen i zwraca krotkę z nowym RandGen i wyprodukowaną liczbą. Alternatywą jest podanie gdzieś listy liczb losowych od wartości minimalnej do maksymalnej. Zwróci to leniwie oceniany nieskończony strumień liczb, dzięki czemu możemy po prostu przejrzeć tę listę, ilekroć potrzebujemy nowej liczby losowej.
Qqwy,

W ten sam sposób, w jaki dostajesz je w innym języku! dostajesz pewien algorytm generowania liczb pseudolosowych, a następnie wysiewasz go pewną wartością i zbierasz wyskakujące „losowe” liczby! Jedyną różnicą jest to, że języki takie jak C # i Java automatycznie uruchamiają PRNG za pomocą zegara systemowego lub podobnych rzeczy. To i fakt, że w haskell otrzymujesz również nowy PRNG, którego możesz użyć, aby uzyskać „następny” numer, podczas gdy w C # / Java wszystko to odbywa się wewnętrznie przy użyciu zmiennych zmiennych w Randomobiekcie.
sara

4

To trochę stare pytanie, ale jest naprawdę dobre, więc odpowiem.

Możesz myśleć o monadach jak o blokach kodu, dla których masz pełną kontrolę nad tym, jak są wykonywane: co zwraca każdy wiersz kodu, czy wykonanie powinno się zatrzymać w dowolnym momencie, czy jakieś inne przetwarzanie powinno nastąpić między poszczególnymi wierszami.

Podam kilka przykładów rzeczy, które umożliwiają monady, które inaczej byłyby trudne. Żaden z tych przykładów nie jest w Haskell, tylko dlatego, że moja wiedza na temat Haskell jest nieco niepewna, ale wszystkie są przykładami tego, jak Haskell zainspirował użycie monad.

Parsery

Zwykle, jeśli chcesz napisać jakiś parser, powiedzmy zaimplementować język programowania, musiałbyś albo przeczytać specyfikację BNF i napisać całą masę pętlowego kodu, aby ją przeanalizować, lub musiałbyś użyć kompilatora kompilatora takich jak Flex, Bison, yacc itp. Ale dzięki monadom możesz stworzyć rodzaj „parsera kompilatora” bezpośrednio w Haskell.

Parserów nie da się tak naprawdę zrobić bez monad ani języków specjalnych, takich jak yacc, bizon itp.

Na przykład wziąłem specyfikację języka BNF dla protokołu IRC :

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

I zmniejszyłem go do około 40 linii kodu w F # (który jest innym językiem obsługującym monady):

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

Składnia monady F # jest dość brzydka w porównaniu do składni Haskella, i prawdopodobnie mógłbym to nieco poprawić - ale chodzi o to, że strukturalnie kod parsera jest identyczny z BNF. Nie tylko wymagałoby to dużo więcej pracy bez monad (lub generatora analizatora składni), ale prawie nie przypominało specyfikacji, a zatem było okropnie zarówno czytać, jak i utrzymywać.

Niestandardowa wielozadaniowość

Zwykle wielozadaniowość jest uważana za funkcję systemu operacyjnego - ale w przypadku monad można napisać własny harmonogram tak, aby po każdej monadzie instrukcji program przekazał kontrolę harmonogramowi, który następnie wybrałby inną monadę do wykonania.

Jeden facet stworzył monadę „zadaniową” do kontrolowania pętli gry (ponownie w F #), aby zamiast pisać wszystko jako maszynę stanu działającą przy każdym Update()wywołaniu, mógł po prostu napisać wszystkie instrukcje, jakby były pojedynczą funkcją .

Innymi słowy, zamiast robić coś takiego:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

Możesz zrobić coś takiego:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ do SQL

LINQ to SQL jest tak naprawdę przykładem monady, a podobną funkcjonalność można łatwo zaimplementować w Haskell.

Nie będę wdawał się w szczegóły, ponieważ nie pamiętam dokładnie tego wszystkiego, ale Erik Meijer wyjaśnia to całkiem dobrze .


1

Jeśli znasz wzory GoF, monady są podobne do wzorca dekoratora i wzorca budowniczego ułożone razem na sterydach, ugryzione przez radioaktywnego borsuka.

Powyżej są lepsze odpowiedzi, ale niektóre z konkretnych korzyści, które widzę, to:

  • monady ozdabiają niektóre typy rdzenia dodatkowymi właściwościami bez zmiany typu rdzenia. Na przykład monada może „podnieść” ciąg znaków i dodać wartości, takie jak „isWellFormed”, „isProfanity” lub „isPalindrome” itp.

  • podobnie monady pozwalają zlepiać prosty typ w typ kolekcji

  • monady umożliwiają późne wiązanie funkcji w tej przestrzeni wyższego rzędu

  • monady pozwalają mieszać dowolne funkcje i argumenty z dowolnym typem danych w przestrzeni wyższego rzędu

  • monady umożliwiają łączenie czystych, bezstanowych funkcji z nieczystą, stanową bazą, dzięki czemu możesz śledzić, na czym polega problem

Znanym przykładem monady w Javie jest List. Pobiera niektóre podstawowe klasy, takie jak String, i „przenosi” je do przestrzeni monad List, dodając informacje o liście. Następnie wiąże nowe funkcje w tym obszarze, takie jak get (), getFirst (), add (), empty () itp.

Na dużą skalę wyobraź sobie, że zamiast pisać program, właśnie napisałeś dużego Buildera (jako wzorzec GoF), a metoda build () na końcu wyrzuciła każdą odpowiedź, jaką program miał dać. I że możesz dodać nowe metody do ProgramBuilder bez ponownej kompilacji oryginalnego kodu. Właśnie dlatego monady są potężnym modelem projektowym.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.