关于函数式编程:Haskell中的大规模设计?

Large-scale design in Haskell?

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

我已经学习了一系列的教程(给自己写一个我最喜欢的方案,现实世界中的哈斯凯尔紧随其后),但是大多数程序都比较小,而且是单用途的。另外,我不认为其中的一些特别优雅(例如,Wyas中的大量查找表)。

我现在想写更大的程序,有更多的活动部件-从各种不同的来源获取数据,清理它,以各种方式处理它,在用户界面中显示它,保持它,通过网络通信等等。一个最好的结构如何能使这样的代码清晰易读,可维护,并适应不断变化的需求。恩斯?

对于大型的面向对象命令式程序,有相当多的文献讨论这些问题。MVC、设计模式等思想是实现广泛目标(如OO风格中的关注点分离和可重用性)的好方法。另外,新的命令式语言适合于"随成长而设计"的重构风格,在我的初学者看来,Haskell似乎不太适合这种风格。

哈斯凯尔有同等的文献吗?功能编程(monads、arrows、applicative等)中的外来控制结构动物园是如何最好地用于此目的的?你能推荐什么最佳实践?

谢谢!

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

@Dons提到:"Monads以类型捕获关键的架构设计。"

我想我的问题是:人们应该如何看待纯功能语言中的关键架构设计?

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

什么"正确"的方法可以很好地将数据和解析步骤联系起来?我们可以编写一个大函数,它为各种数据类型做正确的事情。或者可以使用monad来跟踪到目前为止已经处理的内容,并让每个处理步骤从monad状态获取下一步需要的内容。或者一个人可以编写很大程度上独立的程序并发送消息(我不太喜欢这个选项)。

他链接的幻灯片有一个我们需要的要点:"用于将设计映射到类型/功能/类/单子"。习语是什么?:)


我在Haskell的工程大型项目以及XMONAD的设计和实现中对此进行了一些讨论。大型工程就是管理复杂性。Haskell中用于管理复杂性的主要代码结构机制是:好的。

类型系统好的。

  • 使用类型系统来强制抽象,简化交互。
  • 通过类型强制键不变量
    • (例如,某些值不能超出某些范围)
    • 特定的代码没有IO,不接触磁盘
  • 强制安全:检查异常(可能/可能),避免混合概念(word、int、address)
  • 好的数据结构(如拉链)可能会使某些测试不必要,因为它们排除了静态的越界错误。

剖析器好的。

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

纯度好的。

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

测试好的。

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

结构单子好的。

  • monads以类型捕获关键的体系结构设计(此代码访问硬件,此代码是单用户会话等)
  • 例如,x monad中的x monad精确地捕捉了系统中哪些组件可以看到的状态的设计。

类型类和存在主义类型好的。

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

并发性和并行性好的。

  • 把EDOCX1[0]潜入你的程序,以轻松、可组合的并行方式击败竞争对手。

重构好的。

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

明智地使用外国金融机构好的。

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

元程序设计好的。

  • 一些模板haskell或generics可以删除样板文件。

包装与配送好的。

  • 使用阴谋集团。不要滚动自己的构建系统。(编辑:实际上,您可能希望现在使用stack开始工作)。
  • 将haddock用于好的api文档
  • 像graphmod这样的工具可以显示您的模块结构。
  • 如果可能的话,依赖于haskell平台版本的库和工具。它是一个稳定的基地。(编辑:同样,现在您可能希望使用stack来建立和运行一个稳定的基础。)

警告好的。

  • 使用-Wall来保持代码没有异味。您也可以查看agda、isabelle或catch以获得更多的保证。对于类似于棉绒的检查,请参阅great hlint,它将建议改进。

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

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


