54

在我的业务领域 - 金融机构的后台 IT - 软件组件携带全局配置,记录其进度,进行某种错误处理/计算短路是很常见的......可以在 Haskell 中由 Reader-、Writer-、Maybe-monad 等很好地建模,并与 monad 转换器组合在一起。

但似乎有一些缺点:monad 转换器背后的概念非常棘手且难以理解,monad 转换器会导致非常复杂的类型签名,并且会造成一些性能损失。

所以我想知道:处理上述常见任务时,monad 转换器是最佳实践吗?

4

6 回答 6

44

Haskell 社区在这个问题上存在分歧。

  • John Hughes 报告说,他发现教授 monad 转换器比教授 monad 更容易,而且他的学生使用“转换器优先”的方法做得更好。

  • GHC 开发人员通常避免使用 monad 转换器,而是更喜欢汇总他们自己的 monad,这些 monad 集成了他们需要的所有功能。(就在今天,我被毫不含糊地告知 GHC不会使用我三天前定义​​的单子变压器。)

对我来说,monad 转换器很像无点编程(即没有命名变量的编程),这是有道理的;毕竟,它们在类型级别上完全是无点编程的。我从不喜欢无点编程,因为能够引入偶尔的名称很有用。

我在实践中观察到的是

  • Hackage 上可用的 monad 转换器的数量非常多,而且大多数都非常简单。这是一个典型的问题实例,学习大型库比滚动自己的实例更难。

  • 像 Writer、State 和 Environment 这样的 Monad 非常简单,以至于我看不到 monad 转换器有什么好处。

  • monad 转换器的亮点在于模块化和重用。Liang、Hudak 和 Jones 在其具有里程碑意义的论文“Monad Transformers and Modular Interpreters”中完美地展示了这一特性。

处理上述常见任务时,monad 转换器是最佳实践吗?

我会说不是。 monad 转换器是最佳实践的地方是您拥有相关抽象的产品线,您可以通过以不同方式组合和重用 monad 转换器来创建这些产品线。在这种情况下,您可能开发了许多对您的问题域很重要的 monad 转换器(例如被 GHC 拒绝的那个),并且您(a)以多种方式组合它们;(b) 实现大多数变压器的大量重复使用;(c) 在每个 monad 转换器中封装一些重要的东西。

我的被​​ GHC 拒绝的 monad 转换器不符合上述任何标准 (a)/(b)/(c)。

于 2010-05-03T19:40:28.323 回答
8

monad 转换器背后的概念非常棘手且难以理解,monad 转换器导致非常复杂的类型签名

我觉得这有点夸张:

  • 使用变压器的特定 Monad 堆栈并不比普通 Monad 更难使用。想想层\堆栈,你会没事的。您几乎总是不需要多次提升纯函数(或特定 IO 操作)。
  • 如前所述,将 Monad 堆栈隐藏在新类型中,使用通用派生并隐藏模块中的数据构造函数。
  • 尽量不要在函数类型签名中使用特定的 Monad 堆栈,使用 MonadIO、MonadReader 和 MonadState 等 Monad 类型类编写通用代码(使用在 Haskell 2010 中标准化的灵活上下文扩展)。
  • 使用 fclabels 之类的库来减少访问 Monad 中部分记录的样板操作。

Monad 转换器不是您唯一的选择,您可以编写自定义 Monad,使用延续 Monad。您在 IO(全局)、ST(本地和受控,无 IO 操作)、MVar(同步)、TVar(事务性)中有可变引用/数组。

我听说 Monad 转换器的潜在效率问题可以通过在 mtl/transformers 库的源中添加 INLINE pragma 来绑定/返回来缓解。

于 2010-05-03T19:21:52.723 回答
3

当我学习 monad 时,我使用一堆 StateT ContT IO 构建了一个应用程序,以创建一个离散事件模拟库;延续用于存储单子线程,StateT 保存可运行线程队列,其他队列用于挂起线程等待各种事件。它工作得很好。我不知道如何为 newtype 包装器编写 Monad 实例,所以我只是将其设为类型同义词,并且效果很好。

这些天我可能会从头开始创建自己的 monad。然而,每当我这样做时,我发现自己在看“All About Monads”和 MTL 的源代码来提醒我绑定操作是什么样的,所以从某种意义上说,我仍然在考虑 MTL 堆栈,即使结果是一个自定义单子。

于 2010-05-04T20:35:50.610 回答
3

