24

我正在阅读Learn You a Haskell并且我已经介绍了应用程序,现在我在使用 monoids。我对两者的理解都没有问题,尽管我发现 applicative 在实践中很有用,而 monoid 并非如此。所以我想我不了解 Haskell 的一些东西。

首先,说到Applicative,它创建了类似统一语法的东西来对“容器”执行各种操作。所以我们可以使用普通函数对Maybe, 列表执行操作IO(我应该说 monads 吗?我还不知道 monads ),函数:

λ> :m + Control.Applicative
λ> (+) <$> (Just 10) <*> (Just 13)
Just 23
λ> (+) <$> [1..5] <*> [1..5]
[2,3,4,5,6,3,4,5,6,7,4,5,6,7,8,5,6,7,8,9,6,7,8,9,10]
λ> (++) <$> getLine <*> getLine
one line
 and another one
"one line and another one"
λ> (+) <$> (* 7) <*> (+ 7) $ 10
87

所以 applicative 是一种抽象。我认为我们可以没有它,但它有助于清楚地表达一些想法模式,这很好。

现在,让我们来看看Monoid。它也是抽象且非常简单的一种。但它对我们有帮助吗?对于书中的每个示例,似乎都有更清晰的方法来做事:

λ> :m + Data.Monoid
λ> mempty :: [a]
[]
λ> [1..3] `mappend` [4..6]
[1,2,3,4,5,6]
λ> [1..3] ++ [4..6]
[1,2,3,4,5,6]
λ> mconcat [[1,2],[3,6],[9]]
[1,2,3,6,9]
λ> concat [[1,2],[3,6],[9]]
[1,2,3,6,9]
λ> getProduct $ Product 3 `mappend` Product 9
27
λ> 3 * 9
27
λ> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2
24
λ> product [3,4,2]
24
λ> getSum . mconcat . map Sum $ [1,2,3]
6
λ> sum [1..3]
6
λ> getAny . mconcat . map Any $ [False, False, False, True]
True
λ> or [False, False, False, True]
True
λ> getAll . mconcat . map All $ [True, True, True]
True
λ> and [True, True, True]
True

所以我们注意到了一些模式并创建了新的类型类......好吧,我喜欢数学。但是从实际的角度来看,有什么意义Monoid呢?它如何帮助我们更好地表达想法?

4

4 回答 4

32

加布里埃尔冈萨雷斯在他的博客中写了很多关于为什么你应该关心的信息,你真的应该关心。你可以在这里阅读(也可以看到这个)。

它是关于 API 的可扩展性、架构和设计的。这个想法是“传统架构”说:

将 A 类型的多个组件组合在一起,生成 B 类型的“网络”或“拓扑”

这种设计的问题在于,随着您的程序扩展,重构时您的地狱也会如此。

所以你想改变模块 A 来改进你的设计或领域,所以你这样做了。哦,但是现在依赖于 A 的模块 B 和 C 坏了。你修复了B,太好了。现在你修复了 C。现在 B 又坏了,因为 B 也使用了 C 的一些功能。我可以永远继续下去,如果你曾经使用过 OOP - 你也可以。

然后就是 Gabriel 所说的“Haskell 架构”:

将 A 型的几个组分组合在一起,生成相同 A 型的新组分,其性质与它的取代部分没有区别

这也优雅地解决了这个问题。基本上:不要对模块进行分层或扩展以制作专门的模块。
相反,结合。

所以现在,鼓励的是,不要说“我有多个 X,所以让我们创建一个类型来表示它们的联合”,而是说“我有多个 X,所以让我们将它们组合成一个 X”。或者用简单的英语:“让我们首先制作可组合类型。” (你感觉到幺半群的潜伏了吗?)。

想象一下,您想为您的网页或应用程序制作一个表单,并且您拥有创建的模块“个人信息表单”,因为您需要个人信息。后来你发现你也需要“更改图片格式”所以就这么快写了。现在你说我想把它们结合起来,所以我们来做一个“个人信息和图片表格”模块。在现实生活中的可扩展应用程序中,这可能而且确实会失控。可能不是表格,而是演示,您需要撰写和撰写,因此您最终会得到“个人信息 & 更改图片 & 更改密码 & 更改状态 & 管理朋友 & 管理愿望清单 & 更改视图设置 & 请不要再扩展我&请&请停止!&停止!!!!“ 模块。这个不好看 您将不得不在 API 中管理这种复杂性。哦,如果你想改变任何东西 - 它可能有依赖关系。所以..是的..欢迎来到地狱。

