Reine Funktionen und Nebenwirkungen in Haskell verstehen - putStrLn

10

Vor kurzem habe ich angefangen, Haskell zu lernen, weil ich mein Wissen über funktionale Programmierung erweitern wollte und ich muss sagen, dass ich es bis jetzt wirklich liebe. Die Ressource, die ich derzeit verwende, ist der Kurs 'Haskell Fundamentals Part 1' über Pluralsight. Leider habe ich einige Schwierigkeiten, ein bestimmtes Zitat des Dozenten über den folgenden Code zu verstehen, und ich hatte gehofft, dass Sie etwas Licht in das Thema bringen können.

Begleitcode

helloWorld :: IO ()
helloWorld = putStrLn "Hello World"

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

Das Zitat

Wenn Sie dieselbe E / A-Aktion mehrmals in einem Do-Block haben, wird sie mehrmals ausgeführt. Dieses Programm druckt also die Zeichenfolge 'Hello World' dreimal aus. Dieses Beispiel zeigt, dass putStrLnes sich nicht um eine Funktion mit Nebenwirkungen handelt. Wir rufen die putStrLnFunktion einmal auf, um die helloWorldVariable zu definieren . Wenn putStrLndas Drucken der Zeichenfolge einen Nebeneffekt hätte, würde sie nur einmal gedruckt, und die helloWorldim Haupt-Do-Block wiederholte Variable hätte keine Auswirkung.

In den meisten anderen Programmiersprachen würde ein Programm wie dieses 'Hello World' nur einmal drucken, da das Drucken beim Aufrufen der putStrLnFunktion erfolgen würde . Diese subtile Unterscheidung stolpert oft über Anfänger. Denken Sie also ein wenig darüber nach und stellen Sie sicher, dass Sie verstehen, warum dieses Programm 'Hello World' dreimal druckt und warum es nur einmal druckt, wenn die putStrLnFunktion den Druck als Nebeneffekt ausführt.

Was ich nicht verstehe

Für mich ist es fast selbstverständlich, dass die Zeichenfolge 'Hello World' dreimal gedruckt wird. Ich nehme die helloWorldVariable (oder Funktion?) Als eine Art Rückruf wahr, der später aufgerufen wird. Was ich nicht verstehe ist, wie wenn putStrLnein Nebeneffekt dazu führen würde, dass die Zeichenfolge nur einmal gedruckt wird. Oder warum es nur einmal in anderen Programmiersprachen gedruckt wird.

Nehmen wir im C # -Code an, ich würde annehmen, dass es so aussehen würde:

C # (Geige)

using System;

public class Program
{
    public static void HelloWorld()
    {
        Console.WriteLine("Hello World");
    }

    public static void Main()
    {
        HelloWorld();
        HelloWorld();
        HelloWorld();
    }
}

Ich bin sicher, ich übersehen etwas ganz Einfaches oder interpretiere seine Terminologie falsch. Jede Hilfe wäre sehr dankbar.

BEARBEITEN:

Vielen Dank für Ihre Antworten! Ihre Antworten haben mir geholfen, diese Konzepte besser zu verstehen. Ich denke, es hat noch nicht vollständig geklickt, aber ich werde das Thema in Zukunft noch einmal aufgreifen, danke!

Fließend
quelle
2
Stellen Sie sich helloWorldeine Konstante wie ein Feld oder eine Variable in C # vor. Es gibt keinen Parameter, auf den angewendet wird helloWorld.
Caramiriel
2
putStrLn hat keine Nebenwirkung; Es wird einfach eine E / A-Aktion zurückgegeben, dieselbe E / A-Aktion für das Argument, "Hello World"unabhängig davon, wie oft Sie aufrufen putStrLn.
Chepner
1
Wenn dies helloworldder Fall wäre , wäre dies keine Aktion, die gedruckt wird Hello world. es würde durch den Wert zurückgeführt werden , putStrLn nachdem es gedruckt Hello World(nämlich ()).
Chepner
2
Ich denke, um dieses Beispiel zu verstehen, müsste man bereits verstehen, wie Nebenwirkungen in Haskell funktionieren. Es ist kein gutes Beispiel.
user253751
In Ihrem C # -Schnipsel gefällt es Ihnen nicht helloWorld = Console.WriteLine("Hello World");. Sie enthalten nur Console.WriteLine("Hello World");die HelloWorldFunktion, die bei jedem HelloWorldAufruf ausgeführt werden soll. Denken Sie jetzt darüber nach, was helloWorld = putStrLn "Hello World"macht helloWorld. Es wird einer E / A-Monade zugewiesen, die enthält (). Sobald Sie es daran gebunden haben, führt >>=es erst dann seine Aktivität aus (etwas drucken) und zeigt Sie ()auf der rechten Seite des Bindungsoperators an.
Reduzieren Sie den

Antworten:

8

Es wäre wahrscheinlich einfacher zu verstehen, was der Autor bedeutet, wenn wir helloWorldals lokale Variable definieren:

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

was Sie mit diesem C # -ähnlichen Pseudocode vergleichen könnten:

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

Dh in C # WriteLineist eine Prozedur, die ihr Argument druckt und nichts zurückgibt. In Haskell putStrLnist dies eine Funktion, die eine Zeichenfolge verwendet und Ihnen eine Aktion gibt, die diese Zeichenfolge druckt, wenn sie ausgeführt wird. Es bedeutet, dass es absolut keinen Unterschied zwischen dem Schreiben gibt

