设计/构建大型功能程序的好方法是什么,特别是在Haskell中?
我已经阅读了很多教程(自己写一个方案是我最喜欢的,真实世界Haskell紧随其后) - 但大多数程序都相对较小,而且是单一目的.另外,我不认为它们中的一些特别优雅(例如,WYAS中的大量查找表).
我现在想要编写更大的程序,包含更多移动部件 - 从各种不同来源获取数据,清理数据,以各种方式处理数据,在用户界面中显示,持久化,通过网络进行通信等.一个最好的结构,这样的代码是易读,可维护,适应不断变化的要求?
有大量文献针对大型面向对象的命令式程序解决这些问题.像MVC,设计模式等的想法是实现广泛目标的理想规定,例如在OO风格中分离关注点和可重用性.此外,较新的命令式语言适合于"随着您的成长而设计"的重构风格,在我的新手看来,Haskell似乎不太适合.
Haskell有相同的文献吗?如何在功能性编程(单子,箭头,应用等)中使用异域控制结构的动物园最好地用于此目的?你能推荐什么最佳实践?
谢谢!
编辑(这是Don Stewart回答的后续行动):
@dons提到:"Monads在类型中捕获关键的建筑设计."
我想我的问题是:如何在纯函数式语言中考虑关键的架构设计?
考虑几个数据流的示例和几个处理步骤.我可以将数据流的模块化解析器编写为一组数据结构,我可以将每个处理步骤实现为纯函数.一个数据所需的处理步骤将取决于其值和其他数据.一些步骤之后应该是GUI更新或数据库查询等副作用.
什么是以正确方式绑定数据和解析步骤的"正确"方法?人们可以编写一个大功能,为各种数据类型做正确的事情.或者可以使用monad来跟踪到目前为止已处理的内容,并让每个处理步骤从monad状态获得接下来需要的任何内容.或者可以写很多单独的程序并发送消息(我不太喜欢这个选项).
他链接的幻灯片有一个我们需要的东西子弹:"将设计映射到类型/函数/类/ monad上的成语".什么是成语?:)
我在Haskell的工程大项目以及XMonad的设计和实现中谈到了这一点.大型工程是关于管理复杂性.Haskell中用于管理复杂性的主要代码结构机制是:
类型系统
使用类型系统来强制执行抽象,简化交互.
通过类型强制实施关键不变量
(例如,某些值无法逃脱某些范围)
某些代码没有IO,不会触及磁盘
强制安全:检查异常(可能/可能),避免混合概念(Word,Int,Address)
良好的数据结构(如拉链)可以使某些类别的测试变得不必要,因为它们会静态地排除例如越界错误.
剖析器
提供程序的堆和时间配置文件的客观证据.
特别是堆分析是确保不使用不必要的内存的最佳方法.
纯度
通过删除状态显着降低复杂性.纯功能代码可以扩展,因为它是组合的.您需要的只是确定如何使用某些代码的类型 - 当您更改程序的其他部分时,它不会神秘地破坏.
使用大量的"模型/视图/控制器"样式编程:尽快将外部数据解析为纯函数数据结构,对这些结构进行操作,然后在完成所有工作后,渲染/刷新/序列化.保持大部分代码纯净
测试
QuickCheck + Haskell代码覆盖率,以确保您测试无法检查类型的内容.
GHC + RTS很适合看你是否花费太多时间做GC.
QuickCheck还可以帮助您为模块识别干净,正交的API.如果代码的属性很难说明,那么它们可能过于复杂.继续重构,直到你拥有一组可以测试代码的完整属性,这些属性组合得很好.那么代码也可能设计得很好.
Monads用于结构化
Monads以类型捕获关键架构设计(此代码访问硬件,此代码是单用户会话等)
例如,xmonad中的X monad,精确捕获了系统的哪些组件可见的状态设计.
键入类和存在类型
使用类型类来提供抽象:隐藏多态接口背后的实现.
并发和并行
潜入par
您的计划,通过简单,可组合的并行性来击败竞争对手.
重构
你可以在Haskell中进行很多重构.如果您明智地使用类型,这些类型可确保您的大规模更改是安全的.这将有助于您的代码库扩展.确保重构会导致类型错误,直到完成.
明智地使用FFI
FFI使得更容易使用外部代码,但外国代码可能很危险.
关于返回的数据形状的假设要非常小心.
元编程
一些Template Haskell或泛型可以删除样板.
包装和分销
使用Cabal.不要滚动自己的构建系统.(编辑:其实你可能现在想要使用Stack开始.)
使用Haddock获得优秀的API文档
像graphmod这样的工具可以显示您的模块结构.
如果可能的话,依靠Haskell平台版本的库和工具.这是一个稳定的基地.(编辑:再次,这些天你可能想要使用Stack来获得稳定的基础并运行.)
警告
使用-Wall
让您的代码更干净的气味.您还可以查看Agda,Isabelle或Catch以获得更多保证.对于类似lint的检查,请参阅伟大的hlint,它将提出改进建议.
使用所有这些工具,您可以处理复杂性,尽可能多地删除组件之间的交互.理想情况下,你有一个非常大的纯代码基础,它很容易维护,因为它是组合的.这并非总是可行,但值得瞄准.
通常:将系统的逻辑单元分解为可能的最小参考透明组件,然后在模块中实现它们.组件集(或组件内部)的全局或本地环境可能会映射到monad.使用代数数据类型来描述核心数据结构.广泛分享这些定义.
Don给出了上面的大部分细节,但这是我在Haskell中执行系统守护进程等非常实用的有状态程序时的两分钱.
最后,你住在monad变换器堆栈中.最底层是IO.在此之上,每个主要模块(在抽象意义上,而不是文件中的模块意义)将其必要状态映射到该堆栈中的层.因此,如果您将数据库连接代码隐藏在模块中,则将其全部写入MonadReader类型连接m => ... - > m ...然后您的数据库函数始终可以获得其连接而无需其他函数模块必须意识到它的存在.您可能最终得到一个承载数据库连接的层,另一个配置,第三个用于解决并行和同步的各种信号量和mvars,另一个用于日志文件处理等.
首先找出你的错误处理.Haskell在大型系统中目前最大的弱点是过多的错误处理方法,包括像Maybe这样糟糕的错误处理方法(这是错误的,因为你不能返回任何关于出错的信息;总是使用Either而不是Maybe除非你真的只是意味着缺失值).弄清楚如何首先完成它,并从库和其他代码使用的各种错误处理机制中设置适配器到最后一个.这将为您节省一个悲伤的世界.
附录(摘自评论;感谢Lii和liminalisht) -
更多关于将大型程序分成堆栈中的monad的不同方法的讨论:
Ben Kolera为这个主题提供了一个很好的实用介绍,Brian Hurt讨论了将lift
monadic动作问题解决到你的自定义monad中的问题.George Wilson展示了如何使用mtl
编写适用于任何实现所需类型类的monad的代码,而不是自定义monad类.Carlo Hamalainen撰写了一些简短有用的笔记,总结了乔治的演讲.
在Haskell中设计大型程序与在其他语言中进行设计没有什么不同.大型编程是将您的问题分解为可管理的部分,以及如何将这些部分组合在一起; 实现语言不太重要.
也就是说,在大型设计中,尝试利用类型系统以确保您只能以正确的方式将各个部分组合在一起是一件好事.这可能涉及newtype或phantom类型,以使看起来具有相同类型的东西不同.
当你进行重构代码时,纯度是一个很大的好处,所以尽量保持尽可能多的纯代码.纯代码很容易重构,因为它与程序的其他部分没有隐藏的交互.
我本书第一次学习了结构化函数式编程.它可能不是您正在寻找的,但对于函数式编程的初学者来说,这可能是学习构建函数式程序的最佳第一步 - 与规模无关.在所有抽象级别上,设计应始终具有明确排列的结构.
功能编程工艺
http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/
我目前正在写一本名为"功能设计与架构"的书.它为您提供了一套完整的技术,如何使用纯函数方法构建大型应用程序.它描述了许多功能模式和想法,同时构建了类似SCADA的应用程序'Andromeda',用于从头开始控制太空飞船.我的主要语言是Haskell.这本书包括:
使用图表进行体系结构建模的方法;
需求分析;
嵌入式DSL域建模;
外部DSL设计和实现;
Monads作为具有效果的子系统;
免费monad作为功能接口;
箭头化的eDSL;
使用自由monadic eDSL进行控制反转;
软件交易记忆;
镜头;
国家,读者,作家,RWS,ST monads;
不纯的状态:IORef,MVar,STM;
多线程和并发域建模;
GUI;
主流技术和方法的适用性,如UML,SOLID,GRASP;
与不纯子系统的交互.
你可能熟悉这本书的代码在这里,和"仙女"项目代码.
我希望在2017年年底完成这本书在此之前,你可以阅读我的文章"函数式编程设计与建筑"(RUS)这里.
UPDATE
我在网上分享了我的书(前5章).请参阅Reddit上的帖子
Gabriel的博客文章可扩展程序架构可能值得一提.
Haskell设计模式与主流设计模式的区别在于一个重要方面:
传统架构:将A类的几个组件组合在一起,生成B类"网络"或"拓扑"
Haskell架构:将A类的几个组件组合在一起,生成相同类型A的新组件,其特征与其取代部分无法区分
通常情况下,一种看似优雅的建筑往往会从图书馆中脱颖而出,这种图书馆以自下而上的方式展现出这种良好的同质感.在Haskell中,这一点尤其明显 - 传统上被认为是"自上而下的架构"的模式往往会被捕获在像mvc,Netwire和Cloud Haskell这样的库中.也就是说,我希望这个答案不会被解释为尝试取代这个线程中的任何其他人,只是结构选择可以并且应该理想地由域专家在库中抽象出来.在我看来,构建大型系统的真正困难在于评估这些图书馆的建筑"善"与所有实际问题.
正如liminalisht在评论中提到的那样,类别设计模式是Gabriel关于该主题的另一篇文章,类似地.
我发现亚历杭德罗·塞拉诺的文章" 使用Haskell教学软件架构 "(pdf)对于思考Haskell中的大规模结构非常有用.