Niedawno zacząłem uczyć się Haskell, ponieważ chciałem poszerzyć swoją wiedzę na temat programowania funkcjonalnego i muszę powiedzieć, że bardzo ją kocham. Zasób, którego obecnie używam, to kurs „Podstawy Haskella, część 1” na temat Pluralsight. Niestety mam pewne trudności ze zrozumieniem jednego konkretnego cytatu prowadzącego na temat następującego kodu i miałem nadzieję, że moglibyście rzucić nieco światła na ten temat.
Kod towarzyszący
helloWorld :: IO ()
helloWorld = putStrLn "Hello World"
main :: IO ()
main = do
helloWorld
helloWorld
helloWorld
Cytat
Jeśli masz tę samą akcję We / Wy wiele razy w bloku do, zostanie ona uruchomiona wiele razy. Tak więc ten program trzykrotnie wypisuje napis „Hello World”. Ten przykład pomaga zilustrować, że putStrLn
nie jest to funkcja z efektami ubocznymi. Wywołujemy tę putStrLn
funkcję raz, aby zdefiniować helloWorld
zmienną. Gdyby putStrLn
miał efekt uboczny drukowania łańcucha, wydrukowałby tylko raz, a helloWorld
zmienna powtórzona w głównym bloku do nie miałaby żadnego efektu.
W większości innych języków programowania taki program wypisuje „Hello World” tylko raz, ponieważ drukowanie nastąpiłoby po putStrLn
wywołaniu funkcji. To subtelne rozróżnienie często wprawia w osłupienie początkujących, więc zastanów się nad tym i upewnij się, że rozumiesz, dlaczego ten program drukuje „Hello World” trzy razy i dlaczego wydrukuje go tylko raz, jeśli putStrLn
funkcja wykona drukowanie jako efekt uboczny.
Czego nie rozumiem
Dla mnie wydaje się niemal naturalne, że napis „Hello World” jest drukowany trzy razy. Odbieram helloWorld
zmienną (lub funkcję?) Jako rodzaj wywołania zwrotnego, które jest wywoływane później. Nie rozumiem tylko, jak gdyby putStrLn
miał efekt uboczny, to spowodowałoby, że napis byłby wydrukowany tylko raz. Albo dlaczego zostanie wydrukowany tylko raz w innych językach programowania.
Powiedzmy, że w kodzie C # przypuszczam, że wyglądałoby to tak:
C # (skrzypce)
using System;
public class Program
{
public static void HelloWorld()
{
Console.WriteLine("Hello World");
}
public static void Main()
{
HelloWorld();
HelloWorld();
HelloWorld();
}
}
Jestem pewien, że przeoczam coś dość prostego lub źle interpretuję jego terminologię. Każda pomoc byłaby bardzo mile widziana.
EDYTOWAĆ:
Dziękuję wszystkim za odpowiedzi! Twoje odpowiedzi pomogły mi lepiej zrozumieć te pojęcia. Nie sądzę, aby kliknięcie zostało w pełni kliknięte, ale wrócę do tematu w przyszłości, dziękuję!
helloWorld
stałej wartości, takiej jak pole lub zmienna w języku C #. Nie stosuje się żadnego parametruhelloWorld
.putStrLn
nie ma skutków ubocznych; po prostu zwraca akcję IO, tę samą akcję IO dla argumentu"Hello World"
bez względu na to, ile razy wywołujeszputStrLn
.helloworld
nie byłaby to akcja drukowanaHello world
; to jest wartość zwrócony przezputStrLn
po to drukowanaHello World
(tj()
).helloWorld = Console.WriteLine("Hello World");
. Po prostu zawieraćConsole.WriteLine("Hello World");
wHelloWorld
funkcji mają być wykonane za każdym razemHelloWorld
jest wywoływany. Pomyśl teraz o tym, cohelloWorld = putStrLn "Hello World"
czynihelloWorld
. Zostaje przypisany do monady IO, która zawiera()
. Gdy go połączysz,>>=
dopiero wtedy wykona swoją aktywność (wydrukuje coś) i da ci()
po prawej stronie operatora powiązania.Odpowiedzi:
Prawdopodobnie łatwiej byłoby zrozumieć, co autor ma na myśli, jeśli zdefiniujemy
helloWorld
jako zmienną lokalną:który można porównać do tego pseudokodu podobnego do C #:
Tj. W C #
WriteLine
to procedura, która wypisuje swój argument i nic nie zwraca. W HaskellputStrLn
jest funkcją, która pobiera ciąg i daje akcję, która wydrukowałaby ten ciąg, gdyby był on wykonany. Oznacza to, że absolutnie nie ma różnicy między pisaniemi
Biorąc to pod uwagę, w tym przykładzie różnica nie jest szczególnie głęboka, więc dobrze jest, jeśli nie do końca rozumiesz, co autor próbuje uzyskać w tej sekcji i po prostu przejdź na teraz.
działa nieco lepiej, jeśli porównasz go do Pythona
Chodzi tutaj jest, że działania IO w Haskell są „prawdziwe” wartości, które nie muszą być opakowane w dalszych „callbacków” lub coś w tym rodzaju, aby zapobiec ich wykonywania - a jedynym sposobem, aby uczynić je wykonać IS umieścić je w określonym miejscu (tj. gdzieś w środku
main
lub pojawił się wątekmain
).To nie jest tylko sztuczka w salonie, ale w końcu ma to interesujący wpływ na sposób pisania kodu (na przykład jest to część powodu, dla którego Haskell tak naprawdę nie potrzebuje żadnej z typowych struktur kontrolnych, które znasz z imperatywnymi językami i zamiast tego mogę uciec od robienia wszystkiego pod względem funkcji), ale znowu nie martwiłbym się tym zbytnio (analogie takie jak te nie zawsze natychmiast klikają)
źródło
Może być łatwiej zobaczyć różnicę zgodnie z opisem, jeśli używasz funkcji, która faktycznie coś robi, zamiast
helloWorld
. Pomyśl o następujących kwestiach:Spowoduje to wydrukowanie „dodaję 2 i 3” 3 razy.
W języku C # możesz napisać:
Który wydrukuje się tylko raz.
źródło
Jeśli ocena
putStrLn "Hello World"
miała skutki uboczne, wówczas wiadomość zostanie wydrukowana tylko raz.Możemy przybliżyć ten scenariusz za pomocą następującego kodu:
unsafePerformIO
podejmujeIO
akcję i „zapomina”, jest toIO
akcja, odsuwająca ją od zwykłego sekwencjonowania narzuconego przez kompozycjęIO
akcji i pozwalająca, by efekt miał miejsce (lub nie) zgodnie z kaprysem leniwej oceny.evaluate
przyjmuje czystą wartość i zapewnia, że wartość jest oceniana za każdym razem, gdy oceniane jestIO
działanie wynikowe - co dla nas będzie, ponieważ leży ona na ścieżcemain
. Używamy go tutaj, aby połączyć ocenę niektórych wartości z działaniem programu.Ten kod drukuje „Hello World” tylko jeden raz. Traktujemy
helloWorld
jako czystą wartość. Ale to oznacza, że będzie dzielony między wszystkieevaluate helloWorld
połączenia. Czemu nie? W końcu to czysta wartość, po co niepotrzebnie ją przeliczać? Pierwszaevaluate
akcja „wyskakuje” efekt „ukryty”, a późniejsze akcje tylko oceniają wynik()
, co nie powoduje żadnych dalszych efektów.źródło
unsafePerformIO
na tym etapie nauki Haskell. Z jakiegoś powodu ma w nazwie „niebezpieczny” i nie powinieneś go używać, chyba że możesz (i zrobił) dokładnie rozważyć konsekwencje jego użycia w kontekście. Kod, który danidiaz podał w odpowiedzi, doskonale oddaje rodzaj nieintuicyjnego zachowania, które może wyniknąćunsafePerformIO
.Należy zwrócić uwagę na jeden szczegół: wywołujesz
putStrLn
funkcję tylko raz, podczas definiowaniahelloWorld
. Wmain
funkcjiputStrLn "Hello, World"
trzykrotnie używasz zwracanej wartości .Wykładowca mówi, że
putStrLn
połączenie nie ma skutków ubocznych i to prawda. Ale spójrz na rodzajhelloWorld
- jest to działanie IO.putStrLn
po prostu tworzy to dla Ciebie. Później łączysz 3 z nich za pomocądo
bloku, aby utworzyć kolejną akcję IO -main
. Później, kiedy wykonasz swój program, akcja zostanie uruchomiona, tam właśnie leżą efekty uboczne.Mechanizm, który leży u podstaw tego - monady . Ta potężna koncepcja pozwala korzystać z niektórych efektów ubocznych, takich jak drukowanie w języku, który nie obsługuje bezpośrednio efektów ubocznych. Po prostu połączysz niektóre akcje, a łańcuch ten zostanie uruchomiony na początku twojego programu. Musisz głęboko zrozumieć tę koncepcję, jeśli chcesz poważnie wykorzystywać Haskell.
źródło