2016-12-03 3 views
5

Auf meinem journing zu faul IO in Haskell zu erfassen versuchte ich folgendes:Reimplementierung getContents GetChar mit

main = do 
    chars <- getContents 
    consume chars 

consume :: [Char] -> IO() 
consume [] = return() 
consume ('x':_) = consume [] 
consume (c : rest) = do 
    putChar c 
    consume rest 

, die alle Zeichen nur Echos in stdin getippt, bis ich Hit 'x'.

Also, ich naiv gedacht, sollte es möglich sein getContents reimplementieren getChar mit etwas entlang der folgenden Zeilen zu tun:

myGetContents :: IO [Char] 
myGetContents = do 
    c <- getChar 
    -- And now? 
    return (c: ???) 

Stellt sich heraus, es ist nicht so einfach, da die ??? eine Funktion des Typs erfordern würde IO [Char] -> [Char] die würde - Ich denke - breche die ganze Idee der IO-Monade.

Die Überprüfung der Implementierung von getContents (oder besser hGetContents) zeigt eine ganze Wurstfabrik von schmutzigen IO-Zeug. Ist meine Annahme richtig, dass myGetContents nicht implementiert werden kann, ohne schmutzigen, dh monad-breaking, Code zu verwenden?

Antwort

6

Sie benötigen ein neues primitives unsafeInterleaveIO :: IO a -> IO a, das die Ausführung seiner Argumentaktion verzögert, bis das Ergebnis dieser Aktion ausgewertet wird. Dann

+3

Funktioniert wie ein Charme :) Bleibt hinzuzufügen, dass 'unsafeInterleaveIO' von' System.IO.Unsafe' importiert werden muss. – johanneslink

1

Sie sollten wirklich vermeiden, irgendetwas in System.IO.Unsafe zu verwenden, wenn überhaupt möglich. Sie tendieren dazu, die referenzielle Transparenz zu zerstören und sind keine in Haskell verwendeten Funktionen, es sei denn, dies ist absolut notwendig.

Wenn Sie Ihre Typ Unterschrift ein wenig ändern, vermute ich, dass Sie eine mehr idiomatische Annäherung an Ihr Problem bekommen können.

consume :: Char -> Bool 
consume 'x' = False 
consume _ = True 

main :: IO() 
main = loop 
    where 
    loop = do 
     c <- getChar 
     if consume c 
     then do 
     putChar c 
     loop 
     else return() 
+0

Der Hinweis im ersten Absatz ist in den meisten Fällen sinnvoll, aber in diesem Fall trifft er nicht zu, da das OP explizit und absichtlich versucht, faule E/A erneut zu implementieren. – duplode

+0

Für die Aufzeichnung basierte meine Antwort auf einem Twitteraustausch mit dem OP, der in der ursprünglichen Frage nicht reflektiert wird. Ich hatte den Eindruck, dass sie nicht explizit nach "faulen IO" waren und nur ihren Code zum Laufen bringen wollten. Angesichts des Geistes der eigentlichen Frage ist dies jedoch eine falsche Antwort. – bojo

0

Sie können dies ohne irgendwelche Hacks tun.

Wenn Sie nur stdin in einem String lesen möchten, benötigen Sie keine der Funktionen unsafe*.

IO ist ein Monad, und ein Monad ist ein Applicative Functor. A Functor wird durch die Funktion fmap, dessen Signatur definiert ist:

fmap :: Functor f => (a -> b) -> f a -> f b 

, die diese beiden Gesetze erfüllt:

fmap id = id 
fmap (f . g) = fmap f . fmap g 

Effektiv fmap wendet eine Funktion auf Werte gewickelt.

Gegeben ein bestimmtes Zeichen 'c', was ist der Typ fmap ('c':)? Wir können die zwei Typen aufschreiben, dann sie vereinigen:

fmap  :: Functor f => (a  -> b ) -> f a  -> f b 
    ('c':) ::    [Char] -> [Char] 
fmap ('c':) :: Functor f => ([Char] -> [Char]) -> f [Char] -> f [Char] 

Hinweis darauf, dass IO ist ein Funktor, wenn wir myGetContents :: IO [Char] definieren wollen, scheint es sinnvoll, diese zu verwenden:

myGetContents :: IO [Char] 
myGetContents = do 
    x <- getChar 
    fmap (x:) myGetContents 

Dies ist in der Nähe , aber nicht ganz äquivalent zu getContents, da diese Version versuchen wird, über das Ende der Datei hinaus zu lesen und einen Fehler auszulösen, anstatt eine Zeichenfolge zurückzugeben.Wenn man es nur anschaut, sollte man das klarstellen: Es gibt keine Möglichkeit, eine konkrete Liste, nur eine unendliche Betrugskette, zurückzugeben. Zu wissen, dass der konkrete Fall "" bei EOF ist (und mit der Infix Syntax <$> für fmap) bringt uns zu:

import System.IO 
myGetContents :: IO [Char] 
myGetContents = do 
    reachedEOF <- isEOF 
    if reachedEOF 
    then return [] 
    else do 
    x <- getChar 
    (x:) <$> myGetContents 

Die Applicative Klasse bietet eine (leichte) Vereinfachung.

Erinnern Sie sich daran, dass IO ein Applicative Functor ist, nicht nur irgendein alter Functor. Es gibt „Applicative Laws“ im Zusammenhang mit dieser typeclass ähnlich wie die „Functor Gesetze“, aber wir werden speziell Blick auf <*>:

<*> :: Applicative f => f (a -> b) -> f a -> f b 

Dies ist fast identisch mit fmap (aka <$>), mit der Ausnahme, dass die Funktion zu bewerben ist auch verpackt.

import System.IO 
myGetContents :: IO String 
myGetContents = do 
    reachedEOF <- isEOF 
    if reachedEOF 
    then return [] 
    else (:) <$> getChar <*> myGetContents 

Eine Änderung ist notwendig, wenn der Eingang unendlich sein kann: Wir können dann mithilfe des Applicative Stils die bind in unserer else Klausel vermeiden.

Denken Sie daran, wenn ich sagte, dass Sie nicht die unsafe* Funktionen benötigen, wenn Sie nur alle von stdin in eine String lesen wollen? Nun, wenn Sie nur einige der Eingabe möchten, tun Sie. Wenn Ihre Eingabe unendlich lang sein könnte, tun Sie das auf jeden Fall. Das endgültige Programm unterscheidet sich in einem Ein- und einem einzigen Wort:

import System.IO 
import System.IO.Unsafe 
myGetContents :: IO [Char] 
myGetContents = do 
    reachedEOF <- isEOF 
    if reachedEOF 
    then return [] 
    else (:) <$> getChar <*> unsafeInterleaveIO myGetContents 

Die definierende Funktion der lazy IO ist unsafeInterleaveIO (von System.IO.Unsafe). Dies verzögert die Berechnung der Aktion IO, bis sie angefordert wird.