为什么 MonadIO 特定于 IO,而不是更通用的 MonadTrans?

Why is MonadIO specific to IO, rather than a more generic MonadTrans?

所以在 transformers 我明白了,

class (Monad m) => MonadIO m where
    -- | Lift a computation from the 'IO' monad.
    liftIO :: IO a -> m a

instance MonadIO IO where
    liftIO = id

我知道这与 MonadTrans 不同的原因是如果你有一些 M1T (M2T (M3T (M4T IO))) x 由 4 个组成的 monad 转换器组成,那么你不想 lift . lift . lift . lift $ putStrLn "abc" 但你会而只是 liftIO $ putStrLn "abc"

但是,当上面的基本定义似乎是 liftIO 的这组奇怪的递归时,IO 的这种特殊性似乎很奇怪。似乎应该有一些像 (ExceptT :~: MaybeT) IO x 这样的组合器的新类型声明,这样你就只需要一个 lift (我想这是一个 monad transformer transformer?),或者一些 multi-参数类型 class、

class (Monad m) => MonadEmbed e m
    -- | Lift a computation from the `e` monad.
    embed :: e a -> m a

instance (Monad m) => MonadEmbed m m where
     embed = id

为什么 transformers 不使用其中一种方法,这样 MonadTrans 序列就不必植根于 IO?是不是因为转换器处理所有 "other" 效果,所以最底部的唯一东西是 Identity(已经用 return :: a -> m a 处理)或 IO?或者上面是否需要 transformers 库不愿包含的 UndecidableInstances 之类的东西?或者什么?

But, this specificity for IO seems very weird

我质疑这是特定于 IO 的假设。我还在 mtl 中看到许多其他 classes。例如:

class Monad m => MonadError e m | m -> e where
    throwError :: e -> m a
    catchError :: m a -> (e -> m a) -> m a

class Monad m => MonadState s m | m -> s where
    get :: m s
    put :: s -> m ()
    state :: (s -> (a, s)) -> m a

...以及许多其他人。通常,构建 monadic 操作的“mtl 方式”是使用这些类型class-多态操作,因此永远不需要 lift —— 相反,只需将操作单态化为适当的提升型。例如,MonadError 完全取代了假设的 liftMaybe :: MonadMaybe m => Maybe a -> m a:而不是提升 Maybe a 值,Maybe a 值的生产者调用 throwErrorreturn 而不是 NothingJust.

It seems like there should be a newtype declaration for some combinator like (ExceptT :~: MaybeT) IO x so that a single lift is all you ever need

根据此提案,您将需要(至少)两种不同的 种类 的电梯:一部从 m atrans m a 的电梯,以及从 trans m a(trans' :~: trans) m a 需要乘坐一部电梯。处理两种提升的单一操作更加统一。

It seems like there should be some multi-param type class,

class Monad m => MonadEmbed e m
    -- | Lift a computation from the `e` monad.
    embed :: e a -> m a

instance Monad m => MonadEmbed m m where
    embed = id

这种方法起初看起来很漂亮。但是,如果您尝试编写和使用这个 class,您将很快发现为什么所有 mtl class 都包含函数依赖:实例 MonadEmbed m m 出奇地难以选择!即使是一个非常简单的例子,比如

embed (Right ()) :: Either String ()

是歧义错误。 (毕竟,对于某些 a,我们只知道 Right 3 :: Either a () —— 我们还不知道 a ~ String,所以我们不能选择 MonadEmbed m m 实例!)我怀疑您的大多数其他实例都会 运行 陷入类似的问题。如果你添加了明显的函数依赖,你的类型推断问题就消失了,但是 fundep 检查极大地限制了你:一个人可能只能从基础 monad 中提升,而不是像你希望的那样从任意中间 monad 中提升。这在实践中是一个非常痛苦的问题(而且“mtl 方式”的痛苦是如此之小)以至于在 mtl.

中没有完成。

也就是说,您可能会喜欢使用 transformers-base 软件包。

Is it just the fact that the transformers handle all "other" effects so that the only things at the very bottom are either Identity (already handled with return :: a -> m a) or IO?

如您所说,最常见的基数是 IO(我们已经有 MonadIO)或 Identity(通常只使用 return 和纯计算而不是提升的单子计算)。有时 ST 是一个方便的基础 monad,但在 ST 上使用转换器比在 IO.

上使用它们要少见得多。

Monad... classes 似乎主要设计用于直接概括 monad 的特征操作,如果该 monad 被埋在堆栈下也可以工作的变压器。这些操作通常不是那么大的集合,例如如果需要,State 只需要 get, putstatemodify,仅此而已。

IO不是这样; MonadIO class 的大量基本 IO 操作方法是相当不切实际的。当然,如果您只是将 liftIO 作为“转换函数”引入,您当然可以获得其中的 所有 ,但我敢打赌,这一直被认为有点 hack .

此外:IO 是最重要的非平凡基础 monad;我不认为仅仅因为这个原因就给它一个专用的提升功能是不合适的。

关于你的“单次升降机就是你所需要的”的想法::~: 的问题是变压器现在形成一个二叉树而不是一个具有清晰层次结构的简单堆栈。这使得 mtl class 的整个想法变得更加有问题。