特にHaskellで、大規模な機能プログラムを設計/構築する良い方法は何ですか?
私はたくさんのチュートリアルを経験しました(筆者は自分の好きなSchemeを書いて、Real World Haskellがすぐ近くにあります)-しかし、ほとんどのプログラムは比較的小さく、単目的です。さらに、それらの一部は特にエレガントであるとは考えていません(たとえば、WYASの膨大なルックアップテーブル)。
さまざまなソースからのデータの取得、クリーニング、さまざまな方法での処理、ユーザーインターフェイスでの表示、永続化、ネットワーク経由の通信など、より多くの可動部分を備えたより大きなプログラムを作成したいと考えています。このようなコードを読みやすく、保守しやすく、変化する要件に適応できるようにする最適な構造はありますか?
大規模なオブジェクト指向の命令型プログラムに関するこれらの質問を扱った非常に多くの文献があります。 MVC、デザインパターンなどのアイデアは、懸念の分離やOOスタイルの再利用性などの幅広い目標を実現するための適切な処方箋です。さらに、新しい命令型言語は、「成長に合わせて設計する」スタイルのリファクタリングに適しています。これは、私の初心者の意見では、Haskellはあまり適していないようです。
Haskellの同等の文献はありますか?この目的のために最もよく採用されている関数型プログラミング(モナド、矢印、適用など)で、エキゾチックな制御構造の動物園はどのように利用できますか?どのようなベストプラクティスをお勧めしますか?
ありがとう!
編集(これはドン・スチュワートの答えのフォローアップです):
@donsが述べた:「モナドは、主要な建築デザインをタイプでキャプチャします。」
私の質問は、純粋な関数型言語の主要なアーキテクチャ設計についてどう考えるべきかということだと思います。
いくつかのデータストリームといくつかの処理ステップの例を考えてみましょう。データストリームのモジュールパーサーを一連のデータ構造に書き込むことができ、各処理ステップを純粋な関数として実装できます。 1つのデータに必要な処理手順は、その値と他のデータによって異なります。手順の一部には、GUIの更新やデータベースクエリなどの副作用が続く必要があります。
データと解析ステップを素敵な方法で結びつける「正しい」方法は何ですか?さまざまなデータ型に対して正しいことを行う大きな関数を作成できます。または、モナドを使用して、これまでに処理されたものを追跡し、各処理ステップで次に必要なものをモナド状態から取得することができます。または、大きく分けてプログラムを作成してメッセージを送信することもできます(このオプションはあまり好きではありません)。
彼がリンクしたスライドには、「必要なもの」の箇条書き「デザインを型/関数/クラス/モナドにマッピングするイディオム」があります。イディオムは何ですか? :)
Haskellでの大規模プロジェクトのエンジニアリング および XMonadの設計と実装 でこれについて少しお話します。大規模なエンジニアリングとは、複雑さを管理することです。 Haskellの複雑さを管理するための主要なコード構造化メカニズムは次のとおりです。
タイプシステム
プロファイラー
純度
テスト
構造化のためのモナド
型クラスと存在型
並行性と並列性
par
を忍び込ませて、簡単で構成可能な並列処理で競合他社に勝ちます。リファクタリング
FFIを賢く使用する
メタプログラミング
包装と流通
警告
-Wall
を使用して、コードの臭いをきれいに保ちます。また、Agda、Isabelle、またはCatchを見て、より確実に確認することもできます。リントのようなチェックについては、改善を提案する素晴らしい hlint を参照してください。これらすべてのツールを使用すると、複雑さを解消し、コンポーネント間の相互作用を可能な限り削除できます。理想的には、純粋なコードの非常に大きなベースがあります。これは、構成が非常に簡単なため、保守が非常に簡単です。それは常に可能というわけではありませんが、目標とする価値があります。
一般に:decomposeシステムの論理ユニットを可能な限り最小の参照透過コンポーネントに分解し、モジュールに実装します。コンポーネントのセット(または内部コンポーネント)のグローバルまたはローカル環境は、モナドにマッピングされる場合があります。代数データ型を使用して、コアデータ構造を記述します。これらの定義を広く共有します。
ドンは上記のほとんどの詳細を説明しましたが、Haskellのシステムデーモンのような非常にきめの細かいステートフルプログラムを実行して得た2セントです。
最終的に、あなたはモナド変換スタックに住んでいます。一番下はIOです。その上で、すべての主要なモジュール(ファイル内のモジュールの意味ではなく、抽象的な意味で)は、そのスタック内のレイヤーに必要な状態をマップします。したがって、モジュールにデータベース接続コードが隠されている場合、MonadReader Connection m => ...-> m ...というタイプを介してすべてを書くと、データベース関数は常に他の関数から関数なしで接続を取得できますモジュールはその存在に注意する必要があります。データベース接続、別の構成、3番目のさまざまなセマフォと並列処理と同期の解決のためのmvar、別のログファイルハンドルなどを保持する1つの層になることがあります。
エラー処理を理解しますfirst。大規模なシステムでのHaskellの現時点での最大の弱点は、Maybeのようなお粗末な方法を含むエラー処理方法が多すぎることです(これは間違っていることに関する情報を返すことができないため間違っています。単に欠損値を意味します)。最初にそれを行う方法を理解し、ライブラリやその他のコードが使用するさまざまなエラー処理メカニズムから最終的なアダプターにアダプターをセットアップします。これは後で悲しみの世界を救うでしょう。
補遺(コメントから抜粋; Lii & liminalisht )のおかげで—
大きなプログラムをスタックのモナドにスライスするさまざまな方法についての詳細な議論:
Ben Kolera は、このトピックの実用的な入門書です。 Brian Hurt は、カスタムモナドへのlift
ingモナドアクションの問題の解決策について説明します。 George Wilson は、mtl
を使用して、カスタムモナドの種類ではなく、必要なタイプクラスを実装するモナドで動作するコードを記述する方法を示します。 Carlo Hamalainen は、ジョージの講演を要約した短い有用なメモを書いています。
Haskellで大規模なプログラムを設計することは、他の言語で行うこととそれほど変わりません。大規模なプログラミングとは、問題を管理可能な部分に分割し、それらをどのように組み合わせるかです。実装言語はそれほど重要ではありません。
とは言っても、大規模なデザインでは、型システムを活用して、正しい方法でしかピースを合わせられないようにすることをお勧めします。これには、同じタイプを持つように見えるものを異なるものにするために、newtypeまたはファントムタイプが含まれる場合があります。
進むにつれてコードをリファクタリングすることになると、純粋さは大きな恩恵になるので、可能な限り純粋なコードを維持するようにしてください。純粋なコードは、プログラムの他の部分との隠れた相互作用がないため、リファクタリングが簡単です。
この本 で初めて構造化された関数型プログラミングを学びました。それはまさにあなたが探しているものではないかもしれませんが、関数型プログラミングの初心者にとって、これは関数型プログラムを構築することを学ぶための最良の最初のステップの1つかもしれません-規模に依存しません。すべての抽象化レベルで、設計には常に明確に配置された構造が必要です。
関数型プログラミングのクラフト
現在、「Functional Design and Architecture」というタイトルの本を書いています。純粋な機能アプローチを使用して大きなアプリケーションを構築する方法の完全なセットを提供します。宇宙船をゼロから制御するためのSCADAのようなアプリケーション「アンドロメダ」を構築しながら、多くの機能的なパターンとアイデアを説明します。私の第一言語はHaskellです。本は以下をカバーしています:
この本のコード here 、および 'Andromeda' プロジェクトコードに精通しているかもしれません。
この本は2017年末に完成する予定です。それまでは、私の記事「関数型プログラミングの設計とアーキテクチャ」(Rus) here をお読みください。
UPDATE
本をオンラインで共有しました(最初の5章)。 Redditに投稿 を参照してください
Gabrielのブログ投稿 スケーラブルなプログラムアーキテクチャ は言及する価値があるかもしれません。
Haskellのデザインパターンは、主流のデザインパターンと1つの重要な点で異なります。
従来のアーキテクチャ:タイプAのいくつかのコンポーネントを組み合わせて、タイプBの「ネットワーク」または「トポロジ」を生成します
Haskellアーキテクチャ:タイプAのいくつかのコンポーネントを組み合わせて、置換部分と特性が区別できない同じタイプAの新しいコンポーネントを生成します
見たところエレガントなアーキテクチャは、このニースの同質性を示すライブラリからボトムアップで抜け落ちがちであることがよくあります。 Haskellでは、これは特に明らかです。伝統的に「トップダウンアーキテクチャ」と見なされるパターンは、 mvc 、 Netwire 、および Cloud Haskellのようなライブラリでキャプチャされる傾向があります 。つまり、この答えがこのスレッドの他のいずれかを置き換える試みとして解釈されることはないことを願っています。ドメインの専門家がライブラリ内で構造的な選択を抽象化することができます。私の意見では、大規模システムを構築する際の本当の難しさは、これらのライブラリーを、そのアーキテクチャーの「良さ」とあなたの実際的な懸念のすべてで評価することです。
liminalisht がコメントの中で言及しているように、 The category design pattern は、同様の方法でトピックに関するGabrielによる別の投稿です。
アレハンドロセラーノによる論文 "Takeing Software Architecture Using Haskell"(pdf) は、大規模な思考に役立つHaskellの構造。
おそらく、あなたは最初に戻って、最初に問題の説明を設計に変換する方法を考える必要があります。 Haskellは非常にレベルが高いため、問題の説明をデータ構造の形で、アクションをプロシージャとして、純粋な変換を関数としてキャプチャできます。次に、デザインがあります。このコードをコンパイルすると、コードのフィールド、インスタンス、モナド変換子の欠落に関する具体的なエラーが見つかると、開発が開始されます。たとえば、IO内で特定の状態モナドを必要とするライブラリからデータベースアクセスを実行する場合手順。そして出来事、プログラムがあります。コンパイラはメンタルスケッチをフィードし、設計と開発に一貫性を与えます。
このように、最初からHaskellの助けを借りることができ、コーディングは自然です。あなたが念頭に置いていることが具体的な普通の問題であるなら、私は何か「機能的」または「純粋な」または十分に一般的なことをする気にしないでしょう。過剰なエンジニアリングはITで最も危険なことだと思います。問題が関連する一連の問題を抽象化するライブラリを作成することである場合、状況は異なります。