Haskell: winda kontra liftIO

83

W jakich sytuacjach należy liftIOużyć? Kiedy używam ErrorT String IO, liftfunkcja działa w celu podniesienia akcji IO do ErrorT, więc liftIOwydaje się zbędna.

Lachlan
źródło

Odpowiedzi:

94

liftzawsze podnosi się z „poprzedniej” warstwy. Jeśli chcesz podnieść z drugiej warstwy, potrzebujesz lift . lifti tak dalej.

Z drugiej strony, liftIOzawsze podnosi się z warstwy IO (która, jeśli jest obecna, zawsze znajduje się na dole stosu). Jeśli więc masz więcej niż 2 warstwy monad, z pewnością docenisz liftIO.

Porównaj typ argumentu w następujących lambdach:

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
Roman Cheplyaka
źródło
34
Generalnie będę używał liftIOdo podniesienia do warstwy IO, nawet jeśli liftjest to wystarczające, ponieważ wtedy mogę zmienić stos monady, a kod nadal działa.
John L
14
@John: słuszna uwaga. A także sprawia, że ​​staje się oczywiste, że podnosisz IO, a nie jakąkolwiek inną monadę.
Roman Cheplyaka
Jestem nowicjuszem: liftw tej odpowiedzi (i pytaniu) zakłada się, że pochodzi od Control.Monad.Trans.Class? Czy nie jest Monadic liftingto ogólne podnoszenie, jak opisano w pierwszej sekcji tutaj ?
Nawaz
38

liftIO to tylko skrót do monady IO, bez względu na to, w której monadzie się znajdujesz. W zasadzie liftIO to użycie zmiennej liczby wind. Na początku może się to wydawać zbędne, ale użycie liftIO ma jedną wielką zaletę: sprawia, że ​​twój kod IO jest niezależny od faktycznej konstrukcji Monady, więc możesz ponownie użyć tego samego kodu bez względu na liczbę warstw, z których została zbudowana twoja ostateczna Monada (jest to dość ważne podczas pisania transformatora monadowego).

Z drugiej strony, liftIO nie przychodzi za darmo, jak to robi winda: transformatory Monad, których używasz, muszą mieć dla niego wsparcie, np. Monada, w której jesteś, musi być instancją klasy MonadIO, ale większość dzisiejszych Monad to robi. (i oczywiście narzędzie do sprawdzania typów sprawdzi to podczas kompilacji: to jest siła Haskella!).

Cristiano Paris
źródło
2

Wszystkie poprzednie odpowiedzi dość dobrze wyjaśniają różnicę. Chciałem tylko rzucić trochę światła na wewnętrzne funkcjonowanie, aby łatwiej było zrozumieć, dlaczego liftIOnie jest to coś magicznego (dla początkujących Haskellerów, takich jak ja).

liftIO :: IO a -> m a

jest mądrym narzędziem, na którym po prostu buduj

lift :: (Control.Monad.Trans.Class.MonadTrans t, Monad m) => m a -> t m a

i najczęściej używany, gdy jest dolna monada IO. W przypadku IOmonady definicja jest dość prosta.

class (Monad m) => MonadIO m where
  liftIO :: IO a -> m a

instance MonadIO IO where
  liftIO = id

To proste ... liftIOjest w rzeczywistości tylko iddla IOmonady i zasadniczo IOjest jedyną, która mieści się w definicji klasy typu.

Rzecz w tym, że kiedy mamy typ monady, który składa się z kilku warstw transformatorów monadowych IO, lepiej mieć MonadIOinstancję dla każdej z tych warstw transformatora monadowego. Na przykład MonadIOinstancja MaybeT mwymaga, maby była również MonadIOtypeklasą.

Pisanie MonadIOinstancji jest również w zasadzie bardzo prostym zadaniem. Ponieważ MaybeT mjest zdefiniowany jako

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

albo za StateT s m

instance (MonadIO m) => MonadIO (StateT s m) where
  liftIO = lift . liftIO

oni wszyscy są tacy sami. Wyobraź sobie, że masz 4-warstwowy stos transformatorów, wtedy albo musisz to zrobić, lift . lift . lift . lift $ myIOActionalbo po prostu liftIO myIOAction. Jeśli się nad tym zastanowić, każdy lift . liftIOprzeniesie Cię o jedną warstwę w dół w stosie, aż zacznie kopać w dół do miejsca, IOw którym liftIOjest zdefiniowane jako idi kończy się tym samym kodem, co skomponowane liftpowyżej.

Więc dlaczego to jest w zasadzie niezależnie od konfiguracji transformator stosu, pod warunkiem, że wszystkie warstwy pod nią są członkowie MonadIOi MonadTransjeden liftIOjest w porządku.

Redu
źródło