Large-scale design in Haskell? [closed] Ask Question

Large-scale design in Haskell? [closed] Ask Question

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).

彼がリンクしたスライドには、「設計を型/関数/クラス/モナドにマッピングするためのイディオム」という必要なものの箇条書きがあります。そのイディオムとは何でしょうか? :)

ベストアンサー1

これについては、Haskell で大規模プロジェクトを設計するそして、XMonad の設計と実装。大規模なエンジニアリングでは、複雑さを管理する必要があります。複雑さを管理するための Haskell の主なコード構造化メカニズムは次のとおりです。

型システム

  • 型システムを使用して抽象化を強制し、対話を簡素化します。
  • 型を介してキー不変条件を強制する
    • (例えば、特定の値は特定の範囲から逃れることができない)
    • その特定のコードはIOを行わず、ディスクに触れない
  • 安全性を強化する: チェックされた例外 (Maybe/Either)、概念の混在を避ける (Word、Int、Address)
  • 優れたデータ構造 (ジッパーなど) では、たとえば範囲外のエラーを静的に排除できるため、一部のテスト クラスが不要になる場合があります。

プロファイラー

  • プログラムのヒープおよび時間プロファイルの客観的な証拠を提供します。
  • 特にヒーププロファイリングは、不必要なメモリの使用を防ぐ最良の方法です。

純度

  • 状態を削除することで複雑さを大幅に軽減します。純粋に機能的なコードは構成的であるため、拡張可能です。必要なのは、コードの使用方法を決定する型だけです。プログラムの他の部分を変更しても、コードが突然壊れることはありません。
  • 「モデル/ビュー/コントローラ」スタイルのプログラミングを多用します。外部データをできるだけ早く純粋に機能的なデータ構造に解析し、それらの構造を操作し、すべての作業が完了したらレンダリング/フラッシュ/シリアル化します。ほとんどのコードを純粋に保ちます。

テスト

  • QuickCheck + Haskell コード カバレッジ。型ではチェックできないものをテストしていることを確認します。
  • GHC + RTS は、GC に時間がかかりすぎているかどうかを確認するのに最適です。
  • QuickCheck は、モジュールのクリーンで直交した API を識別するのにも役立ちます。コードのプロパティを記述するのが難しい場合は、おそらく複雑すぎます。コードをテストでき、適切に構成できるクリーンなプロパティ セットが得られるまで、リファクタリングを続けます。そうすれば、コードも適切に設計されている可能性があります。

構造化のためのモナド

  • モナドは、主要なアーキテクチャ設計を型でキャプチャします (このコードはハードウェアにアクセスします、このコードはシングルユーザー セッションです、など)。
  • たとえば、xmonad の X モナドは、システムのどのコンポーネントにどの状態が表示されるかの設計を正確に捉えます。

型クラスと存在型

  • 型クラスを使用して抽象化を提供します。つまり、実装を多態的なインターフェースの背後に隠します。

同時実行性と並列性

  • 簡単に構成できる並列処理をプログラムに組み込んparで、競争相手に勝ちましょう。

リファクタリング

  • Haskell では、多くのリファクタリングを行うことができます。型を賢く使用すれば、型によって大規模な変更が安全になります。これにより、コードベースの拡張が容易になります。リファクタリングが完了するまで、型エラーが発生しないようにしてください。

FFIを賢く使う

  • FFI を使用すると、外部コードでのプレイが容易になりますが、その外部コードは危険な場合があります。
  • 返されるデータの形状に関する想定には十分注意してください。

メタプログラミング

  • Template Haskell またはジェネリックを少し使用すると、定型句を削除できます。

包装と配送

  • Cabalを使用してください。独自のビルドシステムを作成しないでください。(編集:実際には、おそらくスタックさあ、始めましょう。
  • 優れたAPIドキュメントにはHaddockを使用する
  • 次のようなツールグラフモッドモジュール構造を表示できます。
  • 可能であれば、Haskell Platformバージョンのライブラリとツールに頼ってください。これは安定した基盤です。(編集:繰り返しますが、最近では、スタック安定した基盤を構築して稼働させるためです。

警告

  • -Wallコードを臭いから守るために使用してください。より確実な方法としては、Agda、Isabelle、Catch などを検討してください。lint のようなチェックについては、リント、改善を提案します。

これらすべてのツールを使用すると、複雑さをコントロールし、コンポーネント間の相互作用を可能な限り排除できます。理想的には、構成的であるためメンテナンスが非常に簡単な、非常に大きな純粋なコード ベースを用意します。これは常に可能であるとは限りませんが、目指す価値はあります。

一般的には、システムの論理単位を可能な限り参照透過的な最小のコンポーネントに分解し、それらをモジュールに実装します。コンポーネント セット (またはコンポーネント内部) のグローバルまたはローカル環境は、モナドにマッピングされる可能性があります。代数データ型を使用して、コア データ構造を記述します。これらの定義を広く共有します。

おすすめ記事