单子变换器的使用方法

5

我正在阅读关于Haskell书中的单子变换器。

作者提到了以下内容:

What about Monad? There’s no problem composing two arbitrary datatypes that have Monad instances. We saw this already when we used Compose with Maybe and list, which both have Monad instances defined. However, the result of having done so does not give you a Monad.

The issue comes down to a lack of information. Both types Compose is working with are polymorphic, so when you try to write bind for the Monad, you’re trying to combine two polymorphic binds into a single combined bind. This, it turns out, is not possible:

{-# LANGUAGE InstanceSigs #-}
-- impossible.
instance (Monad f, Monad g) => Monad (Compose f g) where
  return = pure
  (>>=) :: Compose f g a
  -> (a -> Compose f g b)
  -> Compose f g b
  (>>=) = ???

These are the types we’re trying to combine, because and are necessarily both monads with their own Monad instances:

Monad f => f a -> (a -> f b) -> f b
Monad g => g a -> (a -> g b) -> g b

From those, we are trying to write this bind:

(Monad f, Monad g) => f (g a) -> (a -> f (g b)) -> f (g b)

Or formulated differently:

(Monad f, Monad g) => f (g (f (g a))) -> f (g a)

And this is not possible. There’s not a good way to join that final and . It’s a great exercise to try to make it work, because the barriers you’ll run into are instructive in their own right. You can also read Composing monads1 by Mark P. Jones and Luc Duponcheel to see why it’s not possible.

我无法理解,他的意思是什么?Monad transformer 是什么,它有什么用处?

2个回答

9
作者的意思是说任何两个单子的组合都是不可能的。这并不是因为语言不好,而是因为有些单子的组合不是一个单子。
例如,IsntMonad 不是一个单子:
newtype IsntMonad a = IsntMonad (Maybe (IO a))

instance Monad IsntMonad where
    m >>= k = ???

但是,IsMonad是一个单子:
newtype IsMonad a = IsMonad { runIsMonad :: IO (Maybe a) }

instance Monad IsMonad where
    (IsMonad ioma) >>= k = IsMonad $ do
        ma <- ioma
        case ma of
          Just a -> runIsMonad $ k a
          Nothing -> return Nothing

因此,单子变换器只是一种使组合成为可能的方法。但是,如果我们知道在一般情况下不可能,那么它是如何工作的呢?是的,它对于任何两个单子来说都是不可能的,但是对于某些具体的单子和任何其他单子来说是可能的。
因此,有些单子可以与任何其他单子组合。在第一次近似中,这样的单子就是单子变换器。
例如,单子Maybe可以与任何其他单子组合,如下所示:Monad m => m (Maybe a),因此m (Maybe a)是一个单子。但是我们如何为其创建单子实例呢?
instance Monad m => Monad ??? where ...

然后,MaybeT 出现作为助手,它被称为一个单子变换器(后缀 T 暗示着这一点)。

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

instance Monad m => Monad (MaybeT m) where
    m >>= k = MaybeT $ do
        ma <- runMaybeT m
        case ma of
          Just a -> runMaybeT $ k a
          Nothing -> return Nothing

在此之后,自动 MaybeT IO, MaybeT [], MaybeT STM ... 都是单子。您不需要为它们编写实例。这就是它们的好处。
我们知道,单子变换器只是单子组合的一种方式。但这并不能回答问题:单子组合有什么好处?我们为什么要花时间和精力去找到一种组合单子的方式?
嗯,正如您所知,任何单子都伴随着某些效应。如果我们组合两个单子,我们也将组合它们的效应,并且这个结果将会自动产生。
例如:
StateT s Maybe a -- a computation with a state `s` which can fail
ReaderT e Maybe a -- a computation with access to an environment `e` and option to fail
ReaderT e (StateT s Maybe) a -- a computation with state `s`, access to an environment `e` and option to fail
...

单子的组合不是可交换的。例如,MaybeT (State s)StateT s Maybe不同。理解组合将产生什么影响的简单方法是记住影响是从内部单子到外部单子产生的。在MaybeT (State s)中,第一个影响将由State s单子产生,然后由Maybe产生。但在StateT s Maybe中,第一个影响将由Maybe产生,然后由State s产生。这意味着在StateT s Maybe失败的情况下,我们会丢失状态,但在MaybeT (State s)中,我们会保存发生错误的状态。

因此,单子的组合是在类型级别上构建更复杂单子的简单方法。

这如何实际应用?例如,让我们想象一种情况。您有某些东西和转换它的选项。命令式代码可能如下所示:

thing = defaultValue;
if (transformByOption0)
    doTransformationByOption0(thing);
if (transformByOption1)
    doTransformationByOption1(thing);
...

在 Haskell 中,我们可以这样写:
thing <- flip execStateT defaultValue $ do
    when transformByOption0 $
        modify doTransformationByOption0
    when transformByOption1 $
        modify doTransformationByOption1
    when transformByOption2 $
        put =<< doMonadicTransformationByOption2 =<< get
    ...

这里发生了什么?我在本地封装了我的单子(可以是任何单子)到一个单子State MyThing中,以便更容易解决这个问题。这只是数十亿例子中的一个,单子的组合可以轻松解决你的问题。


第一个例子 IsntMonad,为什么它不是一个 monad? 我们无法编写 instance Monad IsntMonad where (IsntMonad a) >>= k = ??? 吗? - softshipper
是的,但这并不是完整的答案。当您尝试编写实例时,您会发现无法从IO中提取值,然后将其应用于Kleisli k :: a -> IsntMonad b的箭头。 - freestyle
Kleisli 究竟是什么?我认为它就像使用鱼 >=> 运算符的函数组合一样,是单子组合。 - softshipper
看一下 (>>=) 的签名 - Monad m => m a -> (a -> m b) -> m b。第二个参数有时被称为 Kleisli 箭头(它来自 Kleisli 类别,与任何单子自然相关联,其中组合是 (>=>))。这就是为什么第二个参数的变量被称为 k(Kleisli 的第一个字母)的原因。 - freestyle
你提到了“这意味着在 StateT s Maybe 失败的情况下,我们会丢失状态,但在 MaybeT (State s) 中,我们会保存发生错误的状态。” 你能给我举个例子吗? - softshipper
这是一个使用mtl库的例子:flip execStateT 0 (modify (+1) >> empty >> modify (+1) :: StateT Int Maybe ()) => Nothing; flip execState 0 $ runMaybeT (modify (+1) >> empty >> modify (+1) :: MaybeT (State Int) ()) => 1 - freestyle

1

Monad transformer是一种允许自身与其他monad之间的效果组合的monad。

Reader的效果将是函数ask在其上下文中可访问的环境。

State的效果将是get可访问的状态,并可由put进行修改。

一个monad transformer,StateT a m s将是一种能够携带其状态并提供来自monad m的某些效果的monad transformer。

monad transformer stack的一个实例可以是:StateT Int (Reader String) Int,表示具有类型为String的Reader环境、类型为Int的状态和返回类型为Int的计算。如果Reader改为ReaderT,则可以将其与IO链接起来,在同一个函数中读/写一些文件或其他内容。

具体实现中还有很多细节需要注意,但这就是单子变换器的含义和使用方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接