In welchen Situationen sollten liftIO
verwendet werden? Wenn ich Folgendes verwende ErrorT String IO
die lift
Funktion funktioniert, um IO-Aktionen in ErrorT
also liftIO
scheint überflüssig zu sein.
Antworten
Zu viele Anzeigen?lift
hebt immer von der "vorherigen" Ebene ab. Wenn Sie von der zweiten Schicht aus heben müssen, benötigen Sie lift . lift
und so weiter.
Andererseits, liftIO
hebt immer von der IO-Schicht ab (die sich, wenn vorhanden, immer am unteren Ende des Stapels befindet). Wenn Sie also mehr als 2 Schichten von Monaden haben, werden Sie Folgendes zu schätzen wissen liftIO
.
Vergleichen Sie den Typ des Arguments in den folgenden Lambdas:
type T = ReaderT Int (WriterT String IO) Bool
> :t \x -> (lift x :: T)
\x -> (lift x :: T) :: WriterT String IO Bool -> T
> :t \x -> (liftIO x :: T)
\x -> (liftIO x :: T) :: IO Bool -> T
LiftIO ist nur eine Abkürzung zur IO-Monade, egal in welcher Monade Sie sich befinden. Im Grunde genommen ist liftIO gleichbedeutend mit der Verwendung einer variablen Anzahl von Lifts. Auf den ersten Blick mag dies redundant klingen, aber die Verwendung von liftIO hat einen großen Vorteil: Sie macht Ihren IO-Code unabhängig von der eigentlichen Monaden-Konstruktion, so dass Sie denselben Code wiederverwenden können, unabhängig von der Anzahl der Schichten, aus denen Ihre endgültige Monade aufgebaut ist (dies ist sehr wichtig, wenn Sie einen Monaden-Transformator schreiben).
Andererseits gibt es liftIO nicht umsonst, wie lift es tut: die Monad-Transformatoren, die Sie verwenden, müssen es unterstützen, z.B. muss die Monad, in der Sie sich befinden, eine Instanz der MonadIO-Klasse sein, aber die meisten Monads tun das heutzutage (und natürlich wird der Type-Checker dies zur Kompilierzeit für Sie überprüfen: das ist die Stärke von Haskell!)
Die bisherigen Antworten erklären den Unterschied recht gut. Ich wollte nur das Innenleben etwas beleuchten, damit es leichter zu verstehen ist, wie liftIO
ist nichts Magisches (für unerfahrene Haskeller wie mich).
liftIO :: IO a -> m a
ist ein kluges Instrument, auf dem man aufbauen kann
lift :: (Control.Monad.Trans.Class.MonadTrans t, Monad m) => m a -> t m a
y am häufigsten verwendet, wenn die untere Monade IO
. Für die IO
Monade ist ihre Definition recht einfach.
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
instance MonadIO IO where
liftIO = id
So einfach ist das... liftIO
ist in Wirklichkeit nur id
für die IO
Monade und grundsätzlich IO
ist die einzige, die in der Definition der Typklasse enthalten ist.
Die Sache ist die, wenn wir einen Monadentyp haben, der aus mehreren Schichten von Monadentransformatoren besteht über IO
haben wir besser eine MonadIO
Instanz für jede dieser Monaden-Transformationsebenen. Zum Beispiel die MonadIO
Instanz von MaybeT m
erfordert m
zu sein von MonadIO
auch die Typklasse.
Schreiben einer MonadIO
Instanz ist im Grunde auch eine sehr einfache Aufgabe. Für MaybeT m
ist wie folgt definiert
instance (MonadIO m) => MonadIO (MaybeT m) where
liftIO = lift . liftIO
oder für StateT s m
instance (MonadIO m) => MonadIO (StateT s m) where
liftIO = lift . liftIO
sie sind alle gleich. Stellen Sie sich vor, Sie haben einen 4-Schicht-Transformator-Stapel, dann müssen Sie entweder lift . lift . lift . lift $ myIOAction
oder einfach liftIO myIOAction
. Wenn man darüber nachdenkt, ist jeder lift . liftIO
bringt Sie eine Ebene tiefer in den Stapel, bis er ganz nach unten gräbt IO
wo liftIO
ist definiert als id
und endet mit demselben Code wie die zusammengesetzten lift
s oben.
Dies ist der Grund, warum unabhängig von der Konfiguration des Transformatorstapels, sofern alle darunter liegenden Ebenen Mitglieder von MonadIO
y MonadTrans
eine einzige liftIO
ist genau richtig.