被“替代”类型类的含义及其与其他类型类的关系所迷惑


67

我一直在学习Typeclassopedia来学习类型类。我一直无法理解Alternative(并且MonadPlus,对此)。

我遇到的问题:

  • 'pedia说:“ Alternate类型类适用于也具有类半体结构的Applicative函数。” 我不明白这一点-“替代”是否意味着与Monoid完全不同?也就是说,我理解Alternative类型类的要点是在两件事之间进行选择,而我将Monoids理解为要结合事物。

  • 为什么Alternative需要一个empty方法/成员?我可能是错的,但似乎根本没有使用它……至少在我能找到的代码中。而且这似乎与课程的主题不符-如果我有两件事,并且需要选择其中一件事,我需要做什么?

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

  • 什么是点MonadPlus式类?我不能仅通过同时使用aMonad和a来释放其所有优点Alternative吗?为什么不干掉它呢?(我确定我错了,但是我没有任何反例)

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


赏金更新:@Antal的答案是一个不错的开始,但是Q3仍然是公开的:Alternative提供了Monoid没有提供什么?我发现此答案不尽人意,因为它缺少具体的示例,并且对另类的更高种类如何将其与Monoid进行了具体讨论。

如果要将应用程序的效果与Monoid的行为结合起来,为什么不做:

liftA2 mappend

这让我更加困惑,因为许多Monoid实例与Alternative实例完全相同。

这就是为什么我要寻找一些特定的示例这些示例说明了为什么需要“替代”,以及与Monoid有何不同(或意味着不同)。


2
查看此问题以及其中链接的两个问题。
拉斐尔·卡塔诺

另请参阅此答案
马特·芬威克

Answers:


86

首先,让我为每个问题提供简短的答案。然后,我将每个答案扩展成更详细的答案,但是这些简短的答案有望帮助您找到答案。

  1. 没有,Alternative并且Monoid不意味着不同的事情; Alternative适用于同时具有Applicative和的结构的类型Monoid。对于相同的广义概念,“拾取”和“组合”是两种不同的直觉。

  2. Alternative包含empty以及<|>因为设计者认为这将是有用的,并且因为这引起了类半身像。在挑选方面,empty相当于做出了不可能的选择。

  3. 我们两者都需要AlternativeMonoid因为前者比后者服从(或应该)遵守更多的法律。这些定律关系到类型构造函数的单调和适用结构。另外,Alternative不能依赖于内部类型,而Monoid可以。

  4. MonadPlus比稍强Alternative,因为它必须遵守更多的法律;这些定律除适用结构外,还使单曲面结构与单原子结构相关。如果您同时具有这两种情况,则它们应重合。


并不Alternative意味着完全不同Monoid吗?

并不是的!造成混淆的部分原因是,HaskellMonoid类使用了一些非常糟糕的名称(很好,不够通用)。这是数学家定义一个monoid的方式(对此非常明确):

定义。半群是一组中号配备有杰出的元件ε&Element;中号和一个二进制运算符·:中号×中号中号,通过并置来表示,以使得以下两个条件成立:

  1. ε是单位:对于所有中号ε = ε=
  2. ·是结合的:对于所有₁,₂,₃∈中号,(₂)₃=₁(₃)。

而已。在Haskell,ε拼写mempty和·拼写mappend(或者,这些天,<>),以及该组中号的类型是Minstance Monoid M where ...

查看此定义,我们发现它并没有对“组合”(或“挑选”)产生任何影响。它说出有关·和ε的事情,仅此而已。现在,这是千真万确的结合东西这种结构运行良好:ε对应于无事,和₂说,如果我格莱姆教授₁和₂的东西在一起,我可以得到一个包含所有的东西,新的东西。但这里的另一种直觉:ε相当于根本没有选择,₂相当于之间进行选择₁和2。这是“挑剔”的直觉。请注意,它们都遵守monoid律:

  1. 身份一无所有和别无选择。
    • 如果我没有任何东西并将其与某些东西结合在一起,那么我将再次得到相同的东西。
    • 如果我在完全没有选择(某些不可能)和其他选择之间做出选择,则必须选择其他(可能)选择。
  2. 将收藏物放在一起并做出选择都是关联的。
    • 如果我有三个集合,那么我将前两个集合在一起,然后将第三个集合在一起,或者将最后两个集合在一起,然后将第一个集合集合在一起,则没关系。无论哪种方式,我最终都会得到相同的总含糊的收藏。
    • 如果我可以在三件事之间进行选择,那么我(a)首先在第一或第二和第三之间选择,然后,如果需要,可以在第一和第二之间选择,或者(b)首先在第一和第二之间选择,都没关系第二或第三,然后,如果需要的话,在第二和第三之间。无论哪种方式,我都可以选择我想要的。

