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

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

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

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

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

谢谢!

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

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

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

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

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

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


当前回答

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

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

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

其他回答

我的确是通过这本书第一次学习了结构化函数式编程。 它可能不是你想要的,但对于函数式编程的初学者来说,这可能是学习构造函数式程序的最好的第一步之一——与规模无关。在所有抽象层次上,设计都应该有清晰的结构安排。

函数式编程的技巧

http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/

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写了一些简短有用的笔记,总结了乔治的演讲。

用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领域最危险的事情。当问题是创建一个抽象一组相关问题的库时,情况就不同了。