单子的实际用途是什么?

25

我正在阅读《Haskell趣学指南》(Learn You a Haskell),已经学完了应用和现在正在学习单子(monoids)。我没有问题理解这两个概念,虽然我发现应用在实践中很有用,而单子则不太一样。所以我认为我可能对Haskell的某些东西还没有理解透彻。

首先,说到Applicative,它创建了一种像统一语法一样的东西,可以在“容器”上执行各种操作。因此我们可以使用普通函数在Maybe、列表、IO(我是否应该说单子?我还不知道单子)和函数上执行操作:

λ> :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有什么意义呢?它如何帮助我们更好地表达想法?


2
如果你想深入了解一些展示单子威力的高级材料,请查看finger treesData.Sequence背后的引擎)。我相信现在我无法想到更简单的例子。 - luqui
4
我建议你阅读这篇出色的文章 Dan piponi's - Sibi
我建议阅读《单子和指数树》(Monoids and Finger Trees),它展示了我们如何使用相同的代码操作树,并通过插入不同的单子来实现不同的数据结构。 - Petr
4个回答

33

Gabriel Gonzalez在他的博客中介绍了关于为什么你应该关心可扩展性、API的架构和设计的重要信息,而你确实应该关心。你可以在这里(还可以看到这里)阅读。

这是关于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 会强烈反对扩展类型,因为它是一种函数式语言(没有继承)。但仍然可以为某些东西创建一个类型,为另一些东西创建另一个类型,并在第三种类型中将两种类型作为字段。如果这是为了组合,请尝试避免它。


1
你可能还想提到这篇文章,它特别使用Monoid作为运行示例。 - Gabriella Gonzalez

10
好的,我喜欢数学。但从实际角度来看,单子有什么意义?它如何帮助我们更好地表达思想?
这是一个 API。一个简单的 API。针对支持以下特性的类型:
- 有零元素 - 有追加操作
许多类型都支持这些操作。因此,为这些操作命名并创建一个 API 可以帮助我们更清晰地描述这个事实。
API 很棒,因为它们让我们重用代码和概念,从而创造出更好、更易于维护的代码。

7
一个非常简单的例子是foldMap。只需将不同的幺半群插入到这个单一函数中,就可以计算出: 此外,幺半群是可组合的:如果 ab 是幺半群,那么 (a, b) 也是幺半群。因此,在一次遍历中轻松计算多个不同的幺半值(例如在计算元素平均值时的求和和乘积)。虽然可以使用 foldrfoldl 在没有幺半群的情况下完成所有这些操作,但这更加麻烦,而且通常效果还不如使用适当的幺半群和 foldMap:例如,如果有一个平衡二叉树,并且想要查找其最小和最大元素,则无法使用 foldr(或两者都用 foldl)有效地完成两种操作之一,其中一种情况总是 O(n),而当使用适当的幺半群和 foldMap 时,两种情况都将是 O(log n)
这只是一个单一函数foldMap的所有内容!还有许多其他有趣的应用。举个例子,平方求幂是一种计算幂的有效方法。但它实际上并不局限于计算幂。您可以为任何单子实现它,并且如果它的<>O(1),则可以有效地计算nx <> ... <> x。突然间,您可以进行高效的矩阵求幂,并仅使用O(log n)次乘法计算nth Fibonacci number。请参见semigroup中的times1p

另请参阅单子和指数树


6
重点在于,当你将一个Int标记为Product时,你表达了你希望整数被乘以的意图。通过将它们标记为Sum,表示将它们相加。然后你可以在两者上使用同样的mconcat。例如,在Foldable中使用了这种方法,其中一个foldMap表达了在特定单子类的情况下对包含结构进行折叠,并将元素组合起来的想法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接