被“ Alternative”类型类的含义及其与其他类型类的关系弄糊涂了

我一直在通过 典型的经典百科全书学习类型课程。我卡住了理解 Alternative(和 MonadPlus,就此而言)。

我现在的问题是:

  • Wikipedia 说,“ Alternative 类是针对 Applicative 函子的,它也有一个幺半群结构。”我不明白 Alternative 不是和 Monoid 完全不同吗?也就是说,我理解 Alternative 类型类的要点是在两个事物之间进行选择,而我理解 Monoid 是关于组合事物的。

  • 为什么 Alternative 需要 empty方法/成员?我可能是错的,但它似乎没有被使用在所有... 至少在 密码,我可以找到。它似乎不符合这门课的主题——如果我有两样东西,并且需要选择其中一样,那么我为什么需要一个“ em pty”呢?

  • 为什么 Alternative 类型类需要 Applicative 约束,为什么它需要一种 * -> *?为什么不只有 <|> :: a -> a -> a?所有的实例仍然可以用同样的方式实现... ... 我想(不确定)。它能提供什么 Monoid 没有的价值?

  • MonadPlus类型类的意义是什么?难道我不能通过同时使用 MonadAlternative来解锁它所有的优点吗?为什么不扔了它?(我确信我错了,但我没有任何反例)

希望所有这些问题都是连贯的... !


赏金更新:@Antal 的回答是一个很好的开始,但 Q3仍然开放: Alternative 提供了什么 Monoid 没有的东西?我认为 这个答案不能令人满意,因为它缺乏具体的例子,而且我们还具体讨论了 Alternative 的更高的善意是如何将它与 Monoid 区分开来的。

如果要把应用效应和 Monoid 的行为结合起来,为什么不只是:

liftA2 mappend

这对我来说更加令人困惑,因为许多 Monoid 实例与 Alternative 实例完全相同。

这就是为什么我在寻找 具体例子,它展示了为什么 Alternative 是必要的,以及它与 Monoid 如何不同——或意味着什么不同。

9763 次浏览

首先,让我简短地回答以上每一个问题。然后我将把每个问题扩展成一个更长的详细答案,但是这些简短的答案有望帮助我们找到答案。

  1. 不,AlternativeMonoid并不意味着不同的东西; Alternative是指那些具有 ApplicativeMonoid两种结构的类型。“挑选”和“组合”是同一个更广泛的概念的两种不同的直觉。

  2. Alternative包含 empty<|>,因为设计者认为这将是有用的,因为这产生了一个幺半群。在采摘方面,empty相当于做出一个不可能的选择。

  3. 我们既需要 Alternative又需要 Monoid,因为前者遵循(或应该遵循) 更多定律,这些定律关系到型别构造器的一元化和应用结构。此外,Alternative不能依赖于内部类型,而 Monoid可以。

  4. MonadPlus略强于 Alternative,因为它必须遵守更多的定律; 这些定律除了应用结构之外,还将单体结构与单体结构联系起来。如果两者都有,那么它们应该是一致的。


难道 Alternative的意思和 Monoid完全不同吗?

没有!您感到困惑的部分原因是 HaskellMonoid类使用了一些非常糟糕(嗯,不够一般)的名称。这就是数学家如何定义幺半群(非常明确) :

定义。Monoid是一个集合 M,具有区分元 ΕM和二元算子 · : M × MM,用并置表示,使得以下两个条件成立:

  1. Ε 是恒等式: 对于所有的 MΕ = Ε =
  2. · 是结合的: 对于所有 1,2,3∈ M,(12) 3 = 1(23)。

就是这样。在哈斯克尔,ε 的拼写是 mempty,而 · 的拼写是 mappend(或者现在是 <>) ,而集合 Minstance Monoid M where ...中的类型 M

看看这个定义,我们发现它没有提到“组合”(或者说是“选择”)。它说的是关于 · 和 Ε的事情,但仅此而已。现在,这个结构确实可以很好地组合事物: Ε对应于没有事物,而 12说,如果我把 1和 2的东西组合在一起,我可以得到一个包含它们所有东西的新东西。但这里有一个替代的直觉: Ε对应于根本没有选择,而 12对应于 1和 2之间的选择。这就是“挑选”的直觉。注意,两者都遵循幺半群定律:

  1. 一无所有和别无选择都是身份。
    • 如果我没有什么东西,然后用一些东西把它们粘在一起,我最后又会得到同样的东西。
    • 如果我要在完全没有选择(不可能的事情)和其他选择之间做出选择,我必须选择其他(可能的)选择。
  2. 把收藏品放在一起和做出选择都是相关联的。
    • 如果我有三个收藏品,不管我是把前两个放在一起,还是把第三个放在一起,或者把最后两个放在一起,然后放在第一个,都没有关系; 不管怎样,我最终得到的总收藏品是一样的。
    • 如果我要在三件事情中做出选择,无论我(a)首先在第一或第二和第三之间做出选择,如果需要的话,在第一和第二之间做出选择,或者(b)首先在第一和第二或第三之间做出选择,如果需要的话,在第二和第三之间做出选择,都没有关系。不管怎样,我都可以选择我想要的。

(注意: 我在这里玩忽职守,这就是为什么这是直觉。例如,重要的是要记住 · 不需要是可交换的,上面忽略了这一点: 12≠ 21是完全可能的。)

