1. ホーム
  2. haskell

[解決済み] Haskellで大規模設計?[クローズド]

2022-03-18 12:27:23

質問

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

チュートリアルを何度も読みましたが(Write Yourself a Schemeが一番好きで、Real World Haskellはその次)、ほとんどのプログラムは比較的小規模で、単一目的です。 また、一部のプログラムは特にエレガントだとは思いません(例えば、WYASの膨大なルックアップテーブルなど)。

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

大規模なオブジェクト指向の命令型プログラムについて、このような問題を扱う文献はかなり多く存在します。MVCやデザインパターンなどの考え方は、関心事の分離や再利用性といった広い目標をOOスタイルで実現するための適切な処方箋と言えます。 さらに、新しい命令型言語は、「成長に合わせて設計する」スタイルのリファクタリングに適していますが、私の素人考えでは、Haskellはあまり適していないように見えます。

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

ありがとうございます。

EDIT(これはDon Stewartの答えのフォローアップです)。

モナドは型における重要なアーキテクチャ設計を捉えています。

純粋関数型言語における主要なアーキテクチャ設計についてどのように考えるべきか、というのが私の質問です。

いくつかのデータストリームと、いくつかの処理ステップの例を考えてみましょう。 私はデータストリームをデータ構造のセットに対してモジュール式パーサーを書くことができますし、各処理ステップを純粋な関数として実装することも可能です。 あるデータに対して必要な処理手順は、そのデータの値や他のデータの値によって異なる。 いくつかのステップでは、GUIの更新やデータベースのクエリのような副作用が続くはずです。

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

彼がリンクしたスライドには、Things we Needという箇条書きで、"デザインを 型/関数/クラス/モナド". イディオムってなんですか?)

解決方法は?

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

型システム

  • 型システムを使用して抽象化を強制し、相互作用を単純化する。
  • 型によるキーインバリアントの強制
    • (例えば、ある値があるスコープを抜け出せないなど)
    • その特定のコードは、IOを行わず、ディスクに触れません。
  • 安全性の確保:例外チェック(Maybe/Either)、概念の混在回避(Word、Int、Address)。
  • 優れたデータ構造(ジッパーなど)は、境界外エラーなどを静的に排除するため、ある種のテストが不要になることがあります。

プロファイラ

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

純度

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

テスト

  • QuickCheck + Haskell Code Coverageで、型ではチェックできない部分を確実にテストします。
  • GHC + RTSは、GCに時間をかけすぎていないかどうかを確認するのに適しています。
  • QuickCheckは、モジュールのためのクリーンで直交するAPIを特定するのにも役立ちます。コードのプロパティの記述が困難な場合は、複雑すぎる可能性があります。コードをテストでき、うまく構成できるきれいなプロパティのセットができるまで、リファクタリングを続けましょう。そうすれば、そのコードもきっとうまく設計されているはずです。

構造化のためのモナド

  • モナドは、主要なアーキテクチャ設計を型に取り込む(このコードはハードウェアにアクセスする、このコードはシングルユーザーセッションである、など)。
  • 例えば、xmonadのXモナドは、システムのどのコンポーネントからどの状態が見えるか、という設計を正確に捉えている。

型クラスと実存型

  • 型クラスを使って抽象化を行う:実装を多相インターフェースに隠す。

並行処理と並列処理

  • スニーク par をプログラムに組み込むことで、簡単で合成可能な並列処理で競合に打ち勝つことができます。

リファクタリング

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

FFIを賢く使う

  • FFIを使うと、外国のコードを簡単に弄れるようになりますが、その外国のコードが危険な場合もあります。
  • 返されるデータの形についての仮定には、十分な注意が必要です。

メタプログラミング

  • Template Haskellやジェネリックを少し使うだけで、定型文を取り除くことができます。

パッケージングと配布

  • Cabalを使用する。独自のビルドシステムを展開しないでください。 (編集: 実際は、おそらく スタック を使用します)。
  • 優れたAPIドキュメントのためにHaddockを使用する
  • こんなツール グラフモッド は、モジュール構造を表示することができます。
  • 可能な限り、Haskell Platformバージョンのライブラリやツールに頼ってください。安定したベースとなります。 (編集部:繰り返しになりますが、最近だと スタック は、安定したベースを立ち上げるためのものです)。

注意事項

  • 使用方法 -Wall を使えば、匂いのないきれいなコードを保つことができます。AgdaやIsabelle、Catchを使うとより確実かもしれません。lintライクなチェックには、偉大な hlint 改善点を提案してくれます。

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

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