Haskell的大规模设计?

时间:2010-06-20 01:21:11

标签: haskell functional-programming monads large-scale

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

我已经阅读了很多教程(自己写一个方案是我最喜欢的,真实世界Haskell紧随其后) - 但大多数程序都相对较小,而且是单一目的。另外,我不认为它们中的一些特别优雅(例如,WYAS中的大量查找表)。

我现在想要编写更大的程序,包含更多移动部件 - 从各种不同来源获取数据,清理数据,以各种方式处理数据,在用户界面中显示,持久化,通过网络进行通信等如何才能最好地构建这样的代码,使其清晰易读,可维护并适应不断变化的需求?

有大量文献针对大型面向对象的命令式程序解决这些问题。像MVC,设计模式等的想法是实现广泛目标的理想规定,例如在OO风格中分离关注点和可重用性。此外,较新的命令式语言适合于“随着您的成长而设计”的重构风格,在我的新手看来,Haskell似乎不太适合。

Haskell有相同的文献吗?如何在功能性编程(单子,箭头,应用等)中使用异域控制结构的动物园最好地用于此目的?你能推荐什么最佳实践?

谢谢!

编辑(这是Don Stewart回答的后续行动):

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

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

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

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

他链接的幻灯片有一个我们需要的东西子弹:“将设计映射到的成语 类型/函数/类/ monads“。什么是成语?:)

8 个答案:

答案 0 :(得分:519)

我在Engineering Large Projects in HaskellDesign and Implementation of XMonad.工程中谈论这个问题,关于管理复杂性。 Haskell中用于管理复杂性的主要代码结构机制是:

类型系统

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

分析器

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

<强>纯度

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

<强>测试

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

用于构建的Monads

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

输入类和存在类型

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

并发和并行

  • 潜入par进入您的计划,以轻松,可组合的并行性打败竞争对手。

<强>重构

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

明智地使用FFI

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

元编程

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

包装和分发

  • 使用Cabal。不要滚动自己的构建系统。 (编辑:其实您现在可能想要使用Stack开始使用。)。
  • 使用Haddock获得优秀的API文档
  • graphmod这样的工具可以显示您的模块结构。
  • 如果可能的话,依靠Haskell平台版本的库和工具。这是一个稳定的基地。 (编辑:再次,这些天您可能希望使用Stack来获得稳定的基础并运行。)

<强>警告

  • 使用-Wall保持代码清洁。您还可以查看Agda,Isabelle或Catch以获得更多保证。对于类似lint的检查,请参阅优秀的hlint,它会提出改进建议。

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

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

答案 1 :(得分:118)

Don给出了上面的大部分细节,但这是我在Haskell中执行系统守护进程等非常实用的有状态程序时的两分钱。

  1. 最后,你住在monad变压器堆栈中。最底层是IO。在此之上,每个主要模块(在抽象意义上,而不是文件中的模块意义)将其必要状态映射到该堆栈中的层。因此,如果您将数据库连接代码隐藏在模块中,则将其全部写入MonadReader连接类型m =&gt; ... - &gt; m ...然后你的数据库函数总是可以获得它们的连接,而其他模块的函数不必知道它的存在。您可能最终得到一个承载数据库连接的层,另一个配置,第三个用于解决并行和同步的各种信号量和mvars,另一个用于处理日志文件等等。

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

  3. 附录(摘自评论;感谢Lii&amp; liminalisht) -
    更多关于将大型程序分割成堆栈中的monad的不同方法的讨论:

    Ben Kolera为这个主题提供了很好的实用介绍,Brian Hurt讨论了{{1}将monadic动作问题解决到自定义monad中的问题。 George Wilson显示了如何使用lift编写与任何实现所需类型类的monad一起使用的代码,而不是自定义monad类。 Carlo Hamalainen写了一些简短有用的笔记,总结了乔治的讲话。

答案 2 :(得分:43)

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

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

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

答案 3 :(得分:16)

我第一次使用this book学习结构化函数式编程。 它可能不是您正在寻找的,但对于函数式编程的初学者来说,这可能是学习构建函数式程序的最佳第一步 - 与规模无关。在所有抽象级别上,设计应始终具有明确排列的结构。

功能编程工艺

The Craft of Functional Programming

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

答案 4 :(得分:11)

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

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

您可能会熟悉本书here'Andromeda'项目代码的代码。

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

<强>更新

我在线分享了我的书(前5章)。见post on Reddit

答案 5 :(得分:7)

Gabriel的博文Scalable program architectures可能值得一提。

  

Haskell设计模式与主流设计模式不同   重要的方式:

     
      
  • 传统架构:将几个组件组合在一起   键入A以生成类型为B的“网络”或“拓扑”

  •   
  • Haskell架构:将类型A的几个组件组合在一起   生成相同类型A的新组件,无法区分   来自其取代部分的字符

  •   

通常情况下,一种看似优雅的建筑往往会从图书馆中脱颖而出,这种图书馆以自下而上的方式表现出这种良好的同质感。在Haskell中,这一点尤其明显 - 传统上被认为是“自上而下的架构”的模式往往会被捕获在mvcNetwireCloud Haskell等库中。也就是说,我希望这个答案不会被解释为尝试取代这个线程中的任何其他人,只是结构选择可以并且应该理想地由域专家在库中抽象出来。在我看来,构建大型系统的真正困难在于评估这些图书馆的建筑“善”与所有务实的关注点。

正如liminalisht在评论中提到的那样,The category design pattern是加布里埃尔关于该主题的另一篇文章,类似地。

答案 6 :(得分:5)

我发现亚历杭德罗·塞拉诺的文章"Teaching Software Architecture Using Haskell" (pdf)对于思考Haskell中的大规模结构很有用。

答案 7 :(得分:3)

也许你必须退后一步,想一想如何将问题的描述转化为设计。由于Haskell是如此高级,它可以以数据结构的形式捕获问题的描述,将过程的动作和作为函数的纯转换捕获。然后你有一个设计。编译此代码并在代码中查找有关缺少字段,缺少实例和缺少monadic转换器的具体错误时,开始开始,因为例如,您在IO过程中需要某个状态monad的库中执行数据库Access。瞧,有节目。编译器提供您的心理草图,并使设计和开发保持一致。

通过这种方式,您从一开始就受益于Haskell,编码很自然。如果你想到的是一个具体的普通问题,我不会在做“功能性”或“纯粹”或足够的一般性事物。我认为过度工程是IT中最危险的事情。当问题是创建一个抽象出一组相关问题的库时,情况会有所不同。