563

特にHaskellで大規模な機能プログラムを設計/構造化するための良い方法は何ですか?

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

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

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

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

ありがとう!

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

@donsは次のように述べています。「モナドは主要な建築設計をタイプで捉えています。」

私の質問は、純粋な関数型言語での主要な建築設計についてどのように考えるべきかということだと思います。

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

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

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

4

8 に答える 8

516

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

型システム

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

プロファイラー

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

純度

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

テスト

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

構造化のためのモナド

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

型クラスと実存型

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

並行性と並列性

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

リファクタリング

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

FFIを賢く使用する

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

メタプログラミング

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

包装と流通

  • Cabalを使用します。独自のビルドシステムを導入しないでください。(編集:実際には、開始するために今すぐStackを使用することをお勧めします。)
  • 優れたAPIドキュメントにはHaddockを使用してください
  • graphmodのようなツールは、モジュール構造を表示できます。
  • 可能であれば、HaskellPlatformバージョンのライブラリとツールに依存します。安定したベースです。(編集:繰り返しになりますが、最近では、安定したベースを稼働させるためにStackを使用する可能性があります。)

警告

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

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

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

于 2010-06-20T01:42:05.047 に答える
117

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

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

  2. 最初にエラー処理を理解してください。大規模なシステムでのHaskellの現時点での最大の弱点は、Maybeのようなお粗末な方法を含む多数のエラー処理方法です(これは、問題の情報を返すことができないため間違っています。本当にそうでない場合は、Maybeの代わりに常にEitherを使用してください)欠落している値を意味するだけです)。最初にそれをどのように行うかを理解し、ライブラリや他のコードが使用するさまざまなエラー処理メカニズムから最終的なものにアダプタを設定します。これは後であなたに悲しみの世界を救うでしょう。

補遺(コメントから抜粋。Lii&liminalishtに感謝大きなプログラム
をスタック内のモナドにスライスするさまざまな方法についての詳細:

Ben Koleraがこのトピックの実用的なイントロを提供し、BrianHurtliftがモナドアクションをカスタムモナドに組み込む問題の解決策について説明します。George Wilsonmtlは、カスタムモナドの種類ではなく、必要な型クラスを実装する任意のモナドで機能するコードを作成する方法を示しています。Carlo Hamalainenは、Georgeの話を要約した短くて役立つメモをいくつか書いています。

于 2010-06-21T10:39:08.187 に答える
43

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

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

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

于 2010-06-20T09:29:46.157 に答える
16

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

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

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

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

于 2010-10-17T23:23:50.793 に答える
11

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

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

ここで本のコードと「Andromeda」プロジェクトコードに精通しているかもしれません。

この本は2017年末に完成する予定です。それまでは、私の記事「関数型プログラミングの設計とアーキテクチャ」(Rus)をここで読むことができます。

アップデート

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

于 2016-11-20T05:25:58.980 に答える
7

Gabrielのブログ投稿スケーラブルなプログラムアーキテクチャは言及する価値があるかもしれません。

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

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

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

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

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

于 2014-08-31T10:07:39.960 に答える
5

私は、AlejandroSerranoによる論文TeachingSoftwareArchitecture Using Haskell」(pdf)が、Haskellの大規模構造について考えるのに役立つことを発見しました。

于 2016-02-18T19:24:30.863 に答える
3

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

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

于 2013-04-30T08:50:39.170 に答える