为什么 Kleisli 不是 Monoid 的实例?

Why isn't Kleisli an instance of Monoid?

如果您希望追加两个类型为 (a -> m b) 的函数,以便您只得到一个相同类型的函数追加两个结果,您可以使用 Kleisli 来实现:

instance (Monad m, Monoid b) => Monoid (Kleisli m a b) where
    mempty = Kleisli (\_ -> return mempty)
    mappend k1 k2 =
        Kleisli g
            where
                g x = do
                    r1 <- runKleisli k1 x
                    r2 <- runKleisli k2 x
                    return (r1 <> r2)

但是,目前Control.Arrow中没有定义这样的实例。 通常,在 Haskell 中,我怀疑有一个很好的理由,但找不到哪个。

备注

这个问题和this one很相似。但是,对于 Monoid,我看不到定义实例的方法,例如:

instance (Monad m, Monoid b) => Monoid (a -> m b) where
    [...]

因为已经有一个实例:

instance Monoid b => Monoid (a -> b) where
    [...]

在图书馆设计业务中,我们在这里面临一个选择点,我们选择在我们的集体政策中不完全一致(或不一致)。

Monoid Monad(或Applicative)类型构造函数的实例可以以多种方式出现。 Pointwise lifting 总是可用的,但我们没有定义

instance (Applicative f, Monoid x) => Monoid (f x) {- not really -} where
  mempty         = pure mempty
  mappend fa fb  = mappend <$> fa <*> fb

请注意,instance Monoid (a -> b) 就是这样的逐点提升,因此只要 m b 的幺半群实例对 (a -> m b) 的幺半群进行逐点提升,就会发生 (a -> m b) 的逐点提升 b.

我们一般不做pointwise lifting,不仅因为它会阻止其他Monoid实例的载体恰好是应用类型,而且因为f的结构通常是被认为比 x 更重要。一个关键的例子是 free 幺半群,更广为人知的是 [x],它是 [](++)Monoid,而不是逐点提升。幺半群结构来自列表包装,而不是来自包装的元素。

我的首选经验法则确实是优先考虑类型构造函数中固有的幺半群结构,而不是逐点提升或特定类型实例化的幺半群结构,例如 a -> a 的组合幺半群。这些可以而且确实得到 newtype 包装。

关于 Monoid (m x) 是否应该与 MonadPlus m 同时存在的争论爆发了(并且与 Alternative 类似)。我的感觉是,唯一好的 MonadPlus 实例是 Monoid 实例的副本,但其他实例不同。尽管如此,图书馆在这件事上并不一致,尤其是在(许多读者会看到我的这个老笨蛋来了)......

...Maybe 的幺半群实例,它忽略了我们通常使用 Maybe 来模拟可能的故障这一事实,而是观察到相同的数据类型想法,即额外插入一个element 可以用来给半群一个中性元素,如果它还没有的话。这两个结构产生了同构类型,但它们在概念上并不相关。 (编辑 更糟糕的是,这个想法实现得很笨拙,给实例一个 Monoid 约束,而只需要一个 Semigroup。我想看看Semigroup-extends-to-Monoid 想法实现了,但是 没有 Maybe。)

特别是回到 Kleisli,我们有三个明显的候选实例:

  1. Monoid (Kleisli m a a)return 和 Kleisli 组合
  2. MonadPlus m => Monoid (Kleisli m a b) 逐点提升 mzeromplus ->
  3. Monoid b => Monoid (Kleisli m a b) 提升 b 的幺半群结构超过 m 然后 ->

我希望没有做出任何选择,只是因为不清楚做出哪个选择。我犹豫要不要这么说,但我的投票是 2,优先考虑来自 Kleisli m a 的结构而不是来自 b.

的结构