Подтвердить что ты не робот

Избегайте подъема с помощью трансформаторов Monad

У меня есть проблема, которая очень хорошо подходит, используя стек MT (или даже один MT) через IO. Все хорошо, за исключением того, что использование лифта перед каждым действием ужасно раздражает! Я подозреваю, что на самом деле ничего не поделаешь, но я думал, что я все равно спрошу.

Я знаю, что нужно снимать целые блоки, но что, если код действительно смешанных типов? Было бы неплохо, если бы GHC выбрасывал какой-то синтаксический сахар (например, <-$= <- lift)?

4b9b3361

Ответ 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 ()

Преобразуя все монадические действия, которые вы хотите использовать таким образом, вы можете переплетать монады столько, сколько хотите, без утомительного подъема.