设计/构造大型函数程序的好方法是什么,特别是在Haskell中?

我已经看了很多教程(我最喜欢写一个Scheme, Real World Haskell紧随其后)——但是大多数程序都相对较小,而且用途单一。此外,我不认为其中一些特别优雅(例如,WYAS中的大型查找表)。

我现在想写更大的程序,有更多的活动部件——从各种不同的来源获取数据,清洗数据,以各种方式处理数据,在用户界面中显示数据,持久化数据,通过网络通信等等。如何才能使这样的代码具有可读性、可维护性并能适应不断变化的需求?

对于大型面向对象的命令式程序,有相当多的文献解决了这些问题。像MVC、设计模式等思想是在OO风格中实现关注点分离和可重用性等广泛目标的良好处方。此外,新的命令式语言有助于“随增长而设计”的重构风格,在我的新手看来,Haskell似乎不太适合这种风格。

Haskell有类似的文献吗?函数式编程(单子、箭头、应用程序等)中各种奇异的控制结构是如何最好地用于此目的的?你有什么最佳实践建议吗?

谢谢!

编辑(这是唐·斯图尔特回答的后续):

@dons提到:“单子以类型的形式捕捉关键的建筑设计。”

我想我的问题是:如何用纯函数语言来思考关键的架构设计?

考虑几个数据流和几个处理步骤的例子。我可以为一组数据结构的数据流编写模块化解析器,并且可以将每个处理步骤作为一个纯函数来实现。一条数据所需的处理步骤取决于它的值和其他数据的值。有些步骤之后应该会有副作用,如GUI更新或数据库查询。

以一种良好的方式将数据和解析步骤绑定在一起的“正确”方法是什么?人们可以编写一个大函数,为各种数据类型做正确的事情。或者你可以使用一个单子来跟踪到目前为止已经处理了什么,并让每个处理步骤从单子状态中获得它接下来需要的任何东西。或者一个人可以编写很大程度上独立的程序并发送消息(我不太喜欢这个选项)。

他链接的幻灯片有一个“我们需要的东西”:“将设计映射到 类型/函数/类/单体”。有哪些习语?:)


当前回答

我目前正在写一本名为《功能设计与架构》的书。它为您提供了一整套如何使用纯函数方法构建大型应用程序的技术。它描述了许多功能模式和思想,同时构建了一个类似scada的应用程序“仙女座”,用于从头开始控制宇宙飞船。我的主要语言是Haskell。这本书的封面是:

Approaches to architecture modelling using diagrams; Requirements analysis; Embedded DSL domain modelling; External DSL design and implementation; Monads as subsystems with effects; Free monads as functional interfaces; Arrowised eDSLs; Inversion of Control using Free monadic eDSLs; Software Transactional Memory; Lenses; State, Reader, Writer, RWS, ST monads; Impure state: IORef, MVar, STM; Multithreading and concurrent domain modelling; GUI; Applicability of mainstream techniques and approaches such as UML, SOLID, GRASP; Interaction with impure subsystems.

您可能熟悉本书的代码和“Andromeda”项目代码。

我希望在2017年底完成这本书。在此之前,你可以在这里阅读我的文章“函数式编程中的设计和架构”(Rus)。

更新

我在网上分享了我的书(前5章)。参见Reddit上的帖子

其他回答

我在Haskell中的大型项目工程和XMonad的设计与实现中谈到了这一点。工程在很大程度上是关于管理复杂性的。Haskell中用于管理复杂性的主要代码结构机制有:

类型系统

使用类型系统来加强抽象,简化交互。 通过类型强制执行关键不变量 (例如,某些值不能逃脱某个范围) 某些代码不执行IO操作,不接触磁盘 强制安全:检查异常(可能/任意),避免混合概念(Word, Int, Address) 好的数据结构(如拉链)可以使某些测试类变得不必要,因为它们静态地排除了例如越界错误。

性能分析

提供程序堆和时间配置文件的客观证据。 特别是,堆分析是确保没有不必要的内存使用的最佳方法。

纯度

通过删除状态来显著降低复杂性。纯函数式代码可扩展,因为它是组合的。您所需要的只是确定如何使用某些代码的类型——当您更改程序的其他部分时,它不会神秘地中断。 使用大量“模型/视图/控制器”风格的编程:尽快将外部数据解析为纯功能数据结构,对这些结构进行操作,然后一旦所有工作完成,就进行渲染/刷新/序列化。保持大部分代码的纯净

测试

QuickCheck + Haskell代码覆盖率,以确保您正在测试的东西,你不能检查类型。 GHC + RTS能够帮助你判断自己是否在GC上花费了太多时间。 QuickCheck还可以帮助您为模块识别干净、正交的api。如果代码的属性难以表述,那么它们可能太复杂了。继续重构,直到你有了一组干净的属性,可以测试你的代码,并且组合得很好。那么代码可能也设计得很好。

用于结构化的单子

单子以类型的形式捕获关键的架构设计(这段代码访问硬件,这段代码是一个单用户会话,等等)。 例如,xmonad中的X单子,精确地捕捉了对系统的哪些组件可见的状态的设计。

类型类和存在类型

使用类型类来提供抽象:将实现隐藏在多态接口后面。

并发性和并行性

在您的程序中偷偷使用par,以轻松、可组合的并行性击败竞争对手。

重构

