神様私は「コードの臭い」という言葉が嫌いですが、これ以上正確なことは考えられません。
私は高水準言語とコンパイラーを 空白 に設計して、コンパイラーの構築、言語設計、関数型プログラミングについて学びます(コンパイラーはHaskellで書かれています)。
コンパイラのコード生成フェーズでは、構文ツリーをトラバースするときに「状態」のようなデータを維持する必要があります。たとえば、フロー制御ステートメントをコンパイルするときに、ジャンプ先のラベルの一意の名前を生成する必要があります(渡され、更新され、返されるカウンターから生成されたラベル。カウンターの古い値を二度と使用しないでください)。もう1つの例は、構文ツリーでインライン文字列リテラルに遭遇した場合、それらを永続的にヒープ変数に変換する必要があることです(空白では、文字列はヒープに格納するのが最適です)。私は現在、これを処理するためにコード生成モジュール全体を状態モナドでラップしています。
コンパイラーを書くことは関数型パラダイムによく適した問題だと言われましたが、私はこれをCで設計するのとほとんど同じ方法で設計していることがわかりました(実際にはどの言語でもCを書くことができます-でもHaskell w /州のモナド)。
Haskell構文のCではなく、Haskellで(むしろ関数型パラダイムで)考える方法を学びたいです。ステートモナドの使用を本当に排除/最小化する必要がありますか、それとも正当な機能的な「デザインパターン」ですか?
状態が小さく、適切に制御されている限り、一般的に状態はコードの臭いではないと思います。
つまり、State、ST、カスタムビルドなどのモナドを使用したり、いくつかの場所に渡す状態データを含むデータ構造を使用したりすることは悪いことではありません。 (実際、モナドはまさにこれを行うための助けにすぎません!)しかし、あちこちにある状態を持つこと(そう、これはあなたを意味します、IOモナド!)は悪臭です。
これのかなり明確な例は、私のチームが ICFPプログラミングコンテスト2009 (コードはgit://git.cynic.net/haskell/icfp-contest-で入手可能)のエントリに取り組んでいたときでした。 2009)。これには、いくつかの異なるモジュラーパーツが必要になりました。
これらはそれぞれ独自の状態を持っており、VMの入力値と出力値を介してさまざまな方法で相互作用します。いくつかの異なるコントローラーとビジュアライザーがあり、それぞれに異なる種類の状態がありました。
ここで重要な点は、特定の状態の内部は特定のモジュールに限定されており、各モジュールは他のモジュールの状態の存在についてさえ何も知らないということでした。ステートフルコードとデータの特定のセットは、通常、数十行の長さで、州内には少数のデータ項目がありました。
これはすべて、どの州の内部にもアクセスできず、シミュレーションをループするときに適切な順序で正しいものを呼び出すだけで、非常に限られたものを通過する、約12行の1つの小さな関数にまとめられました。各モジュールへの外部情報の量(もちろん、モジュールの以前の状態とともに)。
状態がこのように制限された方法で使用され、型システムが誤って状態を変更することを防いでいる場合、処理は非常に簡単です。これを可能にするのはHaskellの美しさの1つです。
ある答えは、「モナドを使わないでください」と言っています。私の見解では、これはまったく逆です。モナドは、とりわけ、状態に触れるコードの量を最小限に抑えるのに役立つ制御構造です。例としてモナドパーサーを見ると、解析の状態(つまり、解析されているテキスト、そこに到達した距離、蓄積された警告など)は、パーサーで使用されるすべてのコンビネーターを通過する必要があります。 。しかし、実際に状態を直接操作するコンビネータはごくわずかです。他のものはこれらのいくつかの機能の1つを使用します。これにより、状態を変更できる少量のコードをすべて1か所で明確に確認でき、変更方法をより簡単に推論できるため、処理が容易になります。
属性文法 (AG)を見たことがありますか? (モナドリーダーの ウィキペディア および 記事 に関する詳細情報)?
AGを使用すると、属性を構文ツリーに追加できます。これらの属性は、合成属性と継承属性に分けられます。
合成された属性は、構文ツリーから生成(または合成)するものです。これは、生成されたコード、すべてのコメント、またはその他の関心のあるものです。
継承された属性は構文ツリーに入力されます。これは、環境、またはコード生成中に使用するラベルのリストである可能性があります。
ユトレヒト大学では、 属性文法システム ( [〜#〜] uuagc [〜#〜] )を使用してコンパイラを記述しています。これは、提供された.hs
ファイルからhaskellコード(.ag
ファイル)を生成するプリプロセッサです。
ただし、Haskellをまだ学習している場合は、その上にさらに別の抽象化レイヤーを学習し始める時期ではないかもしれません。
その場合、次のように、属性文法が生成する種類のコードを手動で記述できます。
data AbstractSyntax = Literal Int | Block AbstractSyntax
| Comment String AbstractSyntax
compile :: AbstractSyntax -> [Label] -> (Code, Comments)
compile (Literal x) _ = (generateCode x, [])
compile (Block ast) (l:ls) = let (code', comments) = compile ast ls
in (labelCode l code', comments)
compile (Comment s ast) ls = let (code, comments') = compile ast ls
in (code, s : comments')
generateCode :: Int -> Code
labelCode :: Label -> Code -> Code
モナドの代わりにアプリケーションファンクターが必要になる可能性があります。
http://www.haskell.org/haskellwiki/Applicative_functor
ただし、元の論文ではwikiよりも説明が優れていると思います。
State Monadを使用することは、状態をモデル化するために使用されたときのコードの臭いではないと思います。
関数に状態をスレッド化する必要がある場合は、状態を引数として取得し、各関数で返すことで、これを明示的に行うことができます。 State Monadは、優れた抽象化を提供します。状態を渡し、状態を必要とする関数を組み合わせるための便利な関数を多数提供します。この場合、State Monad(またはApplicatives)を使用することはコードの臭いではありません。
ただし、State Monadを使用して命令型のプログラミングをエミュレートし、機能的なソリューションで十分な場合は、事態が複雑になります。
一般に、可能な限り状態を回避するように努める必要がありますが、それが常に実用的であるとは限りません。 Applicative
は、効果的なコードをより見栄えよく機能的にします。特に、ツリートラバーサルコードはこのスタイルの恩恵を受けることができます。名前生成の問題については、かなり素敵なパッケージが利用可能になりました: value-supply 。