请看: 这两种事情(还有其他很多事情ーー乘数真的是“组合”或者“选择”吗?)遵守同样的规则。有一个直觉是重要的,以发展理解,但它的规则和定义,决定什么是 事实上正在进行。

最棒的是,这两种直觉都可以由同一个载体来解释!让 M是一些集合(而不是一组 所有集合!)设 Ε为空集 something,设 · 为联合∪。不难看出 something 是∪的一个恒等式,∪是结合的,因此我们可以得出(M,something,∪)是幺半群的结论。现在:

  1. 如果我们认为集合是事物的集合,那么∪相当于把它们放在一起以获得更多的事物ーー“结合”的直觉。
  2. 如果我们认为集合代表可能的行动,那么∪就相当于增加了你可以从中挑选的可能行动的数量ーー“挑选”的直觉。

这正是哈斯克尔的 []所发生的情况: [a]是所有 aMonoid,而 []作为应用函子(和 monad)被用来表示非确定性。这两个组合和拣选直觉在同一类型一致: mempty = empty = []mappend = (<|>) = (++)

所以 Alternative类只是用来表示对象,这些对象(a)是应用函数,(b)当在类型上实例化时,它们上面有一个值和一个二进制函数,这些函数遵循一些规则。什么规矩?Monoid 规则。为什么?因为事实证明它是有用的: -)


为什么 Alternative需要一个空的方法/成员?

尖刻的回答是“因为 Alternative表示一个幺半群结构。”但真正的问题是: 为什么是一个幺半群结构吗?为什么不只是一个半群,一个没有 Ε的幺半群?一个答案是声称幺半群只是更有用。我认为许多人(但可能不是 Edward Kmett)会同意这一点; 几乎所有时候,如果你有一个合理的 (<|>)/mappend/· ,你就能够定义一个合理的 empty/mempty/Ε。另一方面,有额外的通用性是很好的,因为它可以让你把更多的东西放在伞下。

你也想知道这与“挑选”的直觉有什么关系。记住,在某种意义上,正确的答案是“知道什么时候放弃‘挑选’的直觉,”我认为你 可以统一了这两个。考虑 [],非确定性的应用函子。如果我将两个类型为 [a](<|>)的值结合起来,那就相当于不确定地从左边选择一个动作或者从右边选择一个动作。但是有时候,你一方面没有可能的行动ーー这没关系。类似地,如果我们考虑解析器,(<|>)表示一个解析器,它解析左边或右边的内容(它“选择”)。如果您有一个总是失败的解析器,那么它最终将成为一个标识: 如果您选择了它,您将立即拒绝该选择,并尝试另一个。

所有这一切说,记住,它是完全有可能有一个类似于 Alternative,但缺乏 empty类。这将是完全有效的ーー它甚至可能是 Alternative的一个超类ーー但碰巧不是 Haskell 所做的。大概这是对什么是有用的猜测。


为什么 Alternative类型类需要一个 Applicative约束,为什么它需要一种 * -> *?... 为什么不直接[使用] liftA2 mappend呢?

那么,让我们考虑这三个提议的改变: 去除 AlternativeApplicative约束; 改变 Alternative的参数种类; 使用 liftA2 mappend而不是 <|>pure mempty而不是 empty。我们首先来看第三个变化,因为它是最不同的。假设我们完全去掉了 Alternative,并用两个普通函数替换了这个类:

fempty :: (Applicative f, Monoid a) => f a
fempty = pure mempty


(>|<) :: (Applicative f, Monoid a) => f a -> f a -> f a
(>|<) = liftA2 mappend

我们甚至可以保留 somemany的定义。这个 是的给了我们一个幺半群结构,这是真的。但似乎给了我们错误的答案。既然 (a,a) -> a不是 Monoid的实例,那么 Just fst >|< Just snd应该失败吗?没有,但是这就是上面的代码会导致的结果。我们 想要的幺半群实例是一个内部类型不可知论者(借用 many1中 many0的术语,它提出了一些非常相似的问题) ; Alternative结构是关于由 f的结构决定的幺半群,而不是 f论证的结构。

现在我们认为我们希望将 Alternative作为某种类型类保留下来,让我们看一下提出的两种更改它的方法。如果我们改变类型,我们 去掉 Applicative约束; Applicative只讨论类型 * -> *的东西,所以没有办法引用它。这就留下了两个可能的改变: 第一个更小的改变是去掉 Applicative约束,但不要去碰这种约束:

class Alternative' f where
empty' :: f a
(<||>) :: f a -> f a -> f a

另一个更大的变化是,摆脱 Applicative约束,改变类型:

class Alternative'' a where
empty'' :: a
(<|||>) :: a -> a -> a

