Haskellの大規模デザイン?[閉まっている]


565

特にHaskellで、大規模な関数型プログラムを設計/構造化する良い方法は何ですか?

私はたくさんのチュートリアルを行ってきました(Write Yourself a Schemeが私のお気に入りで、Real World Haskellがすぐ近くにあります)-ほとんどのプログラムは比較的小さく、単一目的です。さらに、それらのいくつかは特にエレガントであるとは見なしません(たとえば、WYASの膨大なルックアップテーブル)。

私は今、より多くの可動部分を備えたより大きなプログラムを書きたいと思っています-さまざまなソースからのデータの取得、クリーニング、さまざまな方法での処理、ユーザーインターフェイスでの表示、永続化、ネットワーク経由の通信など。このようなコードを読みやすく、保守しやすく、変化する要件に適応できるようにするための最良の構造はどれですか。

大規模なオブジェクト指向の命令型プログラムに関するこれらの質問に対処する非常に多くの文献があります。MVCやデザインパターンなどのアイデアは、OOスタイルでの関心の分離や再利用性などの幅広い目標を実現するための適切な処方箋です。さらに、新しい命令型言語は「成長するにつれて設計する」スタイルのリファクタリングに役立ちますが、私の初心者の意見では、Haskellはあまり適していないようです。

Haskellに相当する文献はありますか?エキゾチックな制御構造の動物園は、この目的のために関数型プログラミング(モナド、矢印、アプリケーションなど)でどのように利用するのが最適ですか?おすすめのベストプラクティスは何ですか。

ありがとう!

編集(これはドン・スチュワートの回答のフォローアップです):

@donsは言及しました:「モナドはタイプで主要な建築デザインをキャプチャします。」

私の質問は、純粋な関数型言語での主要なアーキテクチャ設計についてどのように考えればよいのでしょうか。

いくつかのデータストリームといくつかの処理ステップの例を考えてみましょう。データストリームのモジュラーパーサーを一連のデータ構造に書き込むことができ、各処理ステップを純粋な関数として実装できます。1つのデータに必要な処理手順は、その値や他のデータによって異なります。一部の手順の後には、GUIの更新やデータベースクエリなどの副作用が伴う必要があります。

データと解析ステップを適切に結び付ける「正しい」方法は何ですか?さまざまなデータ型に対して正しいことを行う大きな関数を書くことができます。または、モナドを使用して、これまでに処理されたものを追跡し、各処理ステップでモナドの状態から次に必要なものをすべて取得することもできます。または、大部分が別個のプログラムを作成してメッセージを送信することもできます(このオプションはあまり好きではありません)。

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


9
関数型言語で大きなプログラムを書くときの核となるアイデアは、メッセージパッシングを介して通信する、小さく、特殊化された、ステートレスなモジュールだと思います。もちろん、真のプログラムには状態が必要なので、少しふりをする必要があります。これが、F#がHaskellに勝るところだと思います。
ChaosPandion 2010年

18
@Chaos、ただしHaskellのみがデフォルトで無国籍を強制します。選択の余地はなく、Haskellに状態を導入する(構成性を壊すために)努力する必要があります:-)
Don Stewart

7
@ChaosPandion:理論的には同意しません。確かに、命令型言語(またはメッセージパッシングを中心に設計された関数型言語)では、それで十分でしょう。しかし、Haskellには状態を処理する他の方法があり、おそらく「純粋な」利点の多くを維持することができます。
Dan

1
私は、この文書の「設計ガイドライン」の下で、これについて少し書きました:community.haskell.org/~ndm/downloads/...
ニール・ミッチェル

5
@JonHarropは、MLOCが類似の言語でプロジェクトを比較する場合の優れた指標ですが、特にコードの再利用とモジュール化がはるかに簡単で安全なHaskellのような言語では、あまり意味がないことを忘れないでくださいそこにあるいくつかの言語と比較して。
2014

回答:


519

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

型システム

  • 型システムを使用して抽象化を適用し、相互作用を簡素化します。
  • タイプを介して主要な不変条件を適用する
    • (たとえば、特定の値は一部のスコープをエスケープできない)
    • その特定のコードはIOを実行せず、ディスクに触れません
  • 安全性の強制:例外のチェック(多分/どちらでも)、概念の混在(Word、Int、Address)を回避
  • 優れたデータ構造(ジッパーなど)は、たとえば、範囲外のエラーを静的に除外するため、一部のクラスのテストが不要になる可能性があります。

プロファイラー

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

純度

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

テスト中

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

構造化のためのモナド

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

型クラスと存在型

  • タイプクラスを使用して抽象化を提供します。実装をポリモーフィックインターフェイスの背後に隠します。

並行性と並列性

  • parプログラムに忍び込んで、簡単で構成可能な並列処理で競争に打ち勝ちます。

