Po prostu nie rozumiem, jaki problem rozwiązują.
Po prostu nie rozumiem, jaki problem rozwiązują.
Odpowiedzi:
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 putStrLn
ma 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 putStrLn
ma 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).
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 goto
powrotu. Po prostu nie można argumentować, że goto
nie jest to konieczne dla osób, które intensywnie z niego korzystają.
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.
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ą.
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.
Random
obiekcie.
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 .
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.