在Haskell进行大规模设计?[关闭]


565

设计/构造大型功能程序的好方法是什么,尤其是在Haskell中?

我已经看过很多教程(我最喜欢编写自己的方案,紧随其后的是Real World Haskell)-但是大多数程序相对较小,而且用途单一。另外,我认为其中一些并不特别优雅(例如WYAS中庞大的查找表)。

我现在想编写更大的程序,并且要有更多的活动部分-从各种不同的来源获取数据,对其进行清理,以各种方式对其进行处理,在用户界面中显示它,对其进行持久化,通过网络进行通信等。一种最佳的结构,使代码清晰,可维护并适应不断变化的需求?

有大量的文献针对大型面向对象的命令式程序解决了这些问题。诸如MVC,设计模式之类的想法是实现OO风格的广泛目标(如关注点分离和可重用性)的合适处方。此外,较新的命令式语言使其具有“随您成长而设计”的重构风格,在我新手看来,Haskell似乎不太适合这种风格。

Haskell是否有等效的文献?如何最好地在功能编程(单子,箭头,应用程序等)中使用奇异控制结构的动物园?您可以推荐哪些最佳做法?

谢谢!

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

@dons提到:“ Monads以类型捕获关键的架构设计。”

我想我的问题是:如何用一种纯粹的功能语言思考关键的建筑设计?

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

以一种很好的方式绑定数据和解析步骤的“正确”方法是什么?可以编写一个大型函数,对各种数据类型执行正确的操作。或者,可以使用monad跟踪到目前为止已处理的内容,并让每个处理步骤从monad状态中获取下一步所需的内容。或者可以编写很大程度上独立的程序并发送消息(我不太喜欢此选项)。

他链接的幻灯片上有一个“我们需要的东西”项目符号:“将设计映射到类型/函数/类/单子的惯用法”。有哪些成语?:)


9
我认为以功能语言编写大型程序时的核心思想是小型,专业化且无状态的模块,它们通过消息传递进行通信。当然,您必须假装一点,因为真正的程序需要状态。我认为这是F#超越Haskell的地方。
ChaosPandion 2010年

18
@Chaos,但默认情况下只有Haskell强制执行无状态。您别无选择,必须努力在Haskell中引入状态(以打破组成):-)
Don Stewart 2010年

7
@ChaosPandion:从理论上讲,我没有不同意。当然,使用命令式语言(或围绕消息传递设计的功能性语言),这很可能就是我要做的。但是Haskell还有其他处理国家的方法,也许它们让我保留了更多的“纯”利益。

1
我本文件中写了一些关于这个在“设计指南”:community.haskell.org/~ndm/downloads/...
尼尔·米切尔

5
@JonHarrop让我们不要忘记,虽然在比较相似语言的项目时MLOC是一个很好的指标,但是对于跨语言比较来说,它并没有多大意义,尤其是对于像Haskell这样的语言,其中代码重用和模块化更加容易和安全。与那里的一些语言相比。
Tair

Answers:


519

在Haskell的工程大型项目以及XMonad设计和实现中谈到了这一点。大型工程涉及管理复杂性。Haskell中用于管理复杂性的主要代码结构化机制是:

类型系统

  • 使用类型系统来强制抽象,从而简化交互。
  • 通过类型强制执行关键不变式
    • (例如,某些值无法逃脱某些范围)
    • 某些代码没有IO,不会接触磁盘
  • 加强安全性:检查异常(可能),避免混合概念(字,整数,地址)
  • 好的数据结构(例如拉链)可以使某些测试类别变得不必要,因为它们排除了例如静态出界错误。

探查器

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

纯度

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

测试中

  • QuickCheck + Haskell代码覆盖率,以确保您正在测试无法使用类型检查的内容。
  • GHC + RTS非常适合查看您是否花费过多时间进行GC。
  • QuickCheck还可以帮助您为模块识别干净的正交API。如果代码的属性难以声明,那么它们可能太复杂了。继续进行重构,直到您拥有一组可以测试代码的干净属性,并且这些属性组成良好。那么代码也可能设计得很好。

