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

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

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

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

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

谢谢!

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

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

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

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

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

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


当前回答

Gabriel的博客文章可伸缩的程序架构可能值得一提。

Haskell设计模式与主流设计模式有一点不同 重要的方法: 常规架构:将多个组件组合在一起 类型A生成类型B的“网络”或“拓扑” Haskell体系结构:将几个A类型的组件组合在一起 生成一个相同类型a的新组件,在 其取代基部分的特征

It often strikes me that an apparently elegant architecture often tends to fall out of libraries that exhibit this nice sense of homogeneity, in a bottom-up sort of way. In Haskell this is especially apparent - patterns that would traditionally be considered "top-down architecture" tend to be captured in libraries like mvc, Netwire and Cloud Haskell. That is to say, I hope this answer will not be interpreted as an attempt replace any of the others in this thread, just that structural choices can and should ideally be abstracted away in libraries by domain experts. The real difficulty in building large systems, in my opinion, is evaluating these libraries on their architectural "goodness" versus all of your pragmatic concerns.

正如liminalisht在评论中提到的,分类设计模式是Gabriel关于这个主题的另一篇文章,以类似的方式。

其他回答

我发现Alejandro Serrano的论文“使用Haskell教授软件架构”(pdf)对思考Haskell中的大规模结构很有用。

我在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来获得更多的保证。对于类似绒线的检查,请参见大绒线,这将建议改进。

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

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

Gabriel的博客文章可伸缩的程序架构可能值得一提。

Haskell设计模式与主流设计模式有一点不同 重要的方法: 常规架构:将多个组件组合在一起 类型A生成类型B的“网络”或“拓扑” Haskell体系结构:将几个A类型的组件组合在一起 生成一个相同类型a的新组件,在 其取代基部分的特征

It often strikes me that an apparently elegant architecture often tends to fall out of libraries that exhibit this nice sense of homogeneity, in a bottom-up sort of way. In Haskell this is especially apparent - patterns that would traditionally be considered "top-down architecture" tend to be captured in libraries like mvc, Netwire and Cloud Haskell. That is to say, I hope this answer will not be interpreted as an attempt replace any of the others in this thread, just that structural choices can and should ideally be abstracted away in libraries by domain experts. The real difficulty in building large systems, in my opinion, is evaluating these libraries on their architectural "goodness" versus all of your pragmatic concerns.

正如liminalisht在评论中提到的,分类设计模式是Gabriel关于这个主题的另一篇文章,以类似的方式。

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

函数式编程的技巧

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

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

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

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