Избегать лифта с монадными трансформаторами

У меня есть проблема, к которой стека монадных трансформаторов (или даже один монадный трансформатор) более 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 ()

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

Другие вопросы по тегам