单声道结构

  • Monads以类型捕获关键架构设计(此代码访问硬件,此代码是单用户会话,等等)。
  • 例如,xmonad中的X monad可以精确捕获对于系统的哪些组件可见的状态的设计。

类型类和存在类型

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

并发与并行

  • 潜入par您的程序,以轻松,可组合的并行性击败竞争对手。

重构

  • 您可以在Haskell中进行很多重构。如果明智地使用类型,这些类型可确保您进行大规模更改是安全的。这将帮助您扩展代码库。确保重构将导致类型错误,直到完成。

明智地使用FFI

  • FFI使使用外码变得更加容易,但是外码可能很危险。
  • 在关于返回数据的形状的假设中要非常小心。

元编程

  • 一点模板Haskell或泛型都可以删除样板。

包装与配送

  • 使用阴谋集团。不要滚动自己的构建系统。(编辑:实际上,您可能现在想使用Stack入门。)。
  • 使用Haddock获得优秀的API文档
  • 诸如graphmod之类的工具可以显示您的模块结构。
  • 如果可能,请依赖Haskell Platform版本的库和工具。这是一个稳定的基础。(编辑:再次,这些天,您可能希望使用Stack来获得稳定的基础并开始运行。)

警告事项

  • 使用-Wall让您的代码更干净的气味。您也可以考虑使用Agda,Isabelle或Catch以获得更多保证。对于类似皮棉的检查,请参阅hlint,它会建议改进。

使用所有这些工具,您可以处理复杂性,并尽可能减少组件之间的交互。理想情况下,您具有大量的纯代码库,由于它是组合代码,因此真的很容易维护。这并非总是可能的,但值得针对。

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


8
感谢Don,您的回答非常好-这些都是有价值的准则,我会定期参考。我想我的问题发生在需要所有这些步骤之前。我真正想知道的是“将设计映射到类型/函数/类/单子上的惯用法”……我可以尝试发明自己的惯用方法,但是我希望在某个地方可以总结出一组最佳实践-或者,如果没有,建议阅读结构良好的代码,以读取大型系统(而不是集中库)。我修改了帖子,以更直接地问同样的问题。

6
我在模块上添加了一些有关设计分解的文字。您的目标是将逻辑上相关的功能识别为与系统其他部分具有参照透明接口的模块,并尽可能早地使用纯功能数据类型来安全地建模外部世界。该xmonad设计文件涵盖了很多这样的:xmonad.wordpress.com/2009/09/09/...
唐·斯图尔特

3
我试图从Haskell演讲的Engineering Large Projects中下载幻灯片,但是链接似乎已断开。这是一个正在工作的人:galois.com/~dons/talks/dons-londonhug-decade.pdf
mik01aj 2011年

3
我设法找到了这个新的下载链接:pau-za.cz/data/2/sprava.pdf
Riccardo T.

3
@Heather即使我之前在评论中提到的页面上的下载链接不起作用,但看起来仍可以在scribd上查看幻灯片:scribd.com/doc/19503176/The-Design-and-Implementation-of -xmonad
Riccardo T.

118

Don为您提供了上面的大部分详细信息,但这是我在Haskell中执行真正精巧的有状态程序(例如系统守护程序)的两分钱。

  1. 最后,您生活在monad变压器堆栈中。底部是IO。除此之外,每个主要模块(从抽象的意义上讲,不是在文件中的模块意义上)将其必要的状态映射到该堆栈中的一层中。因此,如果您将数据库连接代码隐藏在模块中,则将它们全部写成MonadReader Connection m => ...-> m ...类型,然后您的数据库函数始终可以获取其连接,而无需其他函数模块必须知道其存在。您可能最后一层承载数据库连接,另一层承载配置,第三层承载各种信号量和mvar,以解决并行性和同步问题,另一层承载日志文件,依此类推。

  2. 首先弄清楚您的错误处理。目前,在大型系统中,Haskell的最大弱点是过多的错误处理方法,包括诸如Maybe之类的糟糕方法(这是错误的,因为您无法返回有关错误原因的任何信息;请始终使用Either代替Maybe,除非您确实只是表示缺少值)。首先弄清楚您将如何做,并从您的库和其他代码使用的各种错误处理机制中设置适配器,直到最终使用。这将在以后为您节省很多痛苦。