(注:我玩弄这里,这就是为什么它的直觉举例来说,需要记住的是·不必是可交换的,这上面掩盖了很重要的。这是完全可能的,₂≠₁ )

看哪:这两种事情(以及许多其他事情-乘数字实际上是“组合”还是“挑选”?)都遵循相同的规则。有一种直觉是重要的发展的理解,但它是确定什么是规则和定义,实际上是怎么回事。

最棒的是,这两种直觉都可以由同一个载体解释!令M为包含空集合的集合的集合(而不是所有集合的集合!),令ε为空集合∅,让·为集合联合∪。很容易看出∅是identity的一个恒等式,并且∪是关联的,因此我们可以得出以下结论:(M,∅,∪)是一个类半群。现在:

  1. 如果我们将集合视为事物的集合,则∪对应于将它们聚在一起以获得更多的事物,即“组合”直觉。
  2. 如果我们认为集合代表了可能的动作,则∪对应于增加您从中挑选的可能动作(“挑选”直觉)的集合。

这正是[]Haskell中正在发生的事情:对所有人而言[a]都是a ,并且作为应用函子(和monad)用于表示不确定性。组合直觉和挑选直觉都在同一类型上重合:和。Monoida[]mempty = empty = []mappend = (<|>) = (++)

因此,Alternative该类就在那里表示对象,这些对象(a)是应用函子,并且(b)在实例化类型时,具有遵循某些规则的值和二进制函数。哪个规则?Monoid规则。为什么?因为事实证明是有用的:-)


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

好吧,一个狡猾的答案是“因为Alternative代表一个半同构结构”。但是,真正的问题是:为什么有一个monoid结构?为什么不只是一个半群,一个没有ε的mono半群呢?一种答案是声称mono半身像只是更有用了。我认为许多人(但也许不是Edward Kmett)会同意这一点。几乎所有时间,如果您有一个明智的(<|>)/ mappend/·,您将能够定义明智的empty/ mempty/ ε。另一方面,具有额外的通用性是很好的,因为它使您可以将更多内容放在保护伞下。

您还想知道这与“挑剔”直觉如何配合。请记住,从某种意义上来说,正确的答案是“知道何时放弃'挑剔'的直觉”,我认为您可以将两者结合起来。考虑一下[],非确定性的应用函子。如果我结合型的两个值[a](<|>),对应于不确定地采摘无论是从左边的动作或从右边的动作。但是有时候,您将不会在一侧采取任何可能的行动,这很好。同样地,如果我们考虑解析器,(<|>)表示一个解析器,用于解析左侧或右侧的内容(“拾取”)。而且,如果您有一个始终失败的解析器,那么最终将是一个身份:如果您选择它,则立即拒绝该选择并尝试另一个选择。

所有这一切说,记住,这是完全有可能有一类几乎像Alternative,但缺乏empty。那将是完全正确的,甚至可能是的超类,Alternative但碰巧不是Haskell所做的。大概这是有用的猜测。


为什么Alternative类型类需要Applicative约束,为什么需要一种约束* -> *?……为什么不只是[使用] liftA2 mappend

好吧,让我们考虑一下这三个建议的更改:摆脱对的Applicative约束Alternative;改变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。这的确为我们提供了一个半圆形的结构。但这似乎给了我们错误的答案。应该Just fst >|< Just snd失败,因为(a,a) -> a不是的实例Monoid?不,但这就是上面的代码将导致的结果。我们想要的类四面体实例是一个内部类型不可知的实例(在非常相关的haskell-cafe讨论中借用了Matthew Farkas-Dyck的术语,它提出了一些非常相似的问题);该结构是关于一个由的结构确定的类,而不是的自变量结构。Alternativeff