在这两种情况下,我们都必须去掉 some/many,但这没关系; 我们可以将它们定义为具有 (Applicative f, Alternative' f) => f a -> f [a](Applicative f, Alternative'' (f [a])) => f a -> f [a]类型的独立函数。

现在,在第二种情况下,我们改变类型变量的类型,我们看到我们的类与 Monoid完全相同(或者,如果你仍然想删除 empty''Semigroup) ,所以有一个单独的类没有任何好处。事实上,即使我们放弃 kind 变量而去掉了 Applicative的约束,Alternative也只是变成了 forall a. Monoid (f a),尽管我们不能在哈斯克尔写出这些量化的约束,甚至不能用所有漂亮的 GHc 扩展。(注意,这表达了上面提到的内在类型不可知论。)因此,如果我们能够做出这些改变中的任何一个,那么我们就没有理由保留 Alternative(除非能够表达量化的约束,但这似乎很难令人信服)。

因此,问题归结为“ AlternativefApplicative部分之间是否存在关系,f是两者的实例?”虽然文档中没有任何内容,但我要表明立场,说 是的ーー或者至少有 应该。我认为 Alternative应该遵守一些与 Applicative相关的定律(除了幺半群定律之外) ; 特别是,我认为这些定律类似于

  1. 右分布(<*>) : (f <|> g) <*> a = (f <*> a) <|> (g <*> a)
  2. 右吸收(对于 <*>) : empty <*> a = empty
  3. 左分布(fmap) : f <$> (a <|> b) = (f <$> a) <|> (f <$> b)
  4. 左吸收(对于 fmap) : f <$> empty = empty

这些定律似乎适用于 []Maybe,以及(假设它的 MonadPlus实例是 Alternative实例) IO,但我还没有做任何证明或详尽的测试。(例如,我最初认为 左边的分布性适用于 <*>,但这种“执行效果”的顺序对于 []是错误的。)然而,通过类比的方式,MonadPlus确实应该遵守类似的规律(尽管 很明显,关于哪一个有些模棱两可)。我最初想要提出第三条法则,这似乎很自然:

  • 左吸收(对于 <*>) : a <*> empty = empty

然而,尽管我相信 []Maybe遵守这个法则,但是 IO不遵守,而且我认为(原因将在接下来的几段中显而易见)最好不要要求它。

事实上,爱德华 · 克米特 有一些幻灯片也持有类似的观点,为了深入了解这一点,我们需要简短地离题,使用一些更多的数学术语。最后一张幻灯片,“我想要更多的结构,”上面写着“一个 Monoid 对于一个 Applicative 来说就像一个右半环对于一个 Alternative 来说一样,”还有“如果你扔掉了一个 Applicative 的参数,你就得到了一个 Monoid,如果你扔掉了另一个 Alternative 的参数,你就得到了一个右半环。”

正确的耳环? “正确的耳环怎么会在里面?”

定义。A 右半环(也是 正确的耳环,但前者似乎在 Google 上使用得更多)是一个四元组(R,+ ,· ,0) ,其中(R,+ ,0)是一个幺半群,(R,·)是一个半群,以下两个条件成立:

  1. · 对于所有 R是的TR,(是的 + T) R = 长官 + Tr
  2. 0对于 · : 对于所有的 RR,0R = 0是右吸收的。

左半环是类似地定义的。

现在,这并不完全有效,因为 <*>并不是真正的关联运算符或二进制运算符ーー类型不匹配。我认为这就是爱德华 · 克米特(Edward Kmett)谈到“抛弃争论”时想要表达的意思另一种选择可能是说(我不确定这是否正确) ,我们实际上希望(f a<|><*>empty)形成一个 f a0,其中“-oid”后缀表示二进制运算符只能应用于特定的元素对(à la f a1)。我们还想说(f a<|><$>empty)是一个左近半圆形,尽管这可以想象地遵循 Applicative定律和右近半圆形结构的组合。但是现在我已经不知所措了,反正这也没什么关系。

无论如何,这些定律,作为 更强大比幺半群定律,意味着完全有效的 Monoid实例将成为无效的 Alternative实例。在标准库中(至少)有两个这样的例子: Monoid a => (a,)Maybe。让我们快速浏览一下。

给定任意两个幺半群,它们的产品是幺半群; 因此,元组可以用显而易见的方式(重新格式化 基地包裹的来源)作为 Monoid的一个实例:

instance (Monoid a, Monoid b) => Monoid (a,b) where
mempty = (mempty, mempty)
(a1,b1) `mappend` (a2,b2) = (a1 `mappend` a2, b1 `mappend` b2)

类似地,我们可以通过累加 monoid 元素(重新格式化 基地包裹的来源) ,将第一个组件是 monoid 元素的元组变成 Applicative的实例:

instance Monoid a => Applicative ((,) a) where
pure x = (mempty, x)
(u, f) <*> (v, x) = (u `mappend` v, f x)

然而,元组不是 Alternative的一个实例,因为它们不可能是ーー Monoid a => (a,b)上的单峰结构不存在于所有类型的 b中,而且 Alternative的单峰结构必须是内部类型不可知的。b不仅必须是一个单子,才能表示 (f <> g) <*> a,我们还需要使用函数的 Monoid实例,这个实例用于形式为 Monoid b => a -> b的函数。即使在我们有所有必要的一元结构的情况下,它也违反了 Alternative定律的 Monoid a => (a,b)3。要看到这一点,让 ssf n = (Sum n, (<> Sum n))Monoid a => (a,b)0。然后,为 Monoid a => (a,b)2编写 Monoid a => (a,b)1,我们得到以下结果(可以在 GHCi 中检查,偶尔使用类型注释) :

  1. 正确的分配:
    • (ssf 1 <> ssf 1) <*> ssn 1 = (Sum 3, Sum 4)
    • (ssf 1 <*> ssn 1) <> (ssf 1 <*> ssn 1) = (Sum 4, Sum 4)
  2. 右吸收:
    • mempty <*> ssn 1 = (Sum 1, Sum 0)
    • mempty = (Sum 0, Sum 0)
  3. 左派分配:
    • (<> Sum 1) <$> (ssn 1 <> ssn 1) = (Sum 2, Sum 3)
    • ((<> Sum 1) <$> ssn 1) <> ((<> Sum 1) <$> ssn 1) = (Sum 2, Sum 4)
  4. 左吸收:
    • (<> Sum 1) <$> mempty = (Sum 0, Sum 1)
    • mempty = (Sum 1, Sum 1)

接下来,考虑 Maybe。目前,MaybeMonoidAlternative实例 不同意。(虽然我在本节开头提到的 Haskell 和咖啡馆的讨论建议改变这一点,但是 Maybe0也会产生同样的效果。)作为 MonoidMaybe通过使用 Nothing作为标识将半群提升为幺半群; 由于基本包没有半群类,它只提升幺半群,所以我们得到(重新格式化 Maybe1) :

instance Monoid a => Monoid (Maybe a) where
mempty = Nothing
Nothing `mappend` m       = m
m       `mappend` Nothing = m
Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)

