Zrozumienie czystych funkcji i skutków ubocznych w Haskell - putStrLn

10

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 putStrLnnie jest to funkcja z efektami ubocznymi. Wywołujemy tę putStrLnfunkcję raz, aby zdefiniować helloWorldzmienną. Gdyby putStrLnmiał efekt uboczny drukowania łańcucha, wydrukowałby tylko raz, a helloWorldzmienna 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 putStrLnwywoł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 putStrLnfunkcja wykona drukowanie jako efekt uboczny.

Czego nie rozumiem

Dla mnie wydaje się niemal naturalne, że napis „Hello World” jest drukowany trzy razy. Odbieram helloWorldzmienną (lub funkcję?) Jako rodzaj wywołania zwrotnego, które jest wywoływane później. Nie rozumiem tylko, jak gdyby putStrLnmiał 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ę!

Fluous
źródło
2
Pomyśl o helloWorldstałej wartości, takiej jak pole lub zmienna w języku C #. Nie stosuje się żadnego parametru helloWorld.
Caramiriel
2
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łujesz putStrLn.
chepner
1
Gdyby tak było, helloworldnie byłaby to akcja drukowana Hello world; to jest wartość zwrócony przez putStrLn po to drukowana Hello World(tj ()).
chepner
2
Myślę, że aby zrozumieć ten przykład, musisz już zrozumieć, jak działają działania niepożądane w Haskell. To nie jest dobry przykład.
user253751,
W swoim fragmencie C # nie lubisz helloWorld = Console.WriteLine("Hello World");. Po prostu zawierać Console.WriteLine("Hello World");w HelloWorldfunkcji mają być wykonane za każdym razem HelloWorldjest wywoływany. Pomyśl teraz o tym, co helloWorld = putStrLn "Hello World"czyni helloWorld. 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.
Redu,

Odpowiedzi:

8

Prawdopodobnie łatwiej byłoby zrozumieć, co autor ma na myśli, jeśli zdefiniujemy helloWorldjako zmienną lokalną:

main :: IO ()
main = do
  let helloWorld = putStrLn "Hello World!"
  helloWorld
  helloWorld
  helloWorld

który można porównać do tego pseudokodu podobnego do C #:

void Main() {
  var helloWorld = {
    WriteLine("Hello World!")
  }
  helloWorld;
  helloWorld;
  helloWorld;
}

Tj. W C # WriteLineto procedura, która wypisuje swój argument i nic nie zwraca. W Haskell putStrLnjest 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 pisaniem

do
  let hello = putStrLn "Hello World"
  hello
  hello

i

do
  putStrLn "Hello World"
  putStrLn "Hello World"

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

hello_world = print('hello world')
hello_world
hello_world
hello_world

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 mainlub pojawił się wątek main).

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ą)

Sześcienny
źródło
4

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:

add :: Int -> Int -> IO Int
add x y = do
  putStrLn ("I am adding " ++ show x ++ " and " ++ show y)
  return (x + y)

plus23 :: IO Int
plus23 = add 2 3

main :: IO ()
main = do
  _ <- plus23
  _ <- plus23
  _ <- plus23
  return ()

Spowoduje to wydrukowanie „dodaję 2 i 3” 3 razy.

W języku C # możesz napisać:

using System;

public class Program
{
    public static int add(int x, int y)
    {
        Console.WriteLine("I am adding {0} and {1}", x, y);
        return x + y;
    }

    public static void Main()
    {
        int x;
        int plus23 = add(2, 3);
        x = plus23;
        x = plus23;
        x = plus23;
        return;
    }
}

Który wydrukuje się tylko raz.

oisdk
źródło
3

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:

import System.IO.Unsafe (unsafePerformIO)
import Control.Exception (evaluate)

helloWorld :: ()
helloWorld = unsafePerformIO $ putStrLn "Hello World"

main :: IO ()
main = do
    evaluate helloWorld
    evaluate helloWorld
    evaluate helloWorld

unsafePerformIOpodejmuje IOakcję i „zapomina”, jest to IOakcja, odsuwająca ją od zwykłego sekwencjonowania narzuconego przez kompozycję IOakcji i pozwalająca, by efekt miał miejsce (lub nie) zgodnie z kaprysem leniwej oceny.

evaluateprzyjmuje czystą wartość i zapewnia, że ​​wartość jest oceniana za każdym razem, gdy oceniane jest IOdział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 helloWorldjako czystą wartość. Ale to oznacza, że ​​będzie dzielony między wszystkie evaluate helloWorldpołączenia. Czemu nie? W końcu to czysta wartość, po co niepotrzebnie ją przeliczać? Pierwsza evaluateakcja „wyskakuje” efekt „ukryty”, a późniejsze akcje tylko oceniają wynik (), co nie powoduje żadnych dalszych efektów.

danidiaz
źródło
1
Warto zauważyć, że absolutnie nie powinieneś używać unsafePerformIOna 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.
Andrew Ray
1

Należy zwrócić uwagę na jeden szczegół: wywołujesz putStrLnfunkcję tylko raz, podczas definiowania helloWorld. W mainfunkcji putStrLn "Hello, World"trzykrotnie używasz zwracanej wartości .

Wykładowca mówi, że putStrLnpołączenie nie ma skutków ubocznych i to prawda. Ale spójrz na rodzaj helloWorld- jest to działanie IO. putStrLnpo prostu tworzy to dla Ciebie. Później łączysz 3 z nich za pomocą dobloku, 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.

Jurij Kowalalenko
źródło