Вы можете сделать свои функции не зависящими от монады, используя классы типов вместо конкретных стеков монад.
Допустим, у вас есть эта функция, например:
bangMe :: State String ()
bangMe = do
str <- get
put $ str ++ "!"
-- or just modify (++"!")
Конечно, вы понимаете, что он также работает как трансформатор, поэтому можно написать:
bangMe :: Monad m => StateT String m ()
Однако, если у вас есть функция, которая использует другой стек, скажем, ReaderT [String] (StateT String IO) ()
или что-то еще, вам придется использовать страшную функцию lift
! Так как этого избежать?
Хитрость заключается в том, чтобы сделать сигнатуру функции еще более общей, чтобы она говорила, что монада State
может появляться в любом месте стека монад. Это делается так:
bangMe :: MonadState String m => m ()
Это заставляет m
быть монадой, которая поддерживает состояние (практически) в любом месте стека монад, и поэтому функция будет работать без подъема для любого такого стека.
Хотя есть одна проблема; поскольку IO
не является частью mtl
, он не имеет ни преобразователя (например, IOT
), ни класса удобных типов по умолчанию. Так что же делать, если вы хотите произвольно отменить операции ввода-вывода?
На помощь приходит MonadIO
! Он ведет себя почти идентично MonadState
, MonadReader
и т. Д., С той лишь разницей, что он имеет немного другой механизм подъема. Это работает так: вы можете выполнить любое действие IO
и использовать liftIO
, чтобы превратить его в версию, не зависящую от монады. Итак:
action :: IO ()
liftIO action :: MonadIO m => m ()
Преобразуя все монадические действия, которые вы хотите использовать таким образом, вы можете переплетать монады столько, сколько хотите, без какого-либо утомительного подъема.