Don给出了上面的大部分细节,但这里是我在haskell中做一些非常简单的有状态的程序(如系统守护进程)的两分钱。

  • 最后,你生活在一个单极变压器堆栈中。底部是IO。除此之外,每个主要模块(抽象意义上,不是文件意义上的模块)都将其必要状态映射到该堆栈中的一个层。因此,如果您的数据库连接代码隐藏在模块中,那么您将所有这些代码都写在一个monadreader connection m=>…->…然后,您的数据库函数总是可以在不需要从其他模块了解其存在的情况下获得它们的连接。最后,您可能会遇到一个承载数据库连接的层、另一个承载您的配置的层、三分之一用于并行和同步解析的各种信号量和mvar、另一个承载您的日志文件句柄等。

  • 首先找出错误处理方法。对于大型系统中的haskell来说,目前最大的弱点是错误处理方法太多,包括像maybe这样的糟糕方法(这是错误的,因为您无法返回任何有关出错的信息;总是使用要么而不是maybe,除非您真正的意思是缺少值)。首先要弄清楚您将如何做,并从您的库和其他代码使用的各种错误处理机制中设置适配器到最后一个。这将在以后为你拯救一个悲伤的世界。

  • 附录(摘自评论;感谢LII&liminalist)更多关于将一个大程序分割成堆栈中单子的不同方法的讨论:

    本·科雷拉对这个话题做了一个非常实际的介绍,布赖恩·赫特讨论了如何解决lift将一元动作转换为自定义一元动作的问题。GeorgeWilson演示了如何使用mtl来编写代码,这些代码与实现所需类型类的任何monad一起工作,而不是您的自定义monad类型。卡罗·哈马莱宁写了一些简短、有用的笔记来概括乔治的谈话。


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

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

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


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

    函数编程的技巧

    The Craft of Functional Programming

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


    我目前正在写一本题为"功能设计和建筑"的书。它为您提供了一套完整的技术,如何使用纯功能方法构建大型应用程序。它描述了许多功能模式和想法,同时构建了一个类似于SCADA的应用程序"仙女座"来从头开始控制宇宙飞船。我的母语是哈斯克尔。这本书包括:

    • 使用图表进行建筑建模的方法;
    • 需求分析;
    • 嵌入式DSL域建模;
    • 外部DSL设计和实现;
    • 单子系统的影响;
    • 作为功能接口的自由单子;
    • 箭头化的EDSL;
    • 利用自由单态EDSL进行控制反转;
    • 软件事务存储器;
    • 镜头;
    • 州、读者、作家、RWS、圣莫纳兹;
    • 不纯状态:IOREF、MVAR、STM;
    • 多线程并发域建模;
    • 图形用户界面;
    • 主流技术和方法的适用性,如UML、Solid、Grass;
    • 与不纯子系统的相互作用。

    你可能会熟悉这本书的代码,以及"仙女座"项目代码。

    我希望在2017年底完成这本书。在此之前,您可以阅读我的文章"功能编程中的设计和体系结构"(RUS)。

    更新

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


    加布里埃尔的博客文章可扩展程序架构可能值得一提。

    Haskell design patterns differ from mainstream design patterns in one
    important way:

    • Conventional architecture: Combine a several components together of
      type A to generate a"network" or"topology" of type B

    • Haskell architecture: Combine several components together of type A to
      generate a new component of the same type A, indistinguishable in
      character from its substituent parts

    我经常想到,一个表面上优雅的建筑往往会从图书馆中掉出来,以一种自下而上的方式表现出这种很好的同质性。在Haskell中,这一点尤为明显——传统上被视为"自顶向下架构"的模式往往在MVC、NetWire和CloudHaskell等库中捕获。也就是说,我希望这个答案不会被解释为试图取代这个主题中的其他任何一个,只是结构选择可以并且应该被领域专家理想地抽象到库中。在我看来,构建大型系统的真正困难在于评估这些库的体系结构"优点",而不是所有的实际问题。

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


    我发现Alejandro Serrano的论文"使用Haskell教授软件体系结构"(PDF)对于思考Haskell中的大规模结构很有用。


    也许您必须后退一步,首先考虑如何将问题描述转换为设计。由于haskell具有很高的层次性,它可以以数据结构、作为过程的操作和作为函数的纯转换的形式捕获对问题的描述。然后你有了一个设计。当您编译此代码并在代码中找到关于丢失字段、丢失实例和丢失单元转换器的具体错误时,开发就开始了,因为例如,您从需要在IO过程中使用特定状态单元的库中执行数据库访问。瞧,有节目。编译器为您提供思想上的草图,并为设计和开发提供一致性。

    这样一来,从一开始你就可以从Haskell的帮助中获益,而编码是自然的。如果你所想的是一个具体的普通问题,我不想做一些"实用的"、"纯粹的"或足够一般的事情。我认为过度工程是其中最危险的事情。当问题是创建一个抽象一组相关问题的库时,情况就不同了。