Haskell的大型设计? [英] Large-scale design in Haskell?

查看:136
本文介绍了Haskell的大型设计?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



我已经通过一系列的教程(写自己的一个方案作为我最喜欢的,与真实世界Haskell关闭第二) - 但大多数程序相对较小,单一目的。此外,我不认为其中一些特别优雅(例如,WYAS中的广泛查找表)。



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



有大量的文献解决了面向大型面向对象的强制性程序的这些问题。像MVC,设计模式等的想法是实现广泛目标的体面处方,如分离问题和OO风格的可重用性。此外,较新的命令式语言可以借助自己的重构设计,在我的新手意见中,Haskell看起来不太适合。



是否有Haskell的等效文献?功能性编程(单子,箭头,应用等)中的异国情调控制结构动物园如何最适​​合此目的?您可以推荐哪些最佳做法?



谢谢!



编辑(这是Don Stewart's答案):



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



我想我的问题是:应该如何以纯粹的功能语言来思考关键的架构设计?



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



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



他链接的幻灯片有一个我们需要的东西项目符号:成语用于映射设计到
types / functions / classes / monads。什么是成语? :

解决方案

我在在Haskell工程大型项目和< a href =http://xmonad.wordpress.com/2009/09/09/the-design-and-implementation-of-xmonad/ =noreferrer> XMonad的设计与实现工程在大的是关于管理复杂性。 Haskell中用于管理复杂性的主要代码结构机制是:



类型系统




  • 使用类型系统执行抽象,简化交互。

  • 通过类型

      $ b $强制执行关键不变量b
    • (例如,某些值不能逃避一些范围)

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

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

  • 良好的数据结构(如拉链)使一些测试类别不必要,因为它们排除例如



剖析器




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

  • 特别是堆数据分析是确保无需使用内存的最佳方法。



纯度




  • 通过删除状态来显着降低复杂性。纯功能代码缩放,因为它是组成的。所有你需要的是确定如何使用一些代码的类型 - 当你改变程序的其他部分时,它不会神秘地破裂。

  • 使用大量的model / view /控制器样式编程:尽快将外部数据解析为纯功能数据结构,对这些结构进行操作,然后一旦完成所有工作,渲染/刷新/串行化。保持大部分代码纯



测试




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

  • GHC + RTS非常适合看到您是否在GC上花费太多时间。

  • QuickCheck还可以帮助您为模块识别干净,正交的API。如果你的代码的属性很难说明,它们可能太复杂了。保持重构,直到有一整套属性可以测试您的代码,组成良好。那么代码也可能设计得很好。



结构化单体




  • Monads捕获关键体系结构设计(此代码访问硬件,此代码是单用户会话等)。

  • 例如xmonad中的X monad可以精确地捕获到什么状态对于系统的哪些组件是可见的设计。



类型类和/或存在类型




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



并发和并行




  • 潜行 par 到您的程序中,以轻松,可组合的并行性来击败比赛。



Refactor




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



明智地使用FFI




  • FFI可以更容易地玩外国代码,但外国代码可能是危险的。




元编程




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



包装和分发




  • 使用Cabal。不要滚动自己的构建系统。 (编辑:实际上你可能想要使用 Stack 开始使用。)。

  • 使用Haddock获取良好的API文档

  • graphmod 可以显示您的模块结构。

  • 如果可能,依靠Haskell Platform版本的库和工具。这是一个稳定的基地。 (编辑:再次,这些天,您可能希望使用堆栈获得稳定的基数并运行。)



警告




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



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



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


What is a good way to design/structure large functional programs, especially in Haskell?

I've been through a bunch of the tutorials (Write Yourself a Scheme being my favorite, with Real World Haskell a close second) - but most of the programs are relatively small, and single-purpose. Additionally, I don't consider some of them to be particularly elegant (for example, the vast lookup tables in WYAS).

I'm now wanting to write larger programs, with more moving parts - acquiring data from a variety of different sources, cleaning it, processing it in various ways, displaying it in user interfaces, persisting it, communicating over networks, etc. How could one best structure such code to be legible, maintainable, and adaptable to changing requirements?

There is quite a large literature addressing these questions for large object-oriented imperative programs. Ideas like MVC, design patterns, etc. are decent prescriptions for realizing broad goals like separation of concerns and reusability in an OO style. Additionally, newer imperative languages lend themselves to a 'design as you grow' style of refactoring to which, in my novice opinion, Haskell appears less well-suited.

Is there an equivalent literature for Haskell? How is the zoo of exotic control structures available in functional programming (monads, arrows, applicative, etc.) best employed for this purpose? What best practices could you recommend?

Thanks!

EDIT (this is a follow-up to Don Stewart's answer):

@dons mentioned: "Monads capture key architectural designs in types."

I guess my question is: how should one think about key architectural designs in a pure functional language?

Consider the example of several data streams, and several processing steps. I can write modular parsers for the data streams to a set of data structures, and I can implement each processing step as a pure function. The processing steps required for one piece of data will depend on its value and others'. Some of the steps should be followed by side-effects like GUI updates or database queries.

What's the 'Right' way to tie the data and the parsing steps in a nice way? One could write a big function which does the right thing for the various data types. Or one could use a monad to keep track of what's been processed so far and have each processing step get whatever it needs next from the monad state. Or one could write largely separate programs and send messages around (I don't much like this option).

The slides he linked have a Things we Need bullet: "Idioms for mapping design onto types/functions/classes/monads". What are the idioms? :)

解决方案

I talk a bit about this in Engineering Large Projects in Haskell and in the Design and Implementation of XMonad. Engineering in the large is about managing complexity. The primary code structuring mechanisms in Haskell for managing complexity are :

The type system

  • Use the type system to enforce abstractions, simplifying interactions.
  • Enforce key invariants via types
    • (e.g. that certain values cannot escape some scope)
    • That certain code does no IO, does not touch the disk
  • Enforce safety: checked exceptions (Maybe/Either), avoid mixing concepts (Word,Int,Address)
  • Good data structures (like zippers) can make some classes of testing needless, as they rule out e.g. out of bounds errors statically.

The profiler

  • Provide objective evidence of your programs heap and time profiles.
  • Heap profiling, in particular, is the best way to ensure no uneccessary memory use.

Purity

  • Reduce complexity dramatically by removing state. Purely functional code scales, because it is compositional. All you need is the type to determine how to use some code -- it won't mysteriously break when you change some other part of the program.
  • Use lots of "model/view/controller" style programming: parse external data as soon as possible into purely functional data structures, operate on those structures, then once all work is done, render/flush/serialize out. Keeps most of your code pure

Testing

  • QuickCheck + Haskell Code Coverage, to ensure you are testing the things you can't check with types.
  • GHC +RTS is great for seeing if you're spending too much time doing GC.
  • QuickCheck can also help you identify clean, orthogonal APIs for your modules. If the properties of your code are difficult to state, they're probably too complex. Keep refactoring until you have a clean set of properties that can test your code, that compose well. Then the code is probably well designed too.

Monads for Structuring

  • Monads capture key architectural designs in types (this code accesses hardware, this code is a single-user session, etc .)
  • E.g. the X monad in xmonad, captures precisely the design for what state is visible to what components of the system.

Type classes and existential types

  • Use type classes to provide abstraction: hide implementations behind polymorphic interfaces.

Concurrency and parallelism

  • Sneak par into your program to beat the competition with easy, composable parallelism.

Refactor

  • You can refactor in Haskell a lot. The types ensure your large scale changes will be safe, if you're using types wisely. This will help your codebase scale. Make sure that your refactorings will cause type errors until complete.

Use the FFI wisely

  • The FFI makes it easier to play with foreign code, but that foreign code can be dangerous.
  • Be very careful in assumptions about the shape of data returned.

Meta programming

  • A bit of Template Haskell or generics can remove boilerplate.

Packaging and distribution

  • Use Cabal. Don't roll your own build system. (EDIT: Actually you probably want to use Stack now for getting started.).
  • Use Haddock for good API docs
  • Tools like graphmod can show your module structures.
  • Rely on the Haskell Platform versions of libraries and tools, if at all possible. It is a stable base. (EDIT: Again, these days you likely want to use Stack for getting a stable base up and running.)

Warnings

  • Use -Wall to keep your code clean of smells. You might also look at Agda, Isabelle or Catch for more assurance. For lint-like checking, see the great hlint, which will suggest improvements.

With all these tools you can keep a handle on complexity, removing as many interactions between components as possible. Ideally, you have a very large base of pure code, which is really easy to maintain, since it is compositional. That's not always possible, but it is worth aiming for.

In general: decompose the logical units of your system into the smallest referentially transparent components possible, then implement them in modules. Global or local environments for sets of components (or inside components) might be mapped to monads. Use algebraic data types to describe core data structures. Share those definitions widely.

这篇关于Haskell的大型设计?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