Избегать лифта с монадными трансформаторами - PullRequest
48 голосов
/ 29 января 2012

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

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

Ответы [ 2 ]

55 голосов
/ 29 января 2012

Для всех стандартных 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.

48 голосов
/ 29 января 2012

Вы можете сделать свои функции не зависящими от монады, используя классы типов вместо конкретных стеков монад.

Допустим, у вас есть эта функция, например:

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 ()

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

...