现在我们认为我们想离开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)完全相同,因此拥有一个单独的类没有任何好处。实际上,即使我们不理会种类变量而删除Applicative约束,也Alternative只是成为forall a. Monoid (f a),尽管我们无法在Haskell中编写这些量化的约束,即使使用所有花哨的GHC扩展也是如此。(请注意,这表示了上面提到的内部类型不可知论。)因此,如果我们可以进行这些更改中的任何一个,那么我们就没有理由保留Alternative(除了能够表达该量化的约束,但是似乎很难引起注意) 。

因此,问题归结为“Alternative零件之间以及零件的Applicative零件之间是否存在关系f?” 虽然有在文档中没有的,我要采取的立场,说至少是-or,有应该是。我认为这Alternative应该服从一些有关的法律Applicative(除了偏正定律之外);特别是我认为这些法律就像

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

这些定律似乎适用于[]Maybe,并且(假装其MonadPlus实例为Alternative实例)IO,但是我没有做任何证明或详尽的测试。(例如,我本来以为分布是成立的<*>,但是这以错误的顺序“执行了效果” []。)不过,以此类推,确实MonadPlus可以遵循类似的定律(尽管显然有一些定律)关于哪个含糊不清)。我本来想主张第三条定律,这似乎很自然:

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

但是,尽管我相信[]Maybe遵守该法律,但我IO不认为(出于在接下来的几段中显而易见的原因),最好不要这样做。

确实,爱德华·克梅特(Edward Kmett)似乎有一些幻灯片支持他类似的观点。为此,我们需要进行简短的题外话,涉及更多的数学术语。最后一张幻灯片,“我想要更多的结构”,说“一个半身像是对一个应用词,而正确的半身是对一个替代词,”和“如果抛弃一个可应用词的论点,那么当你抛出一个半角像时离开“替代方案”的论点,您将获得RightSemiNearRing。”

对吧?“正确的半耳环是如何进入的?” 我听到你哭了。 好,