我最近在 F# 的上下文中“迷恋”了 monad 组合。我写了一个强烈依赖于 state monad 的 DSL:所有组件都依赖于 state monad:解析器(基于 state monad 的解析器 monad)、变量匹配表(内部类型不止一个)、标识符查找表。由于这些组件都一起工作,它们依赖于同一个 state monad。因此,有一个状态组合的概念将不同的本地状态结合在一起,状态访问器的概念赋予每个算法自己的状态可见性。

最初,设计实际上是“只是一个大的状态单子”。但是后来我开始需要只有本地生命时间的状态,但仍然处于“持久”状态的上下文中(同样,所有这些状态都由状态单子管理)。为此,我确实需要引入状态 monad 转换器来增强状态并将状态 monad 调整在一起。我还添加了一个转换器以在状态单子和延续状态单子之间自由移动,但我没有费心使用它。

因此,要回答这个问题:是的,monad 转换器存在于“狂野”中。然而,我强烈反对“开箱即用”使用它们。使用简单的构建块编写应用程序,在模块之间使用手工制作的小型桥梁,如果您最终使用类似 monad 转换器之类的东西,那就太好了;不要从那里开始。

关于类型签名:我开始认为这种类型的编程非常类似于下蒙眼国际象棋(我不是国际象棋棋手):你的技能水平需要达到你“看到”你的功能的程度和类型配合在一起。类型签名最终会让人分心,除非您出于安全原因明确想要添加类型约束(或者因为编译器强制您提供它们,例如使用 F# 记录)。

于 2010-12-07T15:56:12.663 回答
2

那么像日志或配置这样的全局性的东西,你会建议放入 IO monad 吗?通过查看(公认的非常有限的一组)示例,我开始认为 Haskell 代码往往是纯粹的(即,根本不是单子的)或在 IO 单子中。或者这是一个误解?

我认为这是一个误解,只有 IO monad 不纯。像 Write/T/Reader/T/State/T/ST monad 这样的 monad 仍然是纯粹的函数式。您可以编写一个纯函数,它在内部使用这些 monad 中的任何一个,就像这个完全无用的示例:

foo :: Int -> Int
foo seed = flip execState seed $ do
    modify $ (+) 3
    modify $ (+) 4
    modify $ (-) 2

所有这一切都是隐式地线程化/管道化状态,你会明确地自己手动做,这里的 do-notation 只是给你一些很好的语法糖,让它看起来很有必要。你不能在这里做任何 IO 动作,你不能调用任何外部函数。ST monad 允许您在本地范围内拥有真正的可变引用,同时拥有纯函数接口,并且您不能在其中执行任何 IO 操作,它仍然是纯函数式的。

您无法避免某些 IO 操作,但您不想在所有事情上都退回到 IO,因为那是任何事情都可以去的地方,可以发射导弹,您无法控制。Haskell 有抽象来控制不同程度的安全/纯度的有效计算,IO monad 应该是最后的手段(但你不能完全避免它)。

在您的示例中,我认为您应该坚持使用 monad 转换器或定制的 monad,其作用与使用转换器组合它们相同。我从来没有写过自定义的 monad(还),但是我已经使用了很多 monad 转换器(我自己的代码,不在工作中),不要太担心它们,使用它们并没有你想象的那么糟糕.

你看过Real World Haskell中使用 monad 转换器的章节吗?

于 2010-05-03T22:36:35.757 回答
2

我认为这是一个误解,只有 IO monad 不纯。像 Write/T/Reader/T/State/T/ST monad 这样的 monad 仍然是纯粹的函数式。

在我看来,关于纯/非纯这个词的概念不止一个。您的定义“IO = 不纯,其他一切 = 纯”听起来类似于 Peyton-Jones 在“驯服效果”中所说的(http://ulf.wiger.net/weblog/2008/02/29/peyton-jones-taming -影响下一个大挑战/)。另一方面,Real World Haskell(在 Monad Transformer 章节的最后几页)将纯函数与一般的 monadic 函数进行了对比——认为这两个世界都需要不同的库。顺便说一句,有人可能会说 IO 也是纯粹的,它的副作用被封装在一个 State 函数中,类型为RealWorld -> (a, RealWorld)。毕竟,Haskell 称自己为纯粹的函数式语言(我认为包括 IO :-)。)

我的问题与其说是理论上可以做什么,不如说是关于从软件工程的角度来看已经证明有用的东西。Monad 转换器允许效果的模块化(以及一般的抽象),但是方向编程应该走向吗?

于 2010-05-04T08:05:42.590 回答