Haskell的大规模设计?

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

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

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

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

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

谢谢!

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

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

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

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

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

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

61661 次浏览

我在在Haskell中设计大型项目XMonad的设计与实现。中谈到了这一点,工程在很大程度上是关于管理复杂性的。Haskell中用于管理复杂性的主要代码结构机制有:

类型系统

  • 使用类型系统来加强抽象,简化交互。
  • 通过类型强制关键不变量
    • (例如,某些值不能逃脱某个范围)
    • 某些代码不执行IO操作,不接触磁盘
    • 李< / ul > < / >
    • 强制安全:检查异常(可能/任意),避免混合概念(Word, Int, Address)
    • 好的数据结构(如拉链)可以使某些测试类变得不必要,因为它们静态地排除了例如越界错误。

    性能分析

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

    纯度

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

    测试

    • QuickCheck + Haskell代码覆盖率,以确保您正在测试的东西,你不能检查类型。
    • GHC + RTS能够帮助你判断自己是否在GC上花费了太多时间。
    • QuickCheck还可以帮助您为模块识别干净、正交的api。如果代码的属性难以表述,那么它们可能太复杂了。继续重构,直到你有了一组干净的属性,可以测试你的代码,并且组合得很好。那么代码可能也设计得很好。

    用于结构化的单子

    • 单子以类型的形式捕获关键的架构设计(这段代码访问硬件,这段代码是一个单用户会话,等等)。
    • 例如,xmonad中的X单子,精确地捕捉了对系统的哪些组件可见的状态的设计。

    类型类和存在类型

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

    并发性和并行性

    • par隐藏到您的程序中,以轻松、可组合的并行性击败竞争对手。

    重构

    • 你可以在Haskell 很多中重构。如果您明智地使用类型,类型可以确保您的大规模更改是安全的。这将有助于代码库的扩展。确保重构在完成之前都会导致类型错误。

    # EYZ0

    • FFI使得使用外国代码更容易,但外国代码可能是危险的。
    • 在假设返回的数据的形状时要非常小心。

    元编程

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

    包装和分销

    • 使用阴谋。不要滚动您自己的构建系统。# EYZ0
    • 对于好的API文档,使用Haddock
    • graphmod这样的工具可以显示模块结构。
    • 如果可能的话,使用Haskell平台版本的库和工具。它是一个稳定的碱。# EYZ0

    警告

    • 使用-Wall使代码中没有异味。你也可以看看Agda, Isabelle或Catch来获得更多的保证。关于类似棉绒的检查,请参阅伟大的hlint,它将提出改进建议。

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

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

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

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

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

Don已经给出了上面的大部分细节,但这里是我在Haskell中编写真正有状态程序(如系统守护进程)的一些观点。

  1. 最后,您生活在一个单子转换器堆栈中。底部是IO。在此之上,每个主要模块(在抽象意义上,而不是文件中的模块意义上)将其必要状态映射到该堆栈中的一层。所以如果你有你的数据库连接代码隐藏在一个模块中,你把它都写在一个类型MonadReader connection m =>…-> m…然后你的数据库函数总是可以得到它们的连接而不需要其他模块的函数知道它的存在。最后可能会有一层承载数据库连接,另一层承载配置,第三层承载用于解决并行性和同步的各种信号量和mvar,另一层承载日志文件处理,等等。

  2. 找出你的错误处理首先。目前Haskell在大型系统中的最大弱点是错误处理方法过多,包括一些糟糕的方法,比如Maybe(这是错误的,因为你不能返回任何关于出错的信息;总是使用Either而不是Maybe,除非你真的只是指缺失的值)。首先弄清楚您将如何完成它,并将您的库和其他代码使用的各种错误处理机制中的适配器设置到最终的机制中。这样你以后就不用再悲伤了。

齿顶高(摘自注释;感谢Lii &# EYZ2)本;< br > 更多关于将一个大程序分割成一个堆栈中的单子的不同方法的讨论:

本Kolera给出了一个伟大的实用介绍这个主题,和布莱恩伤害讨论解决方案的问题lifting单体行动到您的自定义单子。乔治·威尔逊展示了如何使用mtl来编写与任何单子一起工作的代码,实现所需的类型类,而不是您的自定义单子类型。卡洛Hamalainen写了一些简短有用的笔记,总结了乔治的演讲。

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

函数式编程的技巧

函数式编程的技巧

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

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

以这种方式,您从一开始就受益于Haskell的帮助,并且编码是自然的。如果你脑子里想的是一个具体的普通问题,我不想做一些“功能性的”或“纯粹的”或足够一般的事情。我认为过度设计是IT领域最危险的事情。当问题是创建一个抽象一组相关问题的库时,情况就不同了。

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

Haskell设计模式与主流设计模式有一点不同 重要途径:< / p >
  • 传统的建筑:将多个组件组合在一起 输入A以生成类型为B的“网络”或“拓扑”

  • Haskell架构:将A型的多个组件组合在一起到 生成一个相同类型a的新组件,在 取代基部分

让我印象深刻的是,一个表面上优雅的架构往往来自于以一种自下而上的方式表现出这种同质性的图书馆。在Haskell中,这一点尤其明显——传统上被认为是“自顶向下架构”的模式倾向于在mvcNetwire云Haskell这样的库中捕获。也就是说,我希望这个答案不会被解释为试图取代本主题中的任何其他答案,只是结构选择可以并且应该在理想情况下由领域专家在库中抽象出来。在我看来,构建大型系统的真正困难在于评估这些库在架构上的“优点”和所有的实用关注。

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

我发现Alejandro Serrano的论文"使用Haskell教学软件架构" (pdf)对思考Haskell中的大尺度结构很有用。

我目前正在写一本名为《功能设计与架构》的书。它为您提供了一整套如何使用纯函数方法构建大型应用程序的技术。它描述了许多功能模式和思想,同时构建了一个类似scada的应用程序“仙女座”,用于从头开始控制宇宙飞船。我的主要语言是Haskell。这本书的封面是:

  • 使用图表的体系结构建模方法;
  • 需求分析;
  • 嵌入式DSL领域建模;
  • 外部DSL设计与实现;
  • Monads作为具有效果的子系统;
  • 免费的单子作为功能接口;
  • Arrowised eDSLs;
  • 基于自由双元eDSLs的控制反转
  • 软件事务内存;
  • 镜头;
  • 状态,读取器,写入器,RWS, ST单子;
  • 不纯状态:IORef、MVar、STM;
  • 多线程和并发领域建模;
  • GUI;
  • 对UML、SOLID、GRASP等主流技术和方法的适用性;
  • 与不纯子系统的交互。

您可能熟悉在这里这本书的代码和仙女座的项目代码。

我希望在2017年底完成这本书。在此之前,您可以阅读我的文章“函数式编程中的设计和架构”(Rus) 在这里

更新

我在网上分享了我的书(前5章)。看到# EYZ0