定义。 一个右侧近半环(还权seminearring,但前者似乎使用更多的谷歌)是四([R,+,·,0)([R,+,0)是一个幺,([R,·)是一个半群,并且满足以下两个条件:

  1. ·是右分配超过+:对于所有- [R 小号[R ,(小号+- [R = SR + TR
  2. 0是正确的吸收为·:所有ř[R,0 - [R = 0。

一个左侧近半环的定义类似。

现在,这不是很有效,因为<*>它不是真正的关联或二进制运算符-类型不匹配。我认为这就是爱德华·克梅特(Edward Kmett)在谈论“抛弃论点”时的想法。另一种选择可能是说(我不确定这是否是正确的),我们实际上想要的(f a<|><*>empty),以形成近semiringoid权,其中“-oid”后缀表示二元操作只能应用于特定的元素对(àla groupoids)。而且我们还希望想说,( ,f a<|><$>empty是一个左侧近semiringoid,虽然这可能从可以想象的组合跟从Applicative规律和正确的近半环状结构。但是现在,我变得头昏脑胀,无论如何这都不是很重要。

无论如何,这些法律要强于类规法则,意味着完全有效的Monoid实例将变为无效的Alternative实例。标准库中至少有两个示例:Monoid a => (a,)Maybe。让我们快速查看它们。

给定任何两个类半群,它们的乘积就是一个类半群;因此,可以以Monoid明显的方式将元组作为的实例(重新格式化基本包的source):

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

类似地,我们可以Applicative通过累积monoid元素(重新格式化基本包的source),将第一个组成部分是monoid的元素的元组变成的实例:

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

但是,元组不是的实例Alternative,因为它们不是—Monoid a => (a,b)并非所有类型都存在over的单调结构b,并且Alternative'的单调结构必须与内部类型无关。b为了能够表达(f <> g) <*> a,不仅必须是monad,还需要将Monoid实例用于函数,即用于形式的函数Monoid b => a -> b。即使在我们拥有所有必要的monoidal结构的情况下,它违反了所有四个Alternative法律。要看到这一点,请ssf n = (Sum n, (<> Sum n))ssn = (Sum n, Sum n)。于是,写(<>)mappend,我们得到下面的结果(可以在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的咖啡厅讨论我提到在本节的开头提出了改变这一点,有一个Option从所述半群包NEWTYPE这将产生同样的效果。)作为一个MonoidMaybe通过使用升降机半群到类群Nothing作为身份; 由于基本程序包没有半组类,因此它仅提升了类半体,因此我们得到了(重新格式化基本程序包的源):

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实例的失败程度不及(,)。它确实遵守有关的法律<*>,尽管这几乎是偶然的-它来自于Monoidfor函数的唯一实例的行为,(如上所述),它提升了将monoids返回到读者应用函子的函数。如果您进行了计算(这都是非常机械的),您会发现和实例的<*>所有持有者的右分布和右吸收,Alternative以及的Monoid左吸收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)

现在,此示例有一个警告。如果仅要求Alternatives与兼容<*>,而不与兼容<$>,那就Maybe很好。上面提到的Edward Kmett的幻灯片未提及<$>,但我认为也有必要就其制定法律。但是,我找不到任何支持我的东西。

因此,我们可以得出结论,成为Alternativea比成为a是更强的要求Monoid,因此它需要一个不同的类。最纯净的例子是带有内部类型不可知Monoid实例和Applicative彼此不兼容的实例的类型。但是,基本软件包中没有任何此类类型,我想不到任何类型。(尽管可能会不存在,尽管我会感到惊讶。)但是,这些内部类型不可知论的例子说明了为什么两个类型类必须不同。


什么是点MonadPlus式类?

MonadPlus就像Alternative是加强了Monoid,但是Monad代替了Applicative。据爱德华Kmett在他的回答这个问题“类型类之间的区别MonadPlusAlternative以及Monoid?” MonadPlus强于Alternative:法律empty <*> a,例如,并不意味着empty >>= fAndrewC提供了两个示例:Maybe及其双重。事实有两个潜在的法律定律使MonadPlus这个问题变得复杂。普遍同意,MonadPlus它应与mplus和组成一个单面体mempty,并且应满足左零法律mempty >>= f = mempty。Hhowever,一些MonadPlusSES满足左侧分配mplus a b >>= f = mplus (a >>= f) (b >>= f); 其他人满足左手的要求mplus (return a) b = return a。(请注意,的零零/分布MonadPlus类似于的右分布/吸收Alternative;比(<*>)相似。)左分布可能是“更好”的,因此满足左捕获量的任何实例,例如,都是第一种,但不是第一种的。而且,由于左抓依靠排序,你能想象一个NEWTYPE包装器,其实例1-偏移,而不是1-偏移:(=<<)(>>=)MonadPlusMaybeAlternativeMonadPlusMaybeAlternativea <|> Just b = Just b。这既不会满足左分配也不会满足左捕获,但是将是完全有效的Alternative

然而,因为任何类型的一个MonadPlus人应该有它的实例相吻合,其Alternative实例(相信这也是以同样的方式要求,要求ap(<*>)相等的MonadS中的ApplicativeS),你能想象定义MonadPlus类,而不是作为

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

该类不需要声明新的函数。它只是一个有关遵守法律承诺empty(<|>)给定类型。Haskell标准库中没有使用这种设计技术,但出于类似的目的,它在一些具有数学思想的软件包中使用。例如,点阵软件包使用它来表达这样的想法,即点阵只是同一类型的连接半格相遇半格,它们通过吸收定律相连。

Alternative即使您想保证Alternative并且Monoid总是重合,您也无法执行相同的原因,是因为种类不匹配。所需的类声明将具有以下形式

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 ...该做你期望什么。


谢谢,这可以帮助很多。不过,我仍然停留在第3点上-替代方案比Monoid的价值...必须更多地考虑这一点。
马特·芬威克

1
@MattFenwick我在同一条船上。 Monoid我了解,但是我不确定为什么我们需要一个单独的且基本相同的Alternative/ MonadPlus。我的大猜想是虚构的,但有时它们具有不同的语义(这很有用,但恕我直言,这不是一个很好的论据)。
singpolyma

1
从记录上看,我是一家与风车战斗的人,因为他只留<>在了那里Data.Semigroup。爱德华早起折叠。
Yitz

2
从2015年:不信教是MonadPlus的超hackage.haskell.org/package/base-4.8.0.0/docs/...
SAM boosalis

1
GHC Haskell现在支持量化约束
schuelermine

17
import Data.Monoid
import Control.Applicative

让我们追溯一个Monoid和Alternative与Maybe函子和ZipList函子相互作用的示例,但让我们从头开始,一方面让所有的定义都浮现在脑海,另一方面让我们始终避免将标签切换为黑客行为,但是主要是为了让我可以通过ghci来纠正我的错字!

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

这是Maybe克隆:

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:组合元素:Monoid

也许克隆

首先让我们看一下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好像当作,串联实际上在String级别上起作用,而不是在About级别上起作用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

这种monoid结构用于Perhaps尝试在a级别上尽可能多地工作。注意Monoid a约束,告诉我们我们正在使用该a级别的结构。这不是替代结构,而是派生(提升)的Monoid结构。

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

在这里,我使用了数据的结构a来增加整个结构。如果要合并Sets,则可以添加Ord a上下文。

ZipList克隆

那么我们应该如何将元素与zipList结合在一起?如果我们将它们组合在一起,应该压缩到什么位置?

   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实例。

有趣的是,这对于上面的Maybe示例不起作用,因为Haskell不知道如何组合Ints-应该使用+还是使用*?要获取数字数据上的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克隆

我们应该如何在两个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. Zip [1,3,4] 因为它是第一位-与Maybe一致
  2. Zip [10,20,30,40]因为它最长-与Zip []被丢弃一致

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

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

这是我们可以定义的唯一明智的选择。请注意,它与Monoid实例有何不同,因为我们无法弄乱元素,甚至无法查看它们。

关键点

请注意,由于Alternative采用的是构造函数,* -> *因此无法添加Ord aorEq aMonoid acontext。不允许替代方案使用有关结构内部数据的任何信息。无论您要多少,您都不能对数据任何事情,除非可能会将其丢弃。

关键点:替代方案和Monoid有什么区别?

并不是很多-它们都是monoid,但总结一下最后两节:

Monoid * 实例使合并内部数据成为可能。 Alternative (* -> *)实例使之成为不可能。Monoid提供灵活性,Alternative提供保证。种类*(* -> *)是造成这种差异的主要驱动力。同时拥有它们可以让您使用两种操作。

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

Mayid的Monoid实例没有任何问题-它正在完成其工作,合并数据。
Maybe的Alternative实例没有任何问题-它正在完成工作,在事物之间进行选择

Zip的Monoid实例结合了其元素。Zip的Alternative实例被迫选择一个列表-第一个非空列表。

能够同时做到这两个很好。

适用上下文有什么用?

选择和应用之间存在一些相互作用。请参阅此处的问题或回答中间提到的Antal SZ的法律

从实用的角度来看,它是有用的,因为“替代”是供某些应用函子选择的东西。该功能已用于Applicatives,因此发明了通用接口类。应用函子非常适合表示产生值的计算(IO,解析器,输入UI元素等),其中一些必须处理故障-需要替代方法。

为什么会有替代品empty

为什么Alternative需要一个空的方法/成员?我可能是错的,但似乎根本没有使用它……至少在我能找到的代码中。而且这似乎与课程的主题不符-如果我有两件事,并且需要选择其中一件事,我需要做什么?

这就像在问为什么加法需要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 SZ,PetrPudlák和我研究了MonadPlus和Applicative之间的真正关系。答案就 在这里这里, 是什么MonadPlus(从左分布的意义-请单击链接以获取详细信息)也是Alternative,但并非相反。

这意味着,如果您创建Monad和MonadPlus的实例,则无论如何都满足适用性和替代性的条件。这意味着,如果您遵循MonadPlus(左距)的规则,则您可能还使Monad成为了Applicative并使用了Alternative。

但是,如果删除MonadPlus类,则会删除要记录的规则的合理位置,并且您将无法指定某个替代项而不是MonadPlus(从技术上来说,我们应该为Maybe做)。这些是理论上的原因。实际原因是它会破坏现有代码。(这也是为什么Applicative和Functor都不是Monad的超类的原因。)

替代和Monoid不一样吗?替代和Monoid不完全不同吗?

'pedia说:“ Alternate类型类适用于也具有类半体结构的Applicative函数。” 我不明白这一点-“替代”是否意味着与Monoid完全不同?也就是说,我理解Alternative类型类的要点是在两件事之间进行选择,而我将Monoids理解为要结合事物。

Monoid和Alternative是以明智的方式从两个对象中获取一个对象的两种方法。数学并不关心您是要选择,组合,混合还是分解数据,这就是为什么Alternative被称为Applicative Monoid的原因。您现在似乎已经有了这个概念,但是现在您说

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

我不同意这一点,并且我认为我的Maybe和ZipList示例经过了仔细的解释,以说明它们为何与众不同。如果有的话,我认为这是罕见的。我只能想到一个适当的示例,即纯列表。这是因为列表是等式的基本示例++,但是列表在某些情况下还用作元素的不确定选择,因此<|>也应该使用++


8

概要

  • 我们需要为某些应用函子定义(提供与操作相同的实例的)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字符,然后将结果组合为具有pure函数的单个String (:)。然后,应用pure函数LiteralStrings转换为Types。我们将以完全相同的方式解析变量类型,除了以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

这可以解析任何Type:parseTest types "Int" == Literal "Bool"parseTest types "a" == Variable "a"。这结合了两个解析器,而不是两个。这就是它在Applicative Functor级别而不是数据级别工作的意义。

但是,如果我们尝试:

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具有kind *,但是可以肯定地说我们是在组合Parsers,而不是Types。

(即使是类型的一个Monoid的实例,liftA2 mappend不会给你同样的解析器<|>。如果你试试Parser String,你会得到liftA2 mappend哪一个解析的其他然后会连接后,与<|>该会尝试第一解析器和默认为第二IF失败了。)

问题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"

即使您没有任何同义词(Parser并且String是同义词,所以它们都消失了),您仍然需要{-# 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)

(Maybe的monoid实例通过解除底层的monoid来解决此问题。)

使标准库不必要地依赖语言扩展显然是不希望的。


所以你有它。替代方案只是适用函子的Monoid(而不仅仅是Monoid的提升)。它需要更高类型的类型,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实例。


Monoid结构很有用。替代方法是为应用函子提供替代方法的最佳方法。


1
@MattFenwick这些不是愚蠢的问题。替代方案一样的解析器一个独异的实例,是的。我证明(a)(<|>)不等于liftA2 mappend,解决了我们为什么不那样做的问题,(b)您需要语言扩展来定义该类半身实例,这就是为什么要有一个单独的类来解决您的主要问题。
AndrewC

@MattFenwick非常抱歉-我意识到现在我从未真正使用过<|>,所以实际上没有任何<|>示例可以与Monoid的使用方式相反!我主要更改了第3a节的开始,但也更改了3b的部分。
AndrewC

希望@MattFenwick现在我实际上包含了示例,应该更有意义!
AndrewC 2012年

4

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


在尝试并未能找到任何有意义的示例之后,其中一个Applicative的结构自然导致一个与其Monoid实例*不符的Alternative实例,我终于想到了这一点:

替代法则比Monoid律更严格,因为结果不能取决于内部类型。这将大量Monoid实例排除为替代方案。 这些数据类型允许部分的Monoid实例(这意味着它们仅适用于某些内部类型),此类实例被额外的“结构”所禁止* -> *。例子:

  • Monoid的标准Maybe实例假定内部类型为Monoid =>不是替代

  • 如果ZipList,元组和函数的内部类型为Monoids =>不是Alternatives ,它们都可以成为Monoids

  • 具有至少一个元素的序列-不能作为替代方案,因为没有empty

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

另一方面,某些数据类型不能作为替代,因为它们是 *同类的,:

  • 单位- ()
  • Ordering
  • 数字,布尔值

我的推断结论是: 对于同时具有Alternative和Monoid实例的类型,这些实例的意图是相同的。 另请参阅此答案


不包括Maybe,我认为这不算在内,因为它的标准实例不需要内部类型使用Monoid,在这种情况下,它与Alternative相同


2

我理解Alternative类型类的要点是在两件事之间进行选择,而我将Monoids理解为要结合事物。

如果您考虑片刻,它们是相同的。

+联合收割机的事情(通常是数字),它的类型签名Int -> Int -> Int(或其他)。

<|>方案之间的操作者选择,它的类型签名也是相同的:两个匹配的东西,并返回合并的事情。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.