首页 文章

Haskell的大规模设计? [关闭]

提问于
浏览
567

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

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

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

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

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

谢谢!

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

@dons提到:“Monads在类型中捕获关键的建筑设计 . ”

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

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

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

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

8 回答

  • 3

    Gabriel的博客文章Scalable program architectures可能值得一提 .

    Haskell设计模式与主流设计模式的区别在于一个重要方面:传统架构:将A类的几个组件组合在一起,生成B类Haskell架构的“网络”或“拓扑”:将A类的几个组件组合在一起生成相同类型A的新组分,其特征与其取代基部分无法区分

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

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

  • 5

    Don给出了上面的大部分细节,但这是我在Haskell中执行系统守护进程等真正非常有条不紊的有状态程序的两分钱 .

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

    • 首先找出错误处理方法 . Haskell在大型系统中目前最大的弱点是过多的错误处理方法,包括像Maybe这样糟糕的方法(这是错误的,因为你可以首先执行它,并从你的库和其他代码使用的各种错误处理机制中设置适配器到你的最后一个 . 这将为你节省一个世界后来悲伤 .

    Addendum (摘自评论;感谢Liiliminalisht) -
    有关将大型程序分割为堆栈中的monad的不同方法的更多讨论:

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

  • 7

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

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

  • 118

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

    功能编程工艺

    The Craft of Functional Programming

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

  • 43

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

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

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

  • 11

    我发现Alejandro Serrano的论文"Teaching Software Architecture Using Haskell" (pdf)对于考虑Haskell中的大规模结构很有用 .

  • 16

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

    • 使用图表进行体系结构建模的方法;

    • 需求分析;

    • 嵌入式DSL域建模;

    • 外部DSL设计和实施;

    • Monads作为具有效果的子系统;

    • 免费monad作为功能接口;

    • 箭头化的eDSL;

    • 使用免费monadic eDSL进行控制反转;

    • 软件事务存储器;

    • 镜头;

    • 国家,读者,作家,RWS,ST monads;

    • 不纯的状态:IORef,MVar,STM;

    • 多线程和并发域建模;

    • GUI;

    • 主流技术和方法的适用性,如UML,SOLID,GRASP;

    • 与不纯子系统的交互 .

    您可能熟悉本书here的代码和'Andromeda'项目代码 .

    我希望在2017年底完成这本书 . 在此之前,你可以阅读我的文章"Design and Architecture in Functional Programming"(Rus)here .

    UPDATE

    我在网上分享了我的书(前5章) . 见post on Reddit

  • 519

    我在Engineering Large Projects in Haskell和_186351中讨论了这个问题 . 大型的工程是关于管理复杂性 . Haskell中用于管理复杂性的主要代码结构机制是:

    The type system

    • 使用类型系统来强制执行抽象,简化交互 .

    • 通过类型强制实施关键不变量

    • (例如,某些值无法逃脱某些范围)

    • 某些代码没有IO,不触摸磁盘

    • 强制安全:检查异常(可能/可能),避免混合概念(Word,Int,Address)

    • 良好的数据结构(如拉链)可以使某些类别的测试不必要,因为它们排除了例如静态地超出界限错误 .

    The profiler

    • 提供程序的堆和时间配置文件的客观证据 .

    • 特别是堆分析是确保不使用不必要的内存的最佳方法 .

    Purity

    • 通过删除状态显着降低复杂性 . 纯功能代码可以扩展,因为它是组合的 . 您只需要确定如何使用某些代码的类型 - 当您更改程序的其他部分时,它不会神秘地破坏 .

    • 使用大量的"model/view/controller"样式编程:尽快将外部数据解析为纯函数数据结构,对这些结构进行操作,然后在完成所有工作后,渲染/刷新/序列化 . 保持大部分代码纯净

    Testing

    • QuickCheck Haskell代码覆盖率,以确保您正在测试无法使用类型检查的内容 .

    • GHC RTS很适合看你是否花费太多时间做GC .

    • QuickCheck还可以帮助您识别模块的干净,正交的API . 如果代码的属性很难说明,那么它们可能过于复杂 . 继续重构,直到你拥有一组可以测试代码的完整属性,这些属性组合得很好 . 那么代码也可能设计得很好 .

    Monads for Structuring

    • Monads以类型捕获关键架构设计(此代码访问硬件,此代码是单用户会话等)

    • 例如xmonad中的X monad,精确捕获了系统的哪些组件可见的状态设计 .

    Type classes and existential types

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

    Concurrency and parallelism

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

    Refactor

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

    Use the FFI wisely

    • FFI使用外国代码更容易,但外国代码可能很危险 .

    • 在对返回的数据形状的假设中要非常小心 .

    Meta programming

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

    Packaging and distribution

    • 使用Cabal . 不要滚动自己的构建系统 . (EDIT: Actually you probably want to use Stack now for getting started.).

    • 使用Haddock获得优秀的API文档

    • graphmod之类的工具可以显示您的模块结构 .

    • 依靠Haskell平台版本的库和工具,如果可能的话 . 这是一个稳定的基地 . (EDIT: Again, these days you likely want to use Stack for getting a stable base up and running.)

    Warnings

    • 使用 -Wall 保持代码清洁气味 . 您还可以查看Agda,Isabelle或Catch以获得更多保证 . 对于类似lint的检查,请参阅伟大的hlint,这将提出改进建议 .

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

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

相关问题