您可以在Haskell中进行大量重构。如果您明智地使用类型,类型可以确保您的大规模更改是安全的。这将有助于代码库的扩展。确保重构在完成之前都会导致类型错误。

明智地使用FFI

FFI使得使用外国代码更容易,但外国代码可能是危险的。 在假设返回的数据的形状时要非常小心。

元编程

一点Template Haskell或泛型可以删除样板。

包装和分销

使用阴谋。不要滚动您自己的构建系统。(编辑:实际上你现在可能想要使用Stack来开始。) 对于好的API文档,使用Haddock 像graphmod这样的工具可以显示模块结构。 如果可能的话,使用Haskell平台版本的库和工具。它是一个稳定的碱。(编辑:再一次,现在你可能想使用Stack来获得一个稳定的基础并运行。)

警告

使用-Wall让你的代码没有异味。你也可以看看Agda, Isabelle或Catch来获得更多的保证。对于类似绒线的检查,请参见大绒线,这将建议改进。

使用所有这些工具,您可以控制复杂性,尽可能地消除组件之间的交互。理想情况下,您有一个非常大的纯代码库,这非常容易维护,因为它是组合的。这并不总是可能的,但它值得为之奋斗。

一般来说:将系统的逻辑单元分解为尽可能小的引用透明组件,然后在模块中实现它们。组件集(或内部组件)的全局或本地环境可以映射到单子。使用代数数据类型描述核心数据结构。广泛分享这些定义。

用Haskell设计大型程序与用其他语言设计并没有什么不同。 在大范围内编程是关于将你的问题分解成可管理的部分,以及如何将它们组合在一起;实现语言不那么重要。

也就是说,在大型设计中,最好尝试并利用类型系统,以确保只能以正确的方式将各个部分组合在一起。这可能涉及到新类型或幻影类型,以使看起来具有相同类型的东西不同。

在重构代码时,纯粹性是一个很大的好处,所以尽量保持尽可能多的代码纯粹。纯代码很容易重构,因为它与程序的其他部分没有隐藏的交互。

我目前正在写一本名为《功能设计与架构》的书。它为您提供了一整套如何使用纯函数方法构建大型应用程序的技术。它描述了许多功能模式和思想,同时构建了一个类似scada的应用程序“仙女座”,用于从头开始控制宇宙飞船。我的主要语言是Haskell。这本书的封面是:

Approaches to architecture modelling using diagrams; Requirements analysis; Embedded DSL domain modelling; External DSL design and implementation; Monads as subsystems with effects; Free monads as functional interfaces; Arrowised eDSLs; Inversion of Control using Free monadic eDSLs; Software Transactional Memory; Lenses; State, Reader, Writer, RWS, ST monads; Impure state: IORef, MVar, STM; Multithreading and concurrent domain modelling; GUI; Applicability of mainstream techniques and approaches such as UML, SOLID, GRASP; Interaction with impure subsystems.

您可能熟悉本书的代码和“Andromeda”项目代码。

我希望在2017年底完成这本书。在此之前,你可以在这里阅读我的文章“函数式编程中的设计和架构”(Rus)。

更新

我在网上分享了我的书(前5章)。参见Reddit上的帖子

Perhaps you have to go an step back and think of how to translate the description of the problem to a design in the first place. Since Haskell is so high level, it can capture the description of the problem in the form of data structures , the actions as procedures and the pure transformation as functions. Then you have a design. The development start when you compile this code and find concrete errors about missing fields, missing instances and missing monadic transformers in your code, because for example you perform a database Access from a library that need a certain state monad within an IO procedure. And voila, there is the program. The compiler feed your mental sketches and gives coherence to the design and the development.

以这种方式,您从一开始就受益于Haskell的帮助,并且编码是自然的。如果你脑子里想的是一个具体的普通问题,我不想做一些“功能性的”或“纯粹的”或足够一般的事情。我认为过度设计是IT领域最危险的事情。当问题是创建一个抽象一组相关问题的库时,情况就不同了。

Don已经给出了上面的大部分细节,但这里是我在Haskell中编写真正有状态程序(如系统守护进程)的一些观点。

In the end, you live in a monad transformer stack. At the bottom is IO. Above that, every major module (in the abstract sense, not the module-in-a-file sense) maps its necessary state into a layer in that stack. So if you have your database connection code hidden in a module, you write it all to be over a type MonadReader Connection m => ... -> m ... and then your database functions can always get their connection without functions from other modules having to be aware of its existence. You might end up with one layer carrying your database connection, another your configuration, a third your various semaphores and mvars for the resolution of parallelism and synchronization, another your log file handles, etc. Figure out your error handling first. The greatest weakness at the moment for Haskell in larger systems is the plethora of error handling methods, including lousy ones like Maybe (which is wrong because you can't return any information on what went wrong; always use Either instead of Maybe unless you really just mean missing values). Figure out how you're going to do it first, and set up adapters from the various error handling mechanisms your libraries and other code uses into your final one. This will save you a world of grief later.

增编(摘自评论;感谢Lii & liminalisht) - 更多关于将一个大程序分割成一个堆栈中的单子的不同方法的讨论:

Ben Kolera给出了这个主题的一个很好的实用介绍,Brian Hurt讨论了将单体动作提升到自定义单体的解决方案。George Wilson展示了如何使用mtl编写代码,以实现所需类型类的任何单子,而不是您的自定义单子类型。Carlo Hamalainen写了一些简短有用的笔记,总结了乔治的演讲。