リファクタリング

  • あなたはHaskell でたくさんリファクタリングすることができます。タイプは、タイプを賢く使用している場合、大規模な変更が安全であることを保証します。これはコードベースのスケーリングに役立ちます。完了するまで、リファクタリングによって型エラーが発生することを確認してください。

FFIを賢く使う

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

メタプログラミング

  • テンプレートHaskellまたはジェネリックのビットは定型を削除できます。

パッケージングと配布

  • Cabalを使用します。独自のビルドシステムをロールバックしないでください。(編集:実際には、おそらく今すぐにStackを使用して開始する必要があります。)
  • Haddockを使用して適切なAPIドキュメントを作成する
  • graphmodなどのツールは、モジュール構造を表示できます。
  • 可能であれば、ライブラリとツールのHaskellプラットフォームバージョンに依存します。安定した拠点です。(編集:繰り返しになりますが、最近では、安定したベースを稼働させるためにStackを使用する可能性があります。)

警告

  • -Wallコードのにおいをなくすために使用します。さらに確実にするために、Agda、Isabelle、またはCatchを確認することもできます。lintのようなチェックについては、改善を提案する素晴らしいhlintを参照してください。

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

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


8
ありがとうドン、あなたの答えは素晴らしいです-これらはすべて貴重なガイドラインであり、私はそれらを定期的に参照します。私の質問は、これがすべて必要になる前の段階だと思います。私が本当に知りたいのは、「タイプ/関数/クラス/モナドにデザインをマッピングするためのイディオム」です...私は自分で独自に発明することを試みることができましたが、どこかに一連のベストプラクティスが抽出されることを望んでいました-そうでない場合は、大規模なシステムを読み取るための適切に構造化されたコードの推奨事項(たとえば、集中ライブラリとは対照的)。私は自分の投稿を編集して、この同じ質問をより直接尋ねました。
Dan

6
モジュールへの設計の分解に関するテキストを追加しました。あなたの目標は、論理的に関連する関数を、システムの他の部分との参照透過的なインターフェースを持つモジュールに識別し、純粋に機能的なデータ型をできるだけ早く使用して、外界を安全にモデル化することです。:xmonad設計ドキュメントはこのたくさんのカバーxmonad.wordpress.com/2009/09/09/...
ドン・スチュワート

3
HaskellトークのEngineering Large Projectsからスライドをダウンロードしようとしましたが、リンクが壊れているように見えました。これが機能するものです:galois.com/~dons/talks/dons-londonhug-decade.pdf
mik01aj

3
なんとかこの新しいダウンロードリンクを見つけました:pau-za.cz/data/2/sprava.pdf
Riccardo T.

3
@ヘザー直前のコメントで述べたページのダウンロードリンクが機能しない場合でも、スライドはscribdで表示できるようです:scribd.com/doc/19503176/The-Design-and-Implementation-of -xmonad
リカルドT.

118

ドンは上記の詳細のほとんどを説明しましたが、ここにHaskellのシステムデーモンのような本当に骨の折れるステートフルプログラムを実行することからの私の2セントがあります。

  1. 最後に、モナド変換スタックに住んでいます。一番下はIOです。その上に、すべての主要なモジュール(ファイル内のモジュールという意味ではなく、抽象的な意味で)は、必要な状態をそのスタック内のレイヤーにマッピングします。したがって、モジュールにデータベース接続コードが非表示になっている場合は、すべてをタイプMonadReader接続m => ...-> m ...の上に記述し、データベース関数は常に他の関数なしで接続を取得できます。モジュールはその存在を認識する必要があります。データベース接続、別の構成、3番目の並列処理と同期の解決のためのさまざまなセマフォとmvars、別のログファイルハンドルなどを運ぶ1つのレイヤーになるかもしれません。

  2. 最初にエラー処理を理解します。大きなシステムでのHaskellの現時点での最大の弱点は、Maybe(おかしな点に関する情報を返すことができないため間違っている)などのエラー処理メソッドが大量にあることです。欠損値を意味します)。最初にそれをどのように行うのかを理解し、ライブラリや他のコードが使用するさまざまなエラー処理メカニズムから最終的なものにアダプタを設定します。これにより、後で悲しみの世界が救われます。

補遺(コメントから抽出された。おかげLIIliminalishtを) -
スタック内のモナドに大きなプログラムをスライスするさまざまな方法についてのより多くの議論:

Ben Koleraがこのトピックの実用的な紹介を行い、Brian Hurtがliftカスタムモナドにモナドアクションを組み込む問題の解決策について説明します。George Wilsonmtlが、カスタムモナドの種類ではなく、必要な型クラスを実装するモナドで動作するコードを記述する方法を示します。カルロハマライネンは、ジョージの講演を要約した短い、役立つメモを書きました。