另一方面,作为一个 AlternativeMaybe代表优先选择与失败,因此我们得到(再次重新格式化 基地包裹的来源) :

instance Alternative Maybe where
empty = Nothing
Nothing <|> r = r
l       <|> _ = l

结果表明,只有后者满足 Alternative定律。Monoid实例的失败没有 (,)实例严重; 它 Monoid1遵守与 <*>相关的规律,尽管几乎是偶然的ーー它来自函数的唯一 Monoid实例的行为,它(如上所述)将返回幺半群的函数提升为读者应用函数。如果你算出来(这是非常机械的) ,你会发现 <*>的右分布率和右吸收率对 AlternativeMonoid实例都适用,对 fmap的左吸收率也适用。fmap的左分布率确实适用于 Alternative实例,如下所示:

f <$> (Nothing <|> b)
= f <$> b                          by the definition of (<|>)
= Nothing <|> (f <$> b)            by the definition of (<|>)
= (f <$> Nothing) <|> (f <$> b)    by the definition of (<$>)


f <$> (Just a <|> b)
= f <$> Just a                     by the definition of (<|>)
= Just (f a)                       by the definition of (<$>)
= Just (f a) <|> (f <$> b)         by the definition of (<|>)
= (f <$> Just a) <|> (f <$> b)     by the definition of (<$>)

但是,对于 Monoid实例,它失败了; 为 mappend编写 (<>)时,我们有:

  • (<> Sum 1) <$> (Just (Sum 0) <> Just (Sum 0)) = Just (Sum 1)
  • ((<> Sum 1) <$> Just (Sum 0)) <> ((<> Sum 1) <$> Just (Sum 0)) = Just (Sum 2)

现在,这个例子有一个警告。如果您只需要 Alternative<*>兼容,而不是与 <$>兼容,那么 Maybe就可以了。上面提到的 Edward Kmett 的幻灯片并没有提到 <$>,但是我认为要求有关它的法律也是合理的; 尽管如此,我还是找不到任何东西来支持我的观点。

因此,我们可以得出结论,作为一个 Alternative是一个 更强大的要求比作为一个 Monoid,因此它需要一个不同的类。最纯粹的例子是一个内部类型不可知的 Monoid实例和一个 Applicative实例,它们彼此不兼容; 然而,在基本包中没有任何这样的类型,我想不出任何类型。(有可能根本不存在,尽管我会感到惊讶。)尽管如此,这些内部类型 gnotic 示例说明了为什么这两个类型类必须是不同的。


MonadPlus类型类的意义是什么?

Alternative一样,MonadPlusMonoid的强化,但是相对于 Monad而言是 Applicative。根据 Edward Kmett 在 Monad4中对问题 Monad5的解释,MonadPlusAlternative强: 例如,定律 Alternative0并不意味着 Alternative1。Monad7提供了这方面的两个例子: Alternative2及其对偶。这个问题是复杂的事实,有 Monad8。人们普遍认为 MonadPlus应该与 Alternative5和 Alternative6形成幺半群,并满足 Monad9定律 Alternative7。然而,有些 MonadPluses 满足 Applicative0、 Alternative9,有些则满足 Applicative1、 Monoid0。(注意,MonadPlus的左零/分布类似于 Alternative的右分布/吸收; Monoid3比 Monoid5更类似于 Monoid4。)左分布可能是“更好的”,因此任何满足左捕获的 MonadPlus实例,如 Alternative2,都是 Alternative,但不是第一种 MonadPlus。由于左捕获依赖于排序,您可以想象一个 Alternative2的新类型包装器,它的 Alternative实例是 Applicative2偏向的,而不是 Applicative3偏向的: Monad2。这既不能满足左分布,也不能满足左捕获,而是一个完全有效的 Alternative

然而,由于任何类型的 a MonadPlus应该有它的实例与它的 Alternative实例一致(我相信这是必需的,就像它要求 ap(<*>)是相等的 Monads 是 Applicatives 一样) ,你可以想象定义 MonadPlus类,而不是

