2016-05-31 8 views
11

Etwas durch den folgenden Code mystifiziert. In der Nicht-Spielzeug-Version des Problems versuche ich eine monadische Berechnung in einer Monade durchzuführen, deren Werte nur innerhalb von IO konstruiert werden können. Scheint so, als ob die Magie hinter IO solche Berechnungen streng macht, aber ich kann nicht herausfinden, wie genau das passiert.IO-Monade verhindert Kurzschlüsse von eingebetteten MapM?

Der Code:

data Result a = Result a | Failure deriving (Show) 

instance Functor Result where 
    fmap f (Result a) = Result (f a) 
    fmap f Failure = Failure 

instance Applicative Result where 
    pure = return 
    (<*>) = ap 

instance Monad Result where 
    return = Result 
    Result a >>= f = f a 
    Failure >>= _ = Failure 

compute :: Int -> Result Int 
compute 3 = Failure 
compute x = traceShow x $ Result x 

compute2 :: Monad m => Int -> m (Result Int) 
compute2 3 = return Failure 
compute2 x = traceShow x $ return $ Result x 

compute3 :: Monad m => Int -> m (Result Int) 
compute3 = return . compute 

main :: IO() 
main = do 
    let results = mapM compute [1..5] 
    print $ results 
    results2 <- mapM compute2 [1..5] 
    print $ sequence results2 
    results3 <- mapM compute3 [1..5] 
    print $ sequence results3 
    let results2' = runIdentity $ mapM compute2 [1..5] 
    print $ sequence results2' 

Der Ausgang:

1 
2 
Failure 
1 
2 
4 
5 
Failure 
1 
2 
Failure 
1 
2 
Failure 

Antwort

10

Nizza Testfälle. Hier ist, was passiert:

  • in mapM compute wir sehen Faulheit bei der Arbeit, wie üblich. Keine Überraschung hier.

  • in mapM compute2 arbeiten wir innerhalb des IO Monade, deren mapM Definition wird die ganze Liste verlangt: Im Gegensatz zu Result, die das Ende der Liste springt, sobald Failure gefunden wird, IO wird immer die ganze Liste scannen. Notieren Sie den Code:

    compute2 x = traceShow x $ return $ Result x 
    

    Also, die oben wil die Debug-Nachricht, sobald jedes Element der Liste der IO Aktionen zugegriffen drucken. Alle sind, also drucken wir alles.

  • in mapM compute3 nutzen wir nun, etwa:

    compute3 x = return $ traceShow x $ Result x 
    

    Da nun return in IO ist faul, es wird nicht Auslöser der traceShow wenn die IO Aktion zurück. Wenn also mapM compute3 ausgeführt wird, wird keine Nachricht angezeigt. Stattdessen sehen wir Nachrichten nur, wenn sequence results3 ausgeführt wird, was die Result - nicht alle von ihnen, aber nur so viel wie nötig erzwingt.

  • das letzte Identity Beispiel ist auch ziemlich schwierig. Beachten Sie dies:

    > newtype Id1 a = Id1 a 
    > data Id2 a = Id2 a 
    > Id1 (trace "hey!" True) `seq` 42 
    hey! 
    42 
    > Id2 (trace "hey!" True) `seq` 42 
    42 
    

    wenn eine newtype Verwendung zur Laufzeit gibt es keine Boxen/Unboxing (AKA Heben) beteiligt, so zwingt ein Id1 x Wert bewirkt x gezwungen werden. Bei data Typen geschieht dies nicht: Der Wert wird in eine Box eingeschlossen (z. B. Id2 undefined entspricht nicht undefined).

    In Ihrem Beispiel fügen Sie einen Identity Konstruktor hinzu, aber das ist von der newtype Identity !! Also, wenn Sie anrufen

    return $ traceShow x $ Result x 
    

    die return hier nicht alles einpacken und die traceShow wird sofort ausgelöst, sobald mapM ausgeführt wird.

+0

Vielen Dank für Ihre Antwort, Chi. Darf ich fragen, woher Sie wissen, dass die IO-Definition von mapM streng ist und dass die Rückkehr faul ist? – NioBium

+3

@NioBium 'Failure >> = f = Fehler' verwirft das 'f': Es ist nicht notwendig, weiter auf der monadischen Kette fortzufahren. IO hat eine niedrigere Hebeldefinition, die nicht leicht geschrieben werden kann, aber - abgesehen von Ausnahmen - "action" = f wird immer "f" aufrufen, da man erwartet, dass z. "action >> print 4" wird schließlich 4 drucken, egal was "action" tut (Ausnahme von IO-Ausnahmen und Nicht-Beendigung). – chi

+0

Rechts. Danke noch einmal! – NioBium

1

Ihre Result Typ praktisch identisch zu sein scheint zu Maybe, mit

Result <-> Just 
Failure <-> Nothing 

Aus Gründen der mein armes Hirn, ich bleibe zu Maybe Terminologie in den Rest dieser Antwort.

chi erklärt, warum IO (Maybe a) nicht Kurzschluss wie erwartet. Aber dort ist eine Art, die Sie für diese Art von Sache verwenden können! Es ist tatsächlich im Wesentlichen der gleiche Typ, aber mit einer anderen Monad Instanz. Sie können es in Control.Monad.Trans.Maybe finden. Es sieht etwa so aus:

newtype MaybeT m a = MaybeT 
    { runMaybeT :: m (Maybe a) } 

Wie Sie sehen können, ist dies nur ein Wrapper um newtypem (Maybe a). Aber seine Monad Instanz ist sehr unterschiedlich:

instance Monad m => Monad (MaybeT m) where 
    return a = MaybeT $ return (Just a) 
    m >>= f = MaybeT $ do 
    mres <- runMaybeT m 
    case mres of 
     Nothing -> return Nothing 
     Just a -> runMaybeT (f a) 

Das heißt, m >>= f läuft die m Berechnung der zugrunde liegenden Monade, immer Maybe etwas oder andere. Wenn es Nothing wird, stoppt es einfach und gibt Nothing zurück. Wenn es etwas bekommt, übergibt es das an f und führt das Ergebnis aus. Sie können auch jede m Aktion in eine „erfolgreiche“ MaybeT m Aktion drehen lift von Control.Monad.Trans.Class mit:

class MonadTrans t where 
    lift :: Monad m => m a -> t m a 

instance MonadTrans MaybeT where 
    lift m = MaybeT $ Just <$> m 

Sie auch diese Klasse verwenden kann, definiert irgendwo wie Control.Monad.IO.Class, die oft klarer und kann viel bequemer sein:

class MonadIO m where 
    liftIO :: IO a -> m a 

instance MonadIO IO where 
    liftIO m = m 

instance MonadIO m => MonadIO (MaybeT m) where 
    liftIO m = lift (liftIO m)