2015-01-28 16 views
9

Haskell IO wird oft dahingehend erklärt, dass das gesamte Programm eine reine Funktion ist (main), die einen E/A-Wert zurückgibt (oft als imperatives IO-Programm beschrieben), der dann von der Runtime ausgeführt wird.Warum/wie funktioniert rekursives IO?

Dieses mentale Modell funktioniert gut für einfache Beispiele, fiel aber für mich um, sobald ich eine rekursive main in Learn You A Haskell sah. Zum Beispiel:

main = do 
    line <- getLine 
    putStrLn line 
    main 

Oder, wenn Sie bevorzugen:

main = getLine >>= putStrLn >> main 

Da main nie beendet, es nie einen IO-Wert tatsächlich zurückkehrt, doch das Programm endlos liest und Echo Linien gerade fein zurück - so die einfache Erklärung oben funktioniert nicht ganz. Fehle ich etwas Einfaches oder gibt es eine vollständigere Erklärung (oder ist es einfach "Compiler Magic")?

+4

Wie alles andere in Haskell, ist der IO-Wert nicht streng und wird daher nur nach Bedarf ausgewertet. – Rufflewind

+0

Alle Typen - einschließlich 'IO()' - haben einen zusätzlichen Wert namens [bottom] (https://wiki.haskell.org/Bottom). Also "main" ist technisch "zurück" ein "IO()"; es ist nur, dass es die langweiligste 'IO()' zurückgibt: ⊥. Haskell kann niemals das "ganze" "Haupt" bewerten, aber aufgrund der Nicht-Strenge kann es versuchen. –

+3

Wenn Sie denken, dass Sie jetzt verwirrt sind, warten Sie einfach, bis Sie etwas über 'fixIO: :(a-> IO a) -> IO a' erfahren. – dfeuer

Antwort

14

In diesem Fall ist main ein Wert vom Typ IO() anstelle einer Funktion. Sie können als eine Folge von IO a Werte daran denken:

main = getLine >>= putStrLn >> main 

Dies macht es einen rekursiven Wert, nicht anders als unendliche Listen:

foo = 1 : 2 : foo 

Wir können einen Wert wie diese zurück, ohne die benötigen zu bewerten das ganze Ding. In der Tat ist es eine einigermaßen gebräuchliche Redewendung.

foowird Schleife für immer wenn Sie versuchen, die ganze Sache zu verwenden. Aber das gilt auch für main: es sei denn, Sie verwenden eine externe Methode, um daraus auszubrechen, wird es nie aufhören zu loopen! Aber Sie können Elemente aus foo herausholen oder Teile von main ausführen, ohne alles zu bewerten.

+9

Sie möchten vielleicht erwähnen, dass für 'IO' die Operation" >> = "in ihrem linken Operanden streng ist, aber in ihrem rechten Operanden faul ist. Es muss zuerst wissen, was zu tun ist, aber es muss nicht wissen, wie es herausfinden soll, was als nächstes kommt, bis es das erste getan hat. – dfeuer

7

Der Wert main bezeichnet ist ein unendliches Programm:

main = do 
    line <- getLine 
    putStrLn line 
    line <- getLine 
    putStrLn line 
    line <- getLine 
    putStrLn line 
    line <- getLine 
    putStrLn line 
    line <- getLine 
    putStrLn line 
    line <- getLine 
    putStrLn line 
    ... 

Aber es ist als rekursive Struktur im Speicher dargestellt, die selbst verweist. Diese Darstellung ist endlich, es sei denn, jemand versucht, das Ganze zu entfalten, um eine nicht-rekursive Darstellung des gesamten Programms zu erhalten - , die niemals beenden würde.

Aber genauso wie Sie wahrscheinlich herausfinden können, wie Sie das oben beschriebene Infinite-Programm ausführen, ohne darauf zu warten, dass ich Ihnen "alles" sage, kann Haskells Laufzeitsystem herausfinden, wie man main ausführt, ohne die Rekursion zu entwickeln im Voraus.

Haskells lazy evaluation ist eigentlich mit dem Runtime-System der Ausführung des main IO Programm verschachtelt, so funktioniert dies auch für eine Funktion, die eine IO Aktion zurück, die rekursiv die Funktion aufruft, wie:

main = foo 1 

foo :: Integer -> IO() 
foo x = do 
    print x 
    foo (x + 1) 

Hier foo 1 ist kein rekursiver Wert (enthält foo 2, nicht foo 1), aber es ist immer noch ein unendliches Programm.Dies funktioniert jedoch gut, da das mit foo 1 gekennzeichnete Programm nur auf Abruf erzeugt wird; Es kann erzeugt werden, wie die Ausführung des Laufzeitsystems von main geht.

Standardmäßig bedeutet Haskells Faulheit, dass nichts ausgewertet wird, bis es benötigt wird, und dann nur "gerade genug", um über den aktuellen Block hinauszukommen. Letztendlich kommt die Quelle aller "Bedürfnisse" in "bis es gebraucht wird" vom Laufzeitsystem, das wissen muss, was der nächste Schritt im main Programm ist, damit es es ausführen kann. Aber es ist immer nur die nächste Schritt; Der Rest des Programms kann danach unbewertet bleiben, bis der nächste Schritt vollständig ausgeführt wurde. So können unendlich viele Programme ausgeführt werden und nützliche Arbeit leisten, solange es immer nur eine begrenzte Menge an Arbeit ist, um "einen weiteren Schritt" zu erzeugen.