【问题标题】:Is Applicative IO implemented based on functions from Monad IO?Applicative IO 是基于 Monad IO 的函数实现的吗?
【发布时间】:2025-11-22 09:50:02
【问题描述】:

在“向您学习 Haskell for Great Good!”中作者声称Applicative IO 实例是这样实现的:

instance Applicative IO where
    pure = return
    a <*> b = do
        f <- a
        x <- b
        return (f x)

我可能错了,但似乎returndo 特定的构造(一些含糖绑定(&gt;&gt;=))都来自Monad IO。假设这是正确的,我的实际问题是:

为什么Applicative IO 的实现依赖于Monad IO 函数/组合器?

Applicative不如Monad更强大的概念吗?


编辑(一些说明):

这个实现违背了我的直觉,因为根据 Typeclassopedia 文章,给定类型必须是 Applicative before 它可以被设为 Monad(或者它应该应该 em>理论上)。

【问题讨论】:

  • @monadic:不,liftMfmapap(&lt;*&gt;)。你可能会想到liftM2,即liftM2 f x y = f &lt;$&gt; x &lt;*&gt; y
  • @camccann 是的,抱歉,我没想到 ^_^

标签: haskell monads applicative


【解决方案1】:

(...) 根据 Typeclassopedia 文章,给定类型必须是 Applicative 才能成为 Monad(或者理论上应该如此)。

是的,您的括号旁正是这里的问题。理论上,任何Monad应该也应该是Applicative,但这实际上并不是必需的,因为历史原因(即,因为Monad 存在的时间更长)。这也不是Monad 的唯一特点。

考虑相关类型类的实际定义,取自 Hackage 上的 base 包源。

这里是Applicative

class Functor f => Applicative f where
    pure  :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b
    (*>)  :: f a -> f b -> f b
    (<*)  :: f a -> f b -> f a

...我们可以观察到以下几点:

  • 给定当前存在的类型类,上下文是正确的,即它需要Functor
  • 它是根据函数应用来定义的,而不是根据提升元组(从数学的角度来看可能更自然)来定义的。
  • 它包括技术上多余的运算符,相当于提升常量函数。

同时,这里是Monad

class Monad m where
    (>>=)       :: m a -> (a -> m b) -> m b
    (>>)        :: m a -> m b -> m b
    return      :: a -> m a
    fail        :: String -> m a

...我们可以观察到以下几点:

  • 上下文不仅忽略了Applicative,还忽略了Functor,这两者在逻辑上都被Monad暗示了,但没有明确要求。
  • 它也是根据函数应用定义的,而不是使用returnjoin 进行更自然的数学定义。
  • 它包含一个技术上多余的运算符,相当于提升一个常量函数。
  • 它还包括fail,它根本不适合。

一般来说,Monad 类型类与其所基于的数学概念的不同之处可以追溯到其作为编程抽象的历史。有些,比如它与Applicative 共享的函数应用偏见,是函数式语言中存在的反映;其他人,比如fail 或缺乏适当的阶级背景,都是历史上的意外。

这一切归结为拥有Monad 的实例意味着Applicative 的实例,这反过来又意味着Functor 的实例。类上下文只是明确地形式化了这一点;无论如何,它仍然是正确的。就目前而言,给定一个Monad 实例,FunctorApplicative 都可以以完全通用的方式定义。 ApplicativeMonad 更“不那么强大”,这与它更通用的意义完全相同:如果你复制+粘贴通用实例,任何Monad 都会自动成为Applicative,但是有存在不能定义为MonadApplicative 实例。

Functor f =&gt; Applicative f 这样的类上下文说明了两件事:后者暗示了前者,并且必须存在一个定义来实现该暗示。在许多情况下,无论如何定义后者都会隐式定义前者,但编译器通常无法推断出这一点,因此需要显式写出这两个实例。使用EqOrd 可以观察到相同的情况——后者显然暗示了前者,但您仍然需要定义一个Eq 实例才能为Ord 定义一个实例。