5
2つのポイント!この回答には、合理的に具体的であるというメリットがあり、他のものはそうではありません。大きなプログラムをスタック内のモナドにスライスするためのさまざまな方法についての詳細な説明を読むのは興味深いでしょう。リンクがあれば投稿してください!
Lii 2013年

6
@Lii Ben Koleraがこのトピックの実用的な紹介を行い、Brian Hurtがliftカスタムモナドにモナドアクションを組み込む問題の解決策について説明します。George Wilsonmtlが、カスタムモナドの種類ではなく、必要な型クラスを実装するモナドで動作するコードを記述する方法を示します。カルロハマライネンは、ジョージの講演を要約した短い、役に立つメモを書きました。
liminalisht

モナド変換スタックは重要なアーキテクチャ上の基盤になる傾向があることには同意しますが、IOがスタックに入らないようにするために一生懸命努力しています。それが常に可能であるとは限りませんが、モナドで「その後」が何を意味するかを考えると、実際には継続またはオートマトンが下部のどこかにあり、それが「実行」関数によってIOに解釈される場合があります。
ポールジョンソン

@PaulJohnsonがすでに指摘したように、このモナド変圧器スタックアプローチは、マイケルSnoymanさんと競合しているようだReaderTデザインパターン
McBearホールデン

43

Haskellで大規模なプログラムを設計することは、他の言語で設計することとそれほど変わりません。大規模なプログラミングとは、問題を扱いやすい部分に分割し、それらをどのように組み合わせるかです。実装言語はそれほど重要ではありません。

とは言っても、大規模な設計では、型システムを活用して、正しい方法でのみピースを組み合わせることができるようにするのは良いことです。これには、同じタイプのように見えるものを異なるものにするために、newtypeまたはファントムタイプが含まれる場合があります。

コードのリファクタリングに関しては、純粋さは大きな恩恵なので、できるだけ多くのコードを純粋に保つようにしてください。純粋なコードはプログラムの他の部分との隠された相互作用がないため、簡単にリファクタリングできます。


14
実際に、データ型を変更する必要がある場合、リファクタリングは非常にイライラすることがわかりました。多くのコンストラクタとパターンマッチのアリティを面倒に変更する必要があります。(私は、同じタイプの他の純粋な関数に純粋な関数をリファクタリングすることは簡単であることに同意-長いように、1つのデータ型に接触しないよう)
ダン

