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