我非常熟悉函数式编程中使用的 单子和 箭的概念。我也知道它们可以用来解决类似的问题。
然而,对于在任何给定的情况下如何选择使用哪一个,我仍然有点困惑。
什么时候应该使用单子,什么时候应该使用箭头?
Lindley,Wadler & Yallop 有两篇优秀的论文(在 LTU 给你上讨论)。
最重要的是要明白,有 更多的东西是箭头,有的东西是单子。相反,单子严格地比箭头更强大(以上第二篇论文精确地指定了方式)。
特别地,单子是带有 (a ~> b, a) ~> b类型的应用函数的箭头,其中 (~>)是给定箭头的构造函数。Lindley 等人指出,这破坏了术语和命令(或者,如果你喜欢的话,对象和变形)之间严格的区分。
(a ~> b, a) ~> b
(~>)
应用函数有着广泛的应用,特别是对于那些被认为是流上的操作的事物。事实上,我们可以把箭头看作是由于推广了流上转换器的概念(例如,为由给定的应用函数构造的对象上的态射引入了一种新的语言)。
根据我的经验,因为单子模糊了对象和态射之间的区别(也就是说,如果我正确地使用单词,就会产生一个封闭的笛卡尔范畴) ,那么单子中的一个术语通常比箭头或应用函数中的术语更加不透明(尽管注意,这两个术语都可以分别通过 arr和 pure方法注入任意函数)。
arr
pure
因此,如果某个东西具有单子的特性(即使在概念上它形成了单子) ,那么它就有可能接受更多的检查和优化。序列化也可能更容易。因此,在解析器和电路建模中使用了应用程序和箭头。
以上试图是一般性的和描述性的。下面是我的一些自以为是的经验法则。
如果您需要建模一些看起来像状态的东西,那么从单子开始。如果您需要建模一些看起来像全局控制流的东西(例如异常、延续) ,那么从单子开始。如果出现了与单子的强大性和通用性相冲突的需求(例如,对于加入 (join :: m (m a) -> m a)的功能太强大了) ,那么考虑削弱你正在使用的东西的强大性。
(join :: m (m a) -> m a)
如果您需要对流进行建模,以及对流进行转换,特别是对于某些特征(特别是对过去和未来的无限视图)应该是不透明的流,那么首先从应用函数开始。如果您需要对流上转换的属性进行更强有力的推理,那么可以考虑使用箭头。
或者,非常粗略地说,应用程序是为了电路的行动,箭头是为了电路的结构,单子是为了通用的计算效果。
当然还有更多的故事。对于应用程序,请特别参见 科纳尔 · 艾略特在玻璃钢方面的研究。对于箭头,请参阅 XML 解析器库,扬帕玻璃钢工程项目,Horse Web 框架上的 Haskell,Hudak 和刘的经典 “用箭头堵住空间泄漏”论文,以及其他内容。对于单子来说,到处都是。当然,请注意,仅仅因为某些东西是单子,并不意味着应用符号可能不更清晰和更具表现力。
简而言之,Arrows 比 Monads 更通用,使用起来也更麻烦。因此你应该尽可能的使用 Monads,在 Monads 不适用的情况下使用 Arrows。
“风景线路”的答案如下。
约翰 · 休斯(John Hughes)介绍了 Arrows,他发表了两篇我推荐的优秀论文: “将单子泛化为箭头”和 “用箭头编程”。这两篇文章很容易阅读,并为你的问题提供了答案。即使有些人不理解这两篇文章中的所有细节或代码,他们肯定会找到很多关于单子和箭头的信息和非常有用的解释。
现在我将强调这些文章中与你的问题有关的要点
当 Monads 被介绍时,人们认为他们是全能的。事实上,Monads 包含了很多能量。但是在某些情况下,我们发现有些情况下 Monads 不能被应用。这些情况与多个输入有关,特别是当某些输入是静态的,而某些输入是动态的时候。所以,约翰 · 休斯挺身而出,介绍了阿罗斯。
箭比单子更一般。箭头是单子的超集。他们可以做到 Monads 所做的一切,甚至更多。但它们也更难使用。John Hughes 建议你应该尽可能的使用 Monads,当你不能使用 Monads 的时候应该使用 Arrows。
我同意约翰 · 休斯的观点。我还想起了爱因斯坦的名言“一切都应该尽可能简单,但不能更简单”。
当然,这完全取决于当前的具体情况。让我解释一下。 让我们假设你正在学习 Haskell。然后它将是一个伟大的任务,做每个程序使用一元方法和重做它使用箭头为基础的方法。当您学习的时候,您应该努力探索所有的可能性并实现各种方法。通过这种方式,您可以获得很好的洞察力,并能够直接比较不同的解决方案。
现在让我们假设您想为社区提供一个图书馆。好吧,你应该对那些会读你代码的人说,你应该使用最容易理解的方法来完成工作。您的解决方案缺乏不必要的复杂性,这也归功于将使用您的代码的人。这样,您的解决方案更容易维护,也更不容易出错。
但是如果你处在一个边缘的情况下呢?让我们假设你不确定你是否需要额外的箭的力量。那你该怎么办?您是否应该从一元方法开始,然后在需要时转换为基于箭头的方法?还是应该从 Arrows 开始,避免在项目中途进行代价高昂的切换?
同样,我的答案是尝试第一种方法: 如果可以的话,尝试使用 Monads。如果你后来发现你不能使用 Monads,你将不得不忍受一个昂贵的切换,你将不得不重新启动和重做项目,以使用箭头。这种方法肯定需要更多的时间和其他资源。但是你会知道你做了正确的事情,那就是尽可能提供最简单,最清晰,最不复杂的解决方案。
避免不必要的复杂性是最重要的。信不信由你,这就是范畴理论中的概念(如复合函数、单子和箭头)被引入计算机科学的原因。讽刺?