附录(摘录自评论;感谢Liiliminalisht)—
有关将大型程序切成堆栈的不同方法的更多讨论:

本·科勒拉(Ben Kolera)为此主题作了非常实用的介绍,而布莱恩·赫特Brian Hurt)讨论了将lift单声道动作输入到自定义单声道中的问题的解决方案。乔治·威尔逊(George Wilson)展示了如何mtl编写与实现所需类型类的任何monad(而不是您的自定义monad类型)兼容的代码。Carlo Hamalainen写了一些简短而有用的笔记,总结了乔治的讲话。


5
两个好点!这个答案的优点是合理具体,而其他答案则不然。阅读更多有关将大型程序切成堆栈的单子的不同方法的讨论将很有趣。如果有的话,请张贴这些文章的链接!
Lii

6
@Lii Ben Kolera对该主题做了非常实用的介绍,Brian Hurt讨论了将lift单子动作纳入您的自定义单子的问题的解决方案。乔治·威尔逊(George Wilson)展示了如何mtl编写与实现所需类型类的任何monad(而不是您的自定义monad类型)兼容的代码。Carlo Hamalainen写了一些简短而有用的笔记,总结了乔治的讲话。
liminalisht '16

我同意monad变压器堆栈往往是关键的架构基础,但是我尽力将IO排除在外。这并不总是可能的,但是如果您想一想“然后”在monad中的含义,您可能会发现您确实在底部某处确实有一个延续或自动机,然后可以通过“运行”功能将其解释为IO。
保罗·约翰逊

正如@PaulJohnson已经指出的那样,这种Monad变压器堆栈方法似乎与Michael Snoyman的ReaderT设计模式
Holden

43

用Haskell设计大型程序与使用其他语言进行设计没有什么不同。大型编程是将问题分解为可管理的部分,以及如何将这些问题组合在一起。实现语言不太重要。

也就是说,在大型设计中,尝试并利用类型系统来确保您只能以正确的方式将各个部分组合在一起是很好的。这可能涉及newtype或phantom类型,以使看起来具有相同类型的事物有所不同。

在进行代码重构时,纯净性是一个很大的福音,因此,请尝试保持尽可能多的纯净性。纯代码易于重构,因为它与程序的其他部分没有隐藏的交互。


14
实际上,我发现如果需要更改数据类型,则重构会令人沮丧。它需要繁琐地修改许多构造函数和模式匹配的类。(我同意将纯函数重构为相同类型的其他纯函数很容易-只要一个不涉及数据类型即可)
Dan

2
@Dan使用记录时,只需进行较小的更改(例如仅添加一个字段),即可完全摆脱困境。有些人可能希望使记录的习惯(我就是其中之一^^“)。
MasterMastic

5
@Dan我的意思是,如果您以任何语言更改函数的数据类型,都不必这样做吗?我看不到像Java或C ++这样的语言在这方面如何帮助您。如果您说可以使用两种类型都遵循的某种通用接口,那么您应该已经在Haskell中使用Typeclasses了。
分号

4
@semicon与Java之类的语言不同的是,存在成熟的,经过测试和完全自动化的重构工具。通常,这些工具具有出色的编辑器集成,并且消除了与重构相关的大量繁琐工作。Haskell为我们提供了一个出色的类型系统,通过该系统可以检测到重构中必须更改的内容,但是(目前)实际执行重构的工具非常有限,尤其是与Java中已有的工具相比生态系统已有十多年的历史了。
jsk

16

我学会结构功能首次与编程这本书。它可能并不完全是您想要的,但对于函数式编程的初学者而言,这可能是学习构建函数式程序的最佳第一步(与规模无关)。在所有抽象级别上,设计应始终具有清晰排列的结构。

函数式编程的技巧

函数式编程的技巧

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


11
一样大FP的工艺是-我学会了哈斯克尔从它-这是一个介绍性文字初级程序员,而不是在Haskell大型系统的设计。
唐·斯图尔特