【讨论】:

  • 很酷的答案。你认为修复这些特性(并可能破坏向后兼容性)是(不久的)将来的一种选择吗?为什么是/为什么不是?不值得付出努力吗?
  • @gorsky:向后兼容性是最大的障碍,与类型类交互方式的一般不灵活密切相关。请参阅 this question 了解为什么它很尴尬的一些讨论,包括我自己对此事的看法。
  • Monad 现在是Applicative 的子类。
【解决方案2】:

IO 类型在 Haskell 中是抽象的,所以如果你想为IO 实现一个通用的Applicative,你必须使用 IO 支持的操作来实现。因为您可以根据Monad 操作来实现Applicative,这似乎是一个不错的选择。你能想出另一种实现方式吗?

是的,Applicative 在某种意义上不如Monad 强大。

【讨论】:

  • IO 是个例外吗?这与我的直觉相反——一般来说,类型类“依赖项”的简单(不完整)图(根据 Typeclassopedia 文章)是Functor =&gt; Applicative =&gt; Monad;因此,在将给定类型作为Monad 的实例之前,它必须是Applicative。在Monad IO 方面实现Applicative IO 对我来说似乎倒退了。不过,我知道一些历史问题(例如 Applicative 类型类比 Monad 更新得多)。
  • 这是一个选择问题,但我认为这是一个不错的选择。您可以为FunctorApplicativeMonad 提供操作作为IO 的原语(因为IO 是抽象的)。或者您可以只提供Monad 操作并根据Monad 实现其他操作。后者对我来说更有意义,因为您需要提供更少的原始操作。提供尽可能强大的原语来减少数量是有意义的。
  • 啊,现在我明白了。我认为Functor =&gt; Applicative =&gt; Monad 依赖会强制执行特定的顺序(即我必须首先实现Functor,然后是Applicative,然后是Monad),但实际上以不同的方式执行它是有意义的。我的立场是正确的。
  • @augustss:尽管在某些情况下,(&lt;*&gt;) 可以比使用(&gt;&gt;=) 的通用实现更有效。无限流就是一个明显的例子,其中泛型ap 必须构造笛卡尔积然后取对角线,而(&lt;*&gt;) 可以简单地表现得像zip
【解决方案3】:

Applicative 是不是比 Monad 更强大?

是的,因此无论何时您拥有Monad,您都可以将其设为Applicative。您可以在示例中将 IO 替换为任何其他 monad,这将是一个有效的 Applicative 实例。

打个比方,虽然彩色打印机可能被认为比灰度打印机更强大,但您仍然可以使用它来打印灰度图像。

当然,也可以将Monad 实例基于Applicative 并设置return = pure,但一般无法定义&gt;&gt;=。这就是Monad 更强大的意思。

【讨论】:

  • 是的,我知道Applicative Monad 关系。我担心Applicative =&gt; Monad 依赖项迫使我在实施实例时坚持给定的顺序(首先是Applicative,然后是Monad),但这是错误的假设(请查看augustss 答案上方的cmets)。无论如何,谢谢,顺便说一句 - 很酷的打印机类比;)
【解决方案4】:

perfect world 中,每个Monad 都是Applicative(所以我们有class Applicative a =&gt; Monad a where ...),但由于历史原因,两个类型类都是独立的。所以你观察到这个定义有点“倒退”(使用更强大的抽象来实现不那么强大的抽象)是正确的。

【讨论】:

  • 消除这些怪癖会很酷;) Haskell 的生态系统似乎足够动态。
  • 从去年的某个时候开始,新版本的 GHC 已经有了 class Applicative m =&gt; Monad m,但是您仍然可以使用 monad 函数来定义应用实例,因为 GHC 在尝试编译它们之前会检查这两个实例是否确实存在
【解决方案5】:

对于旧版本的 GHC,您已经有了非常好的答案,但在最新版本中,您实际上确实有 class Applicative m =&gt; Monad m,所以您的问题需要另一个答案。

在 GHC 实现方面:GHC 只是在尝试编译任何实例之前检查为给定类型定义了哪些实例。

就代码语义而言:class Applicative m =&gt; Monad m 并不意味着必须“首先”定义 Applicative 实例,只是如果在程序结束时尚未定义它,则编译器将中止。

【讨论】:

    最近更新 更多