do
  let hello = putStrLn "Hello World"
  hello
  hello

und

do
  putStrLn "Hello World"
  putStrLn "Hello World"

Abgesehen davon ist der Unterschied in diesem Beispiel nicht besonders tiefgreifend. Es ist also in Ordnung, wenn Sie nicht genau verstehen, worauf der Autor in diesem Abschnitt abzielt, und erst einmal weitermachen.

Es funktioniert ein bisschen besser, wenn Sie es mit Python vergleichen

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

Der Punkt hier ist , dass IO Aktionen in Haskell sind „echte“ Werte , die in weiteren „Callbacks“ oder etwas dergleichen eingewickelt nicht brauchen werden , um sie daran zu hindern, die Ausführung - besser gesagt, der einzige Weg zu tun , sie zu bekommen , ist auszuführen , um sie an einem bestimmten Ort zu platzieren (dh irgendwo drinnen mainoder ein Faden ist entstanden main).

Dies ist nicht nur ein Salon-Trick, sondern hat auch einige interessante Auswirkungen auf das Schreiben von Code (zum Beispiel ist dies ein Teil des Grundes, warum Haskell keine der üblichen Kontrollstrukturen benötigt, die Sie kennen mit aus imperativen Sprachen und kann damit davonkommen, stattdessen alles in Bezug auf Funktionen zu tun), aber auch hier würde ich mir keine Sorgen machen (Analogien wie diese klicken nicht immer sofort)

Kubisch
quelle
4

Es ist möglicherweise einfacher, den beschriebenen Unterschied zu erkennen, wenn Sie eine Funktion verwenden, die tatsächlich etwas tut, anstatt helloWorld. Denken Sie an Folgendes:

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

Dadurch wird dreimal "Ich füge 2 und 3 hinzu" ausgedruckt.

In C # können Sie Folgendes schreiben:

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;
    }
}

Welches würde nur einmal drucken.

oisdk
quelle
3

Wenn die Bewertung von putStrLn "Hello World"Nebenwirkungen hätte, würde die Nachricht nur einmal gedruckt.

Wir können dieses Szenario mit dem folgenden Code approximieren:

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

unsafePerformIOnimmt eine IOHandlung vor und "vergisst", dass es sich um eine IOHandlung handelt, die sich von der üblichen Reihenfolge löst, die durch die Zusammensetzung der IOHandlungen auferlegt wird, und die Wirkung gemäß den Launen der faulen Bewertung stattfinden lässt (oder nicht).

evaluatenimmt einen reinen Wert und stellt sicher, dass der Wert immer dann ausgewertet wird, wenn die resultierende IOAktion ausgewertet wird - was für uns der Fall sein wird, weil er auf dem Weg von liegt main. Wir verwenden es hier, um die Auswertung einiger Werte mit der Ausführung des Programms zu verbinden.

Dieser Code gibt "Hello World" nur einmal aus. Wir behandeln helloWorldals reinen Wert. Dies bedeutet jedoch, dass alle evaluate helloWorldAnrufe gemeinsam genutzt werden. Und warum nicht? Es ist schließlich ein reiner Wert, warum sollte er unnötig neu berechnet werden? Die erste evaluateAktion "knallt" den "versteckten" Effekt und die späteren Aktionen bewerten nur das Ergebnis (), was keine weiteren Effekte verursacht.

danidiaz
quelle
1
Es ist erwähnenswert, dass Sie unsafePerformIOin dieser Phase des Lernens von Haskell absolut nicht verwenden sollten . Der Name enthält aus einem bestimmten Grund "unsicher", und Sie sollten ihn nicht verwenden, es sei denn, Sie können (und haben) die Auswirkungen seiner Verwendung im Kontext sorgfältig abwägen. Der Code, den Danidiaz in die Antwort eingefügt hat, erfasst perfekt die Art von unintuitivem Verhalten, das daraus resultieren kann unsafePerformIO.
Andrew Ray
1

Es gibt ein Detail zu beachten: Sie rufen die putStrLnFunktion beim Definieren nur einmal auf helloWorld. In der mainFunktion verwenden Sie den Rückgabewert nur putStrLn "Hello, World"dreimal.

Der Vortragende sagt, dass putStrLnAnruf keine Nebenwirkungen hat und es wahr ist. Aber schauen Sie sich die Art von an helloWorld- es ist eine E / A-Aktion. putStrLnschafft es einfach für dich. Später verketten Sie 3 davon mit dem doBlock, um eine weitere E / A-Aktion zu erstellen main. Später, wenn Sie Ihr Programm ausführen, wird diese Aktion ausgeführt. Dort liegen die Nebenwirkungen.

Der Mechanismus, der dieser Basis zugrunde liegt - Monaden . Mit diesem leistungsstarken Konzept können Sie einige Nebenwirkungen wie das Drucken in einer Sprache verwenden, die Nebenwirkungen nicht direkt unterstützt. Sie verketten nur einige Aktionen und diese Kette wird beim Start Ihres Programms ausgeführt. Sie müssen dieses Konzept genau verstehen, wenn Sie Haskell ernsthaft einsetzen möchten.

Yuri Kovalenko
quelle