3
好吧,这是我了解的有关设计API和隐藏实现细节的最好的书。有了这本书,我成为了C ++中更好的程序员-仅仅是因为我学会了更好的组织代码的方法。好吧,您的经验(和答案)肯定比这本书要好,但是Dan可能仍然是Haskell 的初学者。(where beginner=do write $ tutorials `about` Monads
2011年

11

我目前正在写一本书,标题为“功能设计与架构”。它为您提供了一套完整的技术,如何使用纯功能方法来构建大型应用程序。它描述了许多功能模式和构想,同时构建了类似SCADA的应用程序“ Andromeda”来从头控制飞船。我的主要语言是Haskell。该书涵盖:

  • 使用图进行体系结构建模的方法;
  • 需求分析;
  • 嵌入式DSL域建模;
  • 外部DSL的设计与实现;
  • 单声道作为具有效果的子系统;
  • 免费的monad作为功能接口;
  • 箭状eDSL;
  • 使用免费monadic eDSL进行控制反转;
  • 软件事务存储;
  • 镜片;
  • 状态,读者,作家,RWS,ST单子;
  • 不纯状态:IORef,MVar,STM;
  • 多线程和并发域建模;
  • GUI;
  • UML,SOLID,GRASP等主流技术和方法的适用性;
  • 与不纯子系统的交互。

您可能会在这里熟悉本书的代码以及'Andromeda'项目代码。

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

更新

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


亚历山大,请您在预订完成后更新本说明,以便我们遵循。干杯。
最大

4
当然!到目前为止,我完成了一半的文字,但这只是整体工作的1/3。因此,请保持您的兴趣,这给我很多启发!
graninas'1

2
嗨!我在线上分享了我的书(仅前5章)。见Reddit上一篇:reddit.com/r/haskell/comments/6ck72h/...
graninas

感谢您的分享和工作!
Max

真的很期待!
patriques

7

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

Haskell设计模式与主流设计模式在一个重要方面不同:

  • 常规体系结构:将A型的几个组件组合在一起以生成B型的“网络”或“拓扑”

  • Haskell体系结构:将几种类型A的组件组合在一起,以生成相同类型A的新组件,该组件的特性与它的取代基部分没有区别

常常让我感到惊讶的是,一个看起来很优雅的体系结构往往倾向于以自下而上的方式从表现出这种均质感的库中掉出来。在Haskell中,这种模式特别明显,通常会在mvcNetwireCloud Haskell之类的库中捕获通常被视为“自上而下的体系结构”的模式。就是说,我希望这个答案不会被解释为试图替换该线程中的任何其他答案,只是结构上的选择可以并且应该由领域专家理想地从库中抽象出来。我认为,构建大型系统的真正困难在于评估这些库的体系结构“优”,而不是您的所有实际关注点。

正如liminalisht在评论中提到的那样,类别设计模式是Gabriel在该主题上发表的另一篇类似文章。


3
我还要提到加布里埃尔·冈萨雷斯(Gabriel Gonzalez)关于类别设计模式的另一篇文章。他的基本观点是,我们的功能程序员认为“好的体系结构”实际上是“构成体系结构”-它使用保证构成的项目来设计程序。由于类别法保证了身份和关联性在构图下得以保留,因此通过使用我们拥有其类别的抽象(例如,纯函数,单
声道


3

也许您必须退后一步,首先考虑如何将问题的描述转换为设计。由于Haskell具有很高的水平,因此它可以以数据结构,操作作为过程以及纯转换作为函数的形式捕获问题的描述。然后您有一个设计。当您编译此代码并在代码中找到有关缺少字段,缺少实例和缺少monadic转换器的具体错误时,开发就开始了,因为例如,您从IO过程中需要某个状态monad的库执行数据库访问。瞧,有程序。编译器提供您的思维草图,并为设计和开发提供连贯性。

从一开始,您就可以从Haskell的帮助中受益,并且编码是自然的。如果您想到的是一个具体的普通问题,我将不愿意做“功能性”或“纯净”的事情,也不必做一般性的事情。我认为过度设计是IT中最危险的事情。当问题是创建一个抽象一系列相关问题的库时,情况就不同了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.