在我的业务领域 - 金融机构的后台IT - 软件组件通常需要进行全局配置,记录其进度,以便进行某种错误处理/计算短路。可以通过Haskell中的Reader-,Writer-,Maybe-monads等很好地建模并与monad变换器组合在一起的东西。
但似乎有一些缺点:monad变换器背后的概念非常棘手且难以理解,monad变换器导致非常复杂的类型签名,并且它们会造成一些性能损失。
所以我想知道:monad变形金刚在处理上述常见任务时是最佳做法吗?
答案 0 :(得分:43)
Haskell社区在此问题上存在分歧。
John Hughes报告说,他发现教monad变形金刚比教monad更容易,而且他的学生用“变形金刚第一”方法做得更好。
GHC开发人员通常会避免使用monad变换器,而是选择整合自己需要的所有功能的monad。 (就在今天,我毫不含糊地告诉我GHC将不使用我三天前定义的monad变压器。)
对我来说,monad变换器很像无点编程(即没有命名变量的编程),这是有道理的;毕竟,它们在类型级别上完全是无点编程的。我从不喜欢无点编程,因为能够引入偶尔的名称是有用的。
我在实践中观察到的是
Hackage上可用的monad变换器的数量非常多,而且大多数都非常简单。这是一个典型的问题实例,学习大型库比滚动自己的实例更困难。
像作家,国家和环境这样的Monad非常简单,我认为monad变换器并没有太大的好处。
monad变形金刚闪耀的是模块化和重用。 Liang,Hudak和Jones在其标志性文章"Monad Transformers and Modular Interpreters"中精美地展示了这一属性。
monad变形金刚在处理上述常见任务时是最佳做法吗?
我会说不是。 monad变形金刚是最佳做法是您拥有相关抽象的产品系列,您可以通过以不同的方式编写和重用monad变换器。在这样的情况下,你可能会开发一些对你的问题领域很重要的monad变换器(比如GHC拒绝的那个)你和(a)以多种方式组合它们; (b)对大多数变压器实现大量重复使用; (c)在每个单子变压器中封装一些非常重要的东西。
被GHC拒绝的我的monad变压器不符合上述(a)/(b)/(c)中的任何标准。
答案 1 :(得分:8)
monad变形金刚背后的概念 是非常棘手和困难的 了解,monad变形金刚导致 非常复杂的类型签名
我认为这有点夸张:
Monad变形金刚不是你唯一的选择,你可以写一个自定义Monad,使用延续Monad。您在IO(全局),ST(本地和受控,无IO操作),MVar(同步),TVar(事务性)中有可变引用/数组。
我听说Monad变换器的潜在效率问题可以通过添加INLINE编译指示来绑定/返回mtl / transformers库的来源来缓解。
答案 2 :(得分:3)
我最近在F#的背景下“堕落”了monad构图。我编写了一个强烈依赖状态monad的DSL:所有组件都依赖于状态monad:解析器(基于状态monad的解析器monad),变量匹配表(多个用于内部类型),标识符查找表。由于这些组件都在一起工作,因此它们依赖于相同的状态monad。因此,有一种国家构成的概念将不同的地方国家和国家访问者的概念结合在一起,这些概念赋予每个算法他们自己的国家可见性。
最初,设计真的“只是一个大州的monad”。但后来我开始需要只有本地生命时间的状态,但仍处于“持久”状态的环境中(同样,所有这些状态都由状态monad管理)。为此,我确实需要引入状态monad变换器来增强状态并使状态monad适应在一起。我还添加了一个变换器,可以在状态monad和continuation状态monad之间自由移动,但我没有费心去使用它。
因此,回答这个问题:是的,monad变形金刚存在于“狂野”中。然而,我坚决反对“开箱即用”使用它们。使用简单的构建块编写应用程序,在模块之间使用小型手工制作的桥接器,如果您最终使用的是monad变压器,那就太棒了;不要从那里开始。
关于类型签名:我已经开始认为这种类型的编程与玩白痴国际象棋非常相似(而且我不是国际象棋手):你的技能水平需要达到你“看见”的程度。 “你的功能和类型融合在一起。类型签名通常最终会分散注意力,除非您出于安全原因明确要添加类型约束(或者因为编译器强制您使用F#记录来提供类型约束)。
答案 3 :(得分:2)
所以往往是相当的东西 全局像日志或配置, 你会建议加入IO 单子?从看(无可否认 非常有限的一组例子,我来了 认为Haskell代码往往是 要么是纯粹的(即根本不是monadic) 或者在IO monad中。或者这是一个 误解?
我认为这是一种误解,只有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变换器(我自己的代码,而不是工作),不要太担心它们,使用它们并没有你想象的那么糟糕
你见过chapter from Real World Haskell that uses monad transformers吗?
答案 4 :(得分:2)
我认为这只是一种误解 IO monad并不纯粹。 monads喜欢 写/ T / Reader / T / State / T / ST monad是 纯粹功能性。
在我看来,关于纯粹/非纯粹一词的概念不止一个。你的定义“IO =不确定,其他一切=纯粹”听起来类似于Peyton-Jones在“驯服效果”(http://ulf.wiger.net/weblog/2008/02/29/peyton-jones-taming-effects-the-next-big-challenge/)中所说的内容。另一方面,真实世界Haskell(在Monad Transformer章节的最后几页)将纯函数与一元函数进行了对比 - 认为你需要为这两个世界提供不同的库。顺便说一句,人们可以说IO也是纯粹的,它的副作用被封装在一个状态函数中,其类型为 RealWorld - > (a,RealWorld)。毕竟,Haskell称自己是一种纯函数式语言(包括IO,我认为: - )。)
我的问题不在于理论上可以做些什么,而在于从软件工程的角度来看已经证明有用的东西。 Monad变换器允许效果的模块化(以及一般的抽象),但编程的方向应该前往?
答案 5 :(得分:2)
当我学习monads时,我使用一堆StateT ContT IO构建了一个应用程序来创建一个离散事件模拟库; continuation用于存储monadic线程,StateT持有可运行的线程队列,其他队列用于挂起的线程等待各种事件。它工作得很好。我无法弄清楚如何为一个newtype包装器编写Monad实例,所以我只是把它作为一个类型的同义词,并且效果很好。
这些天我可能会从头开始推出自己的monad。然而,每当我这样做时,我发现自己正在看“All About Monads”和MTL的来源,以提醒我绑定操作的样子,所以在某种意义上我仍然在考虑MTL堆栈,即使结果是一个定制的monad。