class (Monad m, Alternative m) => MonadPlus' m

这个类不需要声明新的函数; 它只是一个关于给定类型的 empty(<|>)遵循的规则的承诺。这种设计方法在哈斯克尔的标准图书馆中没有使用,但是在一些更具有数学思维的软件包中用于类似的目的,例如,格子软件包使用它来表达这样一种想法,即 格子仅仅是由吸收定律连接的同一类型的 加入半格遇到半格

对于 Alternative,即使你想保证 AlternativeMonoid总是重合,你也不能做同样的事情,这是因为这种不匹配。所需的类声明将具有

class (Applicative f, forall a. Monoid (f a)) => Alternative''' f

但是(如上所述)甚至 GHC Haskell 都不支持量化约束。

另外,请注意,将 Alternative作为 MonadPlus的超类需要 Applicative作为 Monad的超类,所以祝你好运。如果您遇到这个问题,总是有 WrappedMonad新类型,它以显而易见的方式将任何 Monad转换为 Applicative; 还有一个 instance MonadPlus m => Alternative (WrappedMonad m) where ...,它完全符合您的预期。

我理解 Alternative 类型类的要点是在两个事物之间进行选择,而我理解 Monoid 是关于组合事物的。

如果你仔细想想,它们是一样的。

+组合了一些东西(通常是数字) ,它的类型签名是 Int -> Int -> Int(或者其他什么)。

<|>操作符在替代品之间进行选择,它的类型签名也是相同的: 获取两个匹配的对象并返回一个组合的对象。

摘要

  • 我们需要为一些应用函数定义(实例提供相同的操作) Monoid 实例,这些实例在应用函数级别上真正地结合,而不仅仅是提升较低级别的 Monoid。下面来自 litvar = liftA2 mappend literal variable的示例错误表明,<|>通常不能被定义为 liftA2 mappend; 在这种情况下,<|>通过组合解析器而不是它们的数据来工作。

  • 如果我们直接使用 Monoid,我们需要语言扩展来定义实例。Alternative的类型更高,因此您可以在不需要语言扩展的情况下创建这些实例。

示例: 解析器

让我们假设我们正在解析一些声明,因此我们导入需要的所有内容

import Text.Parsec
import Text.Parsec.String
import Control.Applicative ((<$>),(<*>),liftA2,empty)
import Data.Monoid
import Data.Char

然后考虑如何解析一个类型,我们选择简单化:

data Type = Literal String | Variable String  deriving Show
examples = [Literal "Int",Variable "a"]

现在让我们为文字类型编写一个解析器:

literal :: Parser Type
literal = fmap Literal $ (:) <$> upper <*> many alphaNum

意义: 解析一个 upper大小写字符,然后解析 many alphaNumeric 字符,将结果组合成一个带有纯函数 (:)的字符串。然后,应用纯功能 Literal将这些 String转化为 Type。我们将以完全相同的方式解析变量类型,除了以 lower大小写字母开头:

variable :: Parser Type
variable = fmap Variable $ (:) <$> lower <*> many alphaNum

很好 parseTest literal "Bool" == Literal "Bool"正如我们所希望的。

问题3a: 如果要把应用效果和 Monoid 的行为结合起来,为什么不只是 liftA2 mappend

编辑: 哎呀——忘记实际使用 <|>了!
现在,让我们使用 Alternative 组合这两个解析器:

types :: Parser Type
types = literal <|> variable

这可以解析任何类型: parseTest types "Int" == Literal "Bool"parseTest types "a" == Variable "a"。 这是两个 解析器的组合,而不是两个 价值观的组合。这就是它在应用函数级别而不是数据级别工作的意义。

然而,如果我们尝试:

litvar = liftA2 mappend literal variable

那就是要求编译器在数据级别上组合它们生成的两个 价值观。 我们得到

No instance for (Monoid Type)
arising from a use of `mappend'
Possible fix: add an instance declaration for (Monoid Type)
In the first argument of `liftA2', namely `mappend'
In the expression: liftA2 mappend literal variable
In an equation for `litvar':
litvar = liftA2 mappend literal variable

因此,我们发现了第一件事; Alternative 类与 liftA2 mappend完全不同,因为它在不同的层次上组合对象——它组合解析器,而不是解析数据。如果你愿意这样想,它是真正更高级别的组合,而不仅仅是一个电梯。我不喜欢这么说,因为 Parser Type有类似 *的东西,但我们确实是在结合 Parser而不是 Type

(即使对于具有 Monoid 实例的类型,liftA2 mappend也不会提供与 <|>相同的解析器。如果您在 Parser String上尝试它,您将得到一个接一个解析然后连接的 liftA2 mappend,而 <|>将尝试第一个解析器,如果失败则默认为第二个解析器。)

问题3b: Alternative 的 <|> :: f a -> f a -> f a和 Monoid 的 mappend :: b -> b -> b有什么不同?

首先,您应该注意到它并没有在 Monoid 实例上提供新的功能。

然而,其次,直接使用 Monoid 存在一个问题: 让我们尝试在解析器上使用 mappend,同时展示它与 Alternative的结构相同:

instance Monoid (Parser a) where
mempty = empty
mappend = (<|>)

哎呀! 我们得到

Illegal instance declaration for `Monoid (Parser a)'
(All instance types must be of the form (T t1 ... tn)
where T is not a synonym.
Use -XTypeSynonymInstances if you want to disable this.)
In the instance declaration for `Monoid (Parser a)'

因此,如果有一个应用函数 f,那么 Alternative实例显示 f a是一个 monoid,但是只能将它声明为带语言扩展名的 Monoid

一旦我们将 {-# LANGUAGE TypeSynonymInstances #-}添加到文件的顶部,我们就可以定义

typeParser = literal `mappend` variable

令我们高兴的是,它的工作原理: parseTest typeParser "Yes" == Literal "Yes"parseTest typeParser "a" == Literal "a"

即使您没有任何同义词(ParserString是同义词,所以它们不存在) ,您仍然需要 {-# LANGUAGE FlexibleInstances #-}来定义类似于下面这个实例的实例:

data MyMaybe a = MyJust a | MyNothing deriving Show
instance Monoid (MyMaybe Int) where
mempty = MyNothing
mappend MyNothing x = x
mappend x MyNothing = x
mappend (MyJust a) (MyJust b) = MyJust (a + b)

(可能的 monoid 实例通过提升底层 monoid 来绕过这个问题。)

使标准库不必要地依赖于语言扩展显然是不可取的。


这就是了。Alternative 是应用函子的幺半群(不仅仅是幺半群的一个提升)。它需要更高级的类型 f a -> f a -> f a,这样您就可以定义一个不需要语言扩展的类型。

其他问题:

  1. 为什么 Alternative 需要一个空的方法/成员?
    因为拥有操作的标识有时是有用的。 例如,您可以定义 anyA = foldr (<|>) empty而不使用冗长的边界条件。

  2. MonadPlus 类型类的意义何在?难道我不能用 Monad 和 Alternative 来释放它所有的优点吗? 不,我建议你回到 和你有关的问题:

此外,即使 Applicative 是 Monad 的一个超级类,你最终还是会需要 MonadPlus 类,因为严格遵守 empty <*> m = empty并不足以证明 empty >>= f = empty

我举了个例子,也许吧。我详细解释了,并用 这个答案证明了安塔尔的问题。为了得到这个答案,值得注意的是我能够使用 > > = 使 MonadPlus 实例违反了 Alternative 法则。


幺半群结构是有用的。替代是为应用函子提供它的最好方法。

我不会报道 MonadPlus,因为它的法律存在分歧。


在尝试并且找不到任何有意义的例子,在这些例子中 Applicative 的结构自然地导致 Alternative 实例不同意它的 Monoid 实例 * 之后,我最终想出了这个:

Alternative 定律比 Monoid 定律更严格,因为结果 不能依赖于内部类型。这就排除了大量 Monoid 实例作为 Alternative 的可能性。 这些数据类型允许部分(意味着它们只对某些内部类型起作用) Monoid 实例,这是 * -> *类型的额外“结构”所禁止的。例子:

  • Monoid 的标准“也许”实例假定内部类型为 Monoid = > not an Alternative

  • ZipList、 tuple 和函数都可以是 Monoid,如果它们的内部类型是 Monoid = > not Alternative

  • 至少有一个元素的序列不能是替代序列,因为没有 empty:

    data Seq a
    = End a
    | Cons a (Seq a)
    deriving (Show, Eq, Ord)
    

On the other hand, some data types cannot be made Alternatives because they're *-kinded:

  • unit -- ()
  • Ordering
  • numbers, booleans

My inferred conclusion: for types that have both an Alternative and a Monoid instance, the instances are intended to be the same. See also this answer.


excluding Maybe, which I argue doesn't count because its standard instance should not require Monoid for the inner type, in which case it would be identical to Alternative

import Data.Monoid
import Control.Applicative

让我们跟踪 Monoid 和 Alternative 如何与 Maybe函数和 ZipList函数交互的例子,但让我们从头开始,部分是为了让所有的定义在我们的脑海中新鲜,部分是为了停止切换标签到位的黑客所有的时间,但主要是为了我可以 去总部查查纠正我的输入错误!

(<>) :: Monoid a => a -> a -> a
(<>) = mappend -- I'll be using <> freely instead of `mappend`.

这是“也许”的克隆:

data Perhaps a = Yes a | No  deriving (Eq, Show)


instance Functor Perhaps where
fmap f (Yes a) = Yes (f a)
fmap f No      = No


instance Applicative Perhaps where
pure a = Yes a
No    <*> _     = No
_     <*> No    = No
Yes f <*> Yes x = Yes (f x)
   

现在是 ZipList:

data Zip a = Zip [a]  deriving (Eq,Show)


instance Functor Zip where
fmap f (Zip xs) = Zip (map f xs)


instance Applicative Zip where
Zip fs <*> Zip xs = Zip (zipWith id fs xs)   -- zip them up, applying the fs to the xs
pure a = Zip (repeat a)   -- infinite so that when you zip with something, lengths don't change

结构1: 组合元素: 幺半群

也许是克隆人

首先让我们看看 Perhaps String。有两种组合它们的方法。首先是连接

(<++>) :: Perhaps String -> Perhaps String -> Perhaps String
Yes xs <++> Yes ys = Yes (xs ++ ys)
Yes xs <++> No     = Yes xs
No     <++> Yes ys = Yes ys
No     <++> No     = No

串联通过将 No当作 Yes []来处理,本质上是在字符串级别工作,而不是真正的或许级别。等于 liftA2 (++)。这是明智的和有用的,但也许我们可以推广从只使用 ++使用任何方式的组合-任何 Monoid 然后!

(<++>) :: Monoid a => Perhaps a -> Perhaps a -> Perhaps a
Yes xs <++> Yes ys = Yes (xs `mappend` ys)
Yes xs <++> No     = Yes xs
No     <++> Yes ys = Yes ys
No     <++> No     = No

Perhaps的这种幺半群结构试图在 a级别上尽可能多地工作。请注意 Monoid a约束,它告诉我们正在使用来自 a级别的结构。这不是一个替代结构,它是一个派生(提升) Monoid 结构。

instance Monoid a => Monoid (Perhaps a) where
mappend = (<++>)
mempty = No

在这里,我使用数据 a 的结构来为整个事情添加结构。如果我组合了 Set,我就能够添加一个 Ord a上下文。

ZipList 克隆

那么我们应该如何组合 元素和 zipList? 如果我们组合它们,这些 zip 应该是什么?

   Zip ["HELLO","MUM","HOW","ARE","YOU?"]
<> Zip ["this", "is", "fun"]
=  Zip ["HELLO" ? "this",   "MUM" ? "is",   "HOW" ? "fun"]


mempty = ["","","","",..]   -- sensible zero element for zipping with ?

但是对于 ?我们应该使用什么。我说唯一明智的选择是 ++。实际上,对于列表,是 (<>) = (++)

   Zip [Just 1,  Nothing, Just 3, Just 4]
<> Zip [Just 40, Just 70, Nothing]
=  Zip [Just 1 ? Just 40,    Nothing ? Just 70,    Just 3 ? Nothing]


mempty = [Nothing, Nothing, Nothing, .....]  -- sensible zero element

但是我们可以用什么来表示 ?呢? 我说我们要组合元素,所以我们应该再次使用 Monoid 中的元素组合运算符: <>

instance Monoid a => Monoid (Zip a) where
Zip as `mappend` Zip bs = Zip (zipWith (<>) as bs) -- zipWith the internal mappend
mempty = Zip (repeat mempty)  -- repeat the internal mempty

这是使用 zip 组合元素的唯一合理方式——因此它是唯一合理的 monoid 实例。

有趣的是,这并不适用于上面的“也许”例子,因为 Haskell 不知道如何组合 Int-它应该使用 +还是 *?要在数值数据上获得一个 Monoid 实例,需要将它们包装在 SumProduct中,以告诉它使用哪个 Monoid。

Zip [Just (Sum 1),   Nothing,       Just (Sum 3), Just (Sum 4)] <>
Zip [Just (Sum 40),  Just (Sum 70), Nothing]
= Zip [Just (Sum 41),Just (Sum 70), Just (Sum 3)]


Zip [Product 5,Product 10,Product 15]
<> Zip [Product 3, Product 4]
=  Zip [Product 15,Product 40]

关键点

注意: Monoid 中的类型具有类 *这一事实正是允许我们将 Monoid a上下文放在这里的原因——我们也可以添加 Eq aOrd a。在 Monoid 中,原始元素很重要。Monoid 实例是 设计的,用于操作和组合结构中的数据。

结构2: 更高级别的选择: 备选方案

选择运算符是相似的,但也是不同的。

也许是克隆人

(<||>) :: Perhaps String -> Perhaps String -> Perhaps String
Yes xs <||> Yes ys = Yes xs   -- if we can have both, choose the left one
Yes xs <||> No     = Yes xs
No     <||> Yes ys = Yes ys
No     <||> No     = No

这里有 没有连接-我们根本没有使用 ++-这个组合纯粹在 Perhaps级工作,所以让我们将类型签名改为

(<||>) :: Perhaps a -> Perhaps a -> Perhaps a
Yes xs <||> Yes ys = Yes xs   -- if we can have both, choose the left one
Yes xs <||> No     = Yes xs
No     <||> Yes ys = Yes ys
No     <||> No     = No

注意没有约束-我们没有使用 a级别的结构,只是使用 Perhaps级别的结构。这是另一种结构。

instance Alternative Perhaps where
(<|>) = (<||>)
empty = No

ZipList 克隆

我们应该如何选择两个拉链?

Zip [1,3,4] <|> Zip [10,20,30,40] = ????

对元素使用 <|>将是非常诱人的,但是我们不能这样做,因为元素的类型对我们来说是不可用的。让我们从 empty开始。它不能使用元素,因为在定义 Alternative 时我们不知道元素的类型,所以它必须是 Zip []。我们需要它是 <|>的一个左标识(最好是右标识) ,因此

Zip [] <|> Zip ys = Zip ys
Zip xs <|> Zip [] = Zip xs

Zip [1,3,4] <|> Zip [10,20,30,40]有两个明智的选择:

  1. 因为它与“也许”是一致的
  2. 因为它最长的一致性 Zip []被丢弃

这很容易决定: 因为 pure x = Zip (repeat x),两个列表可能是无限的,所以比较它们的长度可能永远不会终止,所以必须选择第一个。因此,唯一合理的替代实例是:

instance Alternative Zip where
empty = Zip []
Zip [] <|> x = x
Zip xs <|> _ = Zip xs

这是我们能够定义的唯一明智的选择。注意它与 Monoid 实例的不同之处,因为我们不能混淆元素,我们甚至不能看它们。

关键点

注意 ,因为 Alternative采用类型为 * -> *的构造函数,所以有 不可能来添加 Ord aEq aMonoid a上下文。Alternative 是 不允许,用于使用有关结构中数据的任何信息。你不能,无论你多么想,任何东西的数据,除了可能扔掉它。

关键点: Alternative 和 Monoid 有什么不同?

不是很多——它们都是幺半群,但是总结一下最后两个部分:

Monoid *实例使组合内部数据成为可能。Alternative (* -> *)实例使其不可能。Monoid 提供灵活性,Alternative 提供保证。*(* -> *)是这种差异的主要驱动因素。同时使用这两种操作允许您同时使用这两种类型的操作。

这是正确的做法,我们的两种口味都很合适。Perhaps String的 Monoid 实例表示将所有字符放在一起,Alternative 实例表示在 String 之间进行选择。

Monoid 实例没有任何问题,它正在做它的工作,结合数据。
也许替代实例没有什么问题-它正在做它的工作,选择之间的事情。

Zip 的 Monoid 实例组合了它的元素。Zip 的 Alternative 实例被迫选择其中一个列表——第一个非空列表。

能同时做这两件事真好。

应用上下文有什么用?

在选择和应用之间有一些交互作用。参见 蚂蚁 S-Z 定律在他的问题中陈述或在他的答案中间。

从实用的角度来看,它是有用的,因为 Alternative 是一些应用函子可以选择的东西。这个功能被应用程序使用,因此发明了一个通用接口类。Applicative Functor 用于表示产生值的计算(IO、 Parser、 Input UI element,...) ,其中一些必须处理失败——需要另一种方法。

为什么选择有 empty

为什么 Alternative 需要一个空的方法/成员?我可能错了,但它似乎没有被使用过... 至少在我能找到的代码中是这样的。它似乎不符合这门课的主题——如果我有两样东西,并且需要选择其中一样,那么我为什么需要一个“ em pty”呢?

这就像问为什么加法需要一个0-如果你想加东西,有什么不加任何东西的意义呢?答案是,0是关键的关键数字,一切都围绕着它转,就像1是乘法的关键,[]是列表的关键(而 y=e^x是微积分的关键)。实际上,您可以使用这些无为元素来开始构建:

sum = foldr (+) 0
concat = foldr (++) []
msum = foldr (`mappend`) mempty          -- any Monoid
whichEverWorksFirst = foldr (<|>) empty  -- any Alternative

我们不能用 Monad + Alternative 代替 MonadPlus 吗?

MonadPlus 类型类的意义何在?难道我不能用 Monad 和 Alternative 来释放它所有的优点吗?为什么不扔了它?(我确信我错了,但我没有任何反例)

你没错,根本没有反例!

你那个有趣的问题让 Antal S-Z、 Petr Pudlák 和我深入研究了 monadPlus 和 Applicative 之间到底是什么关系。答案是, 给你给你 任何一个 MonadPlus(在左侧分布意义上——按照链接获取详细信息)也是 Alternative,但不是反过来。

这意味着,如果你制作一个 Monad 和 monadPlus 的实例,它就是 无论如何都满足应用型和替代型的条件。这意味着,如果您遵循 MonadPlus 的规则(带有左撇子) ,您可能已经使您的 Monad 成为一个应用程序和使用的替代品。

但是如果我们移除了 MonadPlus 类,我们就移除了一个合理的地方来记录规则,这样你就失去了指定某个东西是 Alternative 而不是 MonadPlus 的能力(技术上我们应该这样做)。这些都是理论上的原因。实际的原因是它会破坏现有的代码。(这也是 Applicative 和 Functor 都不是 Monad 超级类的原因。)

Alternative 和 Monoid 不是一样的吗? Alternative 和 Monoid 不是完全不同吗?

Wikipedia 说,“ Alternative 类是针对 Applicative 函子的,它也有一个幺半群结构。”我不明白 Alternative 不是和 Monoid 完全不同吗?也就是说,我理解 Alternative 类型类的要点是在两个事物之间进行选择,而我理解 Monoid 是关于组合事物的。

Monoid 和 Alternative 是从两个对象中得到一个对象的两种合理方法。数学不在乎你是选择、组合、混合还是放大你的数据,这就是为什么 Alternative 被称为应用型的 Monoid。你现在似乎对这个概念很熟悉,但你现在说

对于同时具有 Alternative 和 Monoid 实例的类型,实例应该是相同的

我不同意这一点,我认为我的“也许”和“ ZipList”示例已经仔细地解释了为什么它们是不同的。如果有什么区别的话,我觉得它们是一样的应该是很罕见的。我只能想到一个例子,普通列表,在这种情况下是合适的。这是因为列表是带有 ++的幺半群的一个基本例子,但是在某些上下文中,列表也被用作不确定的元素选择,所以 <|>也应该是 ++