设计/构建大型功能程序的好方法是什么,特别是在Haskell中? html
我已经阅读了不少教程(本身编写一个方案是我最喜欢的,Real World Haskell紧随其后) - 可是大多数程序都相对较小,并且是单一目的。 另外,我不认为它们中的一些特别优雅(例如,WYAS中的大量查找表)。 git
我如今想要编写更大的程序,包含更多移动部件 - 从各类不一样来源获取数据,清理数据,以各类方式处理数据,在用户界面中显示,持久化,经过网络进行通讯等。一个最好的结构,这样的代码是易读,可维护,并适应不断变化的要求? github
有大量文献针对大型面向对象的命令式程序解决了这些问题。 像MVC,设计模式等的想法是实现普遍目标的理想规定,例如在OO风格中分离关注点和可重用性。 此外,较新的命令式语言适合于“随着您的成长而设计”的重构风格,在个人新手看来,Haskell彷佛不太适合。 数据库
Haskell有相同的文献吗? 如何在功能性编程(单子,箭头,应用等)中使用异域控制结构的动物园最好地用于此目的? 你能推荐什么最佳实践? 编程
谢谢! 设计模式
编辑(这是Don Stewart回答的后续行动): 安全
@dons提到:“Monads在类型中捕获关键的建筑设计。” 网络
我想个人问题是:如何在纯函数式语言中考虑关键的架构设计? 数据结构
考虑几个数据流的示例和几个处理步骤。 我能够将数据流的模块化解析器编写为一组数据结构,我能够将每一个处理步骤实现为纯函数。 一个数据所需的处理步骤将取决于其值和其余数据。 一些步骤以后应该是GUI更新或数据库查询等反作用。 架构
什么是以正确的方式绑定数据和解析步骤的“正确”方法? 人们能够编写一个大功能,为各类数据类型作正确的事情。 或者可使用monad来跟踪到目前为止已处理的内容,并让每一个处理步骤从monad状态得到接下来须要的任何内容。 或者能够写不少单独的程序并发送消息(我不太喜欢这个选项)。
他连接的幻灯片有一个咱们须要的东西子弹:“将设计映射到类型/函数/类/ monad上的成语”。 什么是成语? :)
也许你必须退后一步,想想如何将问题的描述转化为设计。 因为Haskell是如此高级,它能够以数据结构的形式捕获问题的描述,将过程的动做和做为函数的纯转换捕获。 而后你有一个设计。 编译此代码并在代码中查找有关缺乏字段,缺乏实例和缺乏monadic转换器的具体错误时,开始开始,由于例如,您在IO过程当中须要某个状态monad的库中执行数据库Access。 瞧,有节目。 编译器提供您的心理草图,并使设计和开发保持一致。
经过这种方式,您从一开始就受益于Haskell的帮助,编码很天然。 若是你想到的是一个具体的普通问题,我不会在作“功能性”或“纯粹”或足够的通常性事物。 我认为过分工程是IT中最危险的事情。 当问题是建立一个抽象一组相关问题的库时,状况就不一样了。
Gabriel的博客文章可扩展程序架构可能值得一提。
Haskell设计模式与主流设计模式的区别在于一个重要方面:
传统架构 :将A类的几个组件组合在一块儿,生成B类“网络”或“拓扑”
Haskell架构 :将A类的几个组件组合在一块儿,生成相同类型A的新组件,其特征与其取代部分没法区分
一般状况下,一种看似优雅的建筑每每会从图书馆中脱颖而出,这种图书馆以自下而上的方式展示出这种良好的同质感。 在Haskell中,这一点尤为明显 - 传统上被认为是“自上而下的架构”的模式每每会被捕获在像mvc , Netwire和Cloud Haskell这样的库中。 也就是说,我但愿这个答案不会被解释为尝试取代这个线程中的任何其余人,只是结构选择能够而且应该理想地由域专家在库中抽象出来。 在我看来,构建大型系统的真正困难在于评估这些图书馆的建筑“善”与全部实际问题。
正如liminalisht在评论中提到的那样, 类别设计模式是Gabriel关于该主题的另外一篇文章,相似地。
我在Haskell的工程大项目 以及XMonad的设计和实现中谈到了这一点。 大型工程是关于管理复杂性。 Haskell中用于管理复杂性的主要代码结构机制是:
类型系统
剖析器
纯度
测试
Monads用于结构化
键入类和存在类型
并发和并行
par
到你的程序打,方便,可组合并行的竞争。 重构
明智地使用FFI
元编程
包装和分销
警告
-Wall
来保持代码清洁气味。 您还能够查看Agda,Isabelle或Catch以得到更多保证。 对于相似lint的检查,请参阅伟大的hlint ,它将提出改进建议。 使用全部这些工具,您能够处理复杂性,尽量多地删除组件之间的交互。 理想状况下,你有一个很是大的纯代码基础,它很容易维护,由于它是组合的。 这并不是老是可行,但值得瞄准。
一般: 将系统的逻辑单元分解为可能的最小参考透明组件,而后在模块中实现它们。 组件集(或组件内部)的全局或本地环境可能会映射到monad。 使用代数数据类型来描述核心数据结构。 普遍分享这些定义。
在Haskell中设计大型程序与在其余语言中进行设计没有什么不一样。 大型编程是将您的问题分解为可管理的部分,以及如何将这些部分组合在一块儿; 实现语言不过重要。
也就是说,在大型设计中,尝试利用类型系统以确保您只能以正确的方式将各个部分组合在一块儿是一件好事。 这可能涉及newtype或phantom类型,以使看起来具备相同类型的东西不一样。
当你进行重构代码时,纯度是一个很大的好处,因此尽可能保持尽量多的纯代码。 纯代码很容易重构,由于它与程序的其余部分没有隐藏的交互。
Don给出了上面的大部分细节,但这是我在Haskell中执行系统守护进程等很是实用的有状态程序时的两分钱。
最后,你住在monad变换器堆栈中。 最底层是IO。 在此之上,每一个主要模块(在抽象意义上,而不是文件中的模块意义)将其必要状态映射到该堆栈中的层。 所以,若是您将数据库链接代码隐藏在模块中,则将其所有写入MonadReader类型链接m => ... - > m ...而后您的数据库函数始终能够得到其链接而无需其余函数模块必须意识到它的存在。 您可能最终获得一个承载数据库链接的层,另外一个配置,第三个用于解决并行和同步的各类信号量和mvars,另外一个用于日志文件处理等。
首先找出你的错误处理。 Haskell在大型系统中目前最大的弱点是过多的错误处理方法,包括像Maybe这样糟糕的错误处理方法(这是错误的,由于你不能返回任何关于出错的信息;老是使用Either而不是Maybe除非你真的只是意味着缺失值)。 弄清楚如何首先完成它,并从库和其余代码使用的各类错误处理机制中设置适配器到最后一个。 这将为您节省一个悲伤的世界。
附录 (摘自评论;感谢Lii和liminalisht ) -
更多关于将大型程序切割成堆栈中的monad的不一样方法的讨论:
Ben Kolera为这个主题提供了一个很好的实用介绍, Brian Hurt讨论了lift
monadic动做lift
到你的自定义monad的问题的解决方案。 George Wilson展现了如何使用mtl
编写适用于任何实现所需类型类的monad的代码,而不是自定义monad类。 Carlo Hamalainen撰写了一些简短有用的笔记,总结了乔治的演讲。