Избегайте подъема с помощью трансформаторов Monad
У меня есть проблема, которая очень хорошо подходит, используя стек MT (или даже один MT) через IO. Все хорошо, за исключением того, что использование лифта перед каждым действием ужасно раздражает! Я подозреваю, что на самом деле ничего не поделаешь, но я думал, что я все равно спрошу.
Я знаю, что нужно снимать целые блоки, но что, если код действительно смешанных типов? Было бы неплохо, если бы GHC выбрасывал какой-то синтаксический сахар (например, <-$
= <- lift
)?
Ответы
Ответ 1
Для всех стандартных монадов mtl вам не нужно lift
вообще. get
, put
, ask
, tell
- все они работают в любой монаде с правильным трансформатором где-то в стеке. Отсутствующий кусок IO
, и даже там liftIO
поднимает произвольное действие IO на произвольное количество слоев.
Это делается с помощью типов для каждого предлагаемого эффекта: например, MonadState
предоставляет get
и put
. Если вы хотите создать свою собственную упаковку newtype
вокруг стека трансформатора, вы можете сделать deriving (..., MonadState MyState, ...)
с расширением GeneralizedNewtypeDeriving
или сверните свой собственный экземпляр:
instance MonadState MyState MyMonad where
get = MyMonad get
put s = MyMonad (put s)
Вы можете использовать это, чтобы выборочно выставлять или скрывать компоненты вашего комбинированного трансформатора, определяя некоторые экземпляры, а не другие.
(Вы можете легко распространить этот подход на все новые монадические эффекты, которые вы сами определяете, определяя свой собственный класс типов и предоставляя шаблонные экземпляры для стандартных трансформаторов, но все новые монады встречаются редко, большую часть времени вы будете просто составите стандартный набор, предложенный mtl.)
Ответ 2
Вы можете сделать свои функции monad-agnostic, используя классы типов вместо конкретных стеков монады.
Скажем, что у вас есть эта функция, например:
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
), а не класса класса по умолчанию. Итак, что вы должны делать, когда хотите поднять IO-действия произвольно?
На помощь приходит MonadIO
! Он ведет себя почти одинаково с MonadState
, MonadReader
и т.д., С той лишь разницей, что он имеет несколько иной механизм подъема. Он работает следующим образом: вы можете выполнить любое действие IO
и использовать liftIO
, чтобы превратить его в анастическую версию монады. Итак:
action :: IO ()
liftIO action :: MonadIO m => m ()
Преобразуя все монадические действия, которые вы хотите использовать таким образом, вы можете переплетать монады столько, сколько хотите, без утомительного подъема.