2
@Danレコードを使用すると、小さな変更(フィールドの追加など)で完全に解放されます。。いくつかは、レコードに習慣(私は彼らの一人です^^ ")を作成することをお勧めします
MasterMastic

5
@Dan関数のデータ型を任意の言語で変更する場合、同じようにする必要はありませんか?この点で、JavaやC ++などの言語がどのように役立つかわかりません。両方のタイプが従うある種の共通のインターフェースを使用できると言えば、HaskellのTypeclassでそれを実行しているはずです。
セミコロン

4
@semicon Javaのような言語の違いは、十分にテストされ、完全に自動化された成熟したリファクタリングツールが存在することです。一般に、これらのツールは素晴らしいエディター統合を備えており、リファクタリングに関連する面倒な作業の膨大な量を取り除きます。Haskellは、リファクタリングで変更する必要のあるものを検出する優れた型システムを提供しますが、実際にそのリファクタリングを実行するためのツールは、特にJavaですでに利用可能なものと比較して、(現時点では)非常に制限されています10年以上のエコシステム。
jsk 2017

16

この本で初めて構造化関数プログラミングを学びました。それはあなたが探しているものとは正確には違うかもしれませんが、関数型プログラミングの初心者にとって、これは、関数型プログラムの構造を学ぶための最良の最初のステップの1つかもしれません-スケールとは無関係です。すべての抽象化レベルで、デザインは常に明確に配置された構造を持つ必要があります。

関数型プログラミングの技

関数型プログラミングの技

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


11
Craft of FPと同じくらい素晴らしいです-私はHaskellをそれから学びました-これは初心者プログラマーのための入門テキストであり、Haskellでの大規模システムの設計のためではありません。
Don Stewart、

3
まあ、それは私がAPIの設計と実装の詳細を隠すことについて知っている最高の本です。この本で、私はC ++のより優れたプログラマーになりました—私がコードを編成するより良い方法を学んだからです。まあ、あなたの経験(と答え)は確かにこの本よりも優れていますが、おそらくDanはまだHaskellの初心者かもしれません。(where beginner=do write $ tutorials `about` Monads
2011

11

「Functional Design and Architecture」というタイトルの本を書いています。純粋な機能的アプローチを使用して大きなアプリケーションを構築する方法の完全なセットを提供します。宇宙船をゼロから制御するためのSCADAのようなアプリケーション「Andromeda」を構築する一方で、多くの機能パターンとアイデアを説明しています。私の主要言語はHaskellです。本の内容:

  • ダイアグラムを使用したアーキテクチャモデリングへのアプローチ。
  • 要件分析;
  • 組み込みDSLドメインモデリング。
  • 外部DSLの設計と実装。
  • 効果を持つサブシステムとしてのモナド。
  • 機能的なインターフェースとしての無料モナド。
  • 矢印の付いたeDSL;
  • 無料のモナディックeDSLを使用した制御の反転。
  • ソフトウェアトランザクションメモリ;
  • レンズ;
  • 状態、リーダー、ライター、RWS、STモナド。
  • 不純な状態:IORef、MVar、STM;
  • マルチスレッドと同時ドメインモデリング。
  • GUI;
  • UML、SOLID、GRASPなどの主流の技術とアプローチの適用性。
  • 不純なサブシステムとの相互作用。

ここの本のコードと「Andromeda」プロジェクトコードに慣れるでしょう。

この本は2017年末に完成する予定です。それが実現するまでは、こちらの記事「関数型プログラミングにおける設計とアーキテクチャ」(Rus)をご覧ください

更新

本をオンラインで共有しました(最初の5章)。Redditの投稿を参照してください


アレクサンダー、あなたが本が完成したらこのノートを親切に更新してもらえますか?乾杯。
最大

4
承知しました!今のところ、テキストの半分を完成させましたが、全体の1/3です。だから、あなたの興味を維持してください、これは私に多くの刺激を与えます!
graninas 2017年

2
こんにちは!本をオンラインで共有しました(最初の5章のみ)。Redditの上の記事を参照してください:reddit.com/r/haskell/comments/6ck72h/...は
graninas

共有して仕事をありがとう!
最大

本当にこれを楽しみにしています!
パトリック

7

Gabrielのブログ投稿スケーラブルなプログラムアーキテクチャは、注目に値するかもしれません。

Haskellの設計パターンは、1つの重要な点で主流の設計パターンと異なります。

  • 従来のアーキテクチャ:タイプAのいくつかのコンポーネントを組み合わせて、タイプBの「ネットワーク」または「トポロジ」を生成します。

  • Haskellアーキテクチャ:タイプAのいくつかのコンポーネントを組み合わせて、同じタイプAの新しいコンポーネントを生成します。これは、置換部分とは特性が区別できません。

見た目はエレガントなアーキテクチャは、この素晴らしい均一性をボトムアップの方法で示すライブラリから抜け落ちる傾向にあることがよくあります。Haskellでは、これは特に明白です。従来「トップダウンアーキテクチャ」と見なされていたパターンは、mvcNetwireCloud Haskellなどのライブラリでキャプチャされる傾向があります。つまり、この回答がこのスレッドの他のいずれかを置き換える試みとして解釈されないことを願っています。構造の選択は、ドメインの専門家によってライブラリで抽象化できるので、理想的です。私の意見では、大規模なシステムを構築する際の本当の難しさは、これらのライブラリーを、それらのライブラリーのアーキテクチャーの「良さ」と実際の懸念のすべてに対して評価することです。

liminalishtがコメントで言及しているように、カテゴリデザインパターンは、同様の傾向でこのトピックに関するGabrielによる別の投稿です。


3
ガブリエルゴンザレスによるカテゴリデザインパターンに関する別の投稿についても触れます。彼の基本的な議論は、私たちの関数型プログラマーが「良いアーキテクチャ」と考えるのは、実際には「構成アーキテクチャ」であるということです。例えば、純粋な機能、モナドアクション、パイプなど-カテゴリ法律はそのアイデンティティと結合性が組成物の下に保存されることを保証するので、組成のアーキテクチャは、我々は、カテゴリを持っている抽象化を使用して達成される
liminalisht


3

おそらく、少し前に戻って、問題の説明を最初から設計にどのように変換するかを考える必要があります。Haskellは非常に高度なレベルであるため、問題の説明をデータ構造、アクションをプロシージャ、純粋な変換を関数としてキャプチャできます。次に、デザインがあります。このコードをコンパイルし、コード内のフィールドの欠落、インスタンスの欠落、モナド変換の欠落に関する具体的なエラーを見つけると、開発が始まります。たとえば、IOプロシージャ内で特定の状態モナドを必要とするライブラリからデータベースアクセスを実行するためです。そして出来上がり、プログラムがあります。コンパイラーはメンタルスケッチをフィードし、設計と開発に一貫性を与えます。

このようにして、最初からHaskellの助けを借りることができ、コーディングは自然です。あなたが考えていることが具体的な通常の問題であるなら、私は「機能的」または「純粋な」何か十分な一般的なことをするつもりはありません。オーバーエンジニアリングはITにおいて最も危険なことだと思います。問題が関連する一連の問題を抽象化するライブラリを作成することである場合、状況は異なります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.