设计/构造大型函数程序的好方法是什么,特别是在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来获得更多的保证。对于类似绒线的检查,请参见大绒线,这将建议改进。
使用所有这些工具,您可以控制复杂性,尽可能地消除组件之间的交互。理想情况下,您有一个非常大的纯代码库,这非常容易维护,因为它是组合的。这并不总是可能的,但它值得为之奋斗。
一般来说:将系统的逻辑单元分解为尽可能小的引用透明组件,然后在模块中实现它们。组件集(或内部组件)的全局或本地环境可以映射到单子。使用代数数据类型描述核心数据结构。广泛分享这些定义。