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

雨燕双飞 提交于 2020-01-07 21:31:32

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

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

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

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

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

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

谢谢!

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

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

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

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

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

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


#1楼

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

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


#2楼

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

Haskell设计模式与主流设计模式的区别在于一个重要方面:

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

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

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

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


#3楼

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

类型系统

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

剖析器

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

纯度

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

测试

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

Monads用于结构化

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

键入类和存在类型

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

并发和并行

  • 潜行par到你的程序打,方便,可组合并行的竞争。

重构

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

明智地使用FFI

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

元编程

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

包装和分销

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

警告

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

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

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


#4楼

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

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

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


#5楼

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

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

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

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

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!