现在让我们看看另一个选项,但首先让我们看看它的好处,因为它会引导我们找到它:

这些抽象无限扩展,因为它们始终保持可组合性,因此我们永远不需要在顶部进一步抽象。这就是你应该学习 Haskell 的原因之一:你学习如何构建扁平架构。

听起来不错,所以,与其制作“个人信息表单”/“更改图片表单”模块,不如停下来想想我们是否可以让这里的任何东西可组合。好吧,我们可以做一个“表格”,对吧?也会更抽象。
然后,为您想要的所有内容构建一个,将它们组合在一起并像其他任何形式一样获得一种形式是有意义的。

因此,您不会再得到一棵凌乱的复杂树,因为您采用两种形式并获得一种形式的关键。所以Form -> Form -> Form。正如您已经清楚地看到的那样,这个签名是mappend.

替代方案和传统架构可能看起来像a -> b -> c然后c -> d -> e...

现在,有了表格,它就没有那么具有挑战性了。挑战在于在现实世界的应用程序中使用它。要做到这一点,只需尽可能多地问自己(因为它得到了回报,正如你所见):我怎样才能使这个概念可组合?既然幺半群是实现这一目标的一种简单方法(我们想要简单),首先问问自己:这个概念怎么是一个幺半群?

旁注:幸运的是,Haskell 非常不鼓励您扩展类型,因为它是一种函数式语言(没有继承)。但是仍然可以为某事创建一个类型,为某事创建另一种类型,并在第三种类型中将两种类型都作为字段。如果这是为了作曲 - 看看你是否可以避免它。

于 2014-10-07T15:15:34.973 回答
10

好吧,我喜欢数学。但从实际的角度来看,Monoid 的意义何在?它如何帮助我们更好地表达想法?

这是一个 API。一个简单的。对于支持的类型:

  • 元素为零
  • 有追加操作

许多类型支持这些操作。因此,为操作和 API 命名有助于我们更清楚地捕捉事实。

API 很好,因为它们让我们重用代码和重用概念。意味着更好、更易维护的代码。

于 2014-10-07T08:36:02.373 回答
6

关键在于,当您将一个标记为Intas时Product,您表达了您希望整数相乘的意图。并通过将它们标记为Sum, 来添加在一起。

然后你可以mconcat在两者上使用相同的。Foldable例如,这用于foldMap表达折叠包含结构的想法,同时以特定的幺半群方式组合元素。

于 2014-10-07T08:22:22.033 回答
6

一个非常简单的例子是foldMap。只需将不同的幺半群插入这个单一函数,您就可以计算:

  • 一个最后一个元素,
  • 元素的总和乘积(由此也是它们的平均值等),
  • 检查所有元素或任何元素是否具有给定属性,
  • 计算最大或最小元素,
  • 将元素映射到一个集合(如列表、集合、字符串、TextByteString或 ByteString Builder)并将它们连接在一起——它们都是 monoid。

此外,幺半群是可组合的:如果ab是幺半群,那么 也是(a, b)。因此,您可以一次轻松地计算几个不同的幺半群值(例如计算元素平均值时的总和和乘积等)。

虽然你可以在没有幺半群的情况下完成所有这些操作,但使用foldrorfoldl会更麻烦,而且通常效率较低:例如,如果你有一个平衡的二叉树并且你想找到它的最小和最大元素,你不能同时做有效地使用foldr(或两者都使用foldl),对于其中一种情况,一个总是O(n),而当foldMap与适当的幺半群一起使用时,在这两种情况下都是O(log n)

而这只是一个单一的功能foldMap!还有许多其他有趣的应用程序。举个例子,通过平方求幂是计算能力的一种有效方式。但它实际上与计算能力无关。您可以为任何幺半群实现它,如果它<>O(1),那么您有一种计算n次的有效方法x <> ... <> x。突然之间,您可以进行有效的矩阵求幂运算并仅用O(log n)乘法计算第n斐波那契数。参见semigroup中的times1p

另请参见Monoids 和手指树

于 2014-10-08T20:22:40.873 回答