関数型プログラミングの状態問題に対処する


18

私は主にOOPの観点からプログラミングの方法を学びました(私たちのほとんどがそうであるように)が、機能的な方法で問題を解決する方法を学ぶために多くの時間を費やしました。FPで計算上の問題を解決する方法はよく理解していますが、より複雑な問題になると、常に可変オブジェクトが必要になります。たとえば、パーティクルシミュレータを作成している場合、変更可能な位置を持つパーティクル「オブジェクト」を更新する必要があります。本質的に「ステートフル」な問題は、通常、関数型プログラミング手法を使用してどのように解決されますか?


4
最初のステップは、問題が本質的にステートフルではないことを認識している可能性があります。
テラスティン14年

4
データベースへの書き込みやGUIの描画など、いくつかの問題は本質的にステートフルです。私の粒子シミュレーターの例を取り上げると、それについて考える別の方法は何でしょうか?状態を回避するために位置が更新されるたびに新しいパーティクルを返すことは、私にとって非効率的であり、現実の良いモデルではありません。
アンドリューマーティン14年

4
おそらくデータベースの例を除いて、これらの問題は本質的にステートフルではありません。たとえば、GUIプログラミングでは、実際には可変状態を貧弱な暗黙の時間モデルとして使用してます。関数型リアクティブプログラミングでは、結合できるイベントのストリームを提供することにより、状態に依存せずに時間を明示的にモデル化できます。
Tikhon Jelvis

1
より簡単な解決策があります。FPテクニックで簡単にモデル化できない問題に遭遇した場合、関数型プログラミングを使用して解決しないください。 右の仕事のためのツールとすべてのこと...
メイソンウィーラー

1
@AndrewMartin現実世界の良いモデルではありませんか?現実世界をモデル化するために物理学で使用される数学は、純粋に機能的です。適切なガベージコレクターを使用すると、オブジェクトを割り当てるのはポインターをぶつけるのと同じくらい安価で、収集時間はライブオブジェクトの数に比例します。どちらかといえば、関数型プログラミングの非効率性の主な原因は、キャッシュ効率が良くないデータ構造を使用していることでしょう。リンクリストとバイナリツリーは、キャッシュの効率性の単なる子ではありません。
ドーバル

回答:


20

機能プログラムは状態を非常によく処理しますが、それを見る別の方法が必要です。位置の例については、位置を固定値ではなく時間の関数にすることを検討する必要があります。これは、固定された数学的なパスをたどるパーティクルに対してはうまく機能しますが、衝突後など、パスの変更を処理するには別の戦略が必要です。

ここでの基本戦略は、状態を取得して新しい状態を返す関数を作成することです。したがって、パーティクルシミュレータはSet、入力としてa の粒子を取りSet、タイムステップ後に新しい粒子を返す関数になります。次に、その入力を前の結果に設定して、その関数を繰り返し呼び出します。


5
+1 変更可能な状態ではなく、FPに状態を保持してもかまいません。
ジェレット14年

1
この洞察力に感謝します。非効率性についての私の心配は、@ logcによって阻止されました。状態がどのように変換されるかの技術的な詳細は、言語自体が解決することになっている低レベルの実装の問題です。Rich HickeyがClojureでこれをどのように行うかをビデオで説明しました。
アンドリューマーティン14年

1
@jhewlett:もっと正確に言うと、FPには状態、さらには可変状態がありますが、可変変数を使用してそれを表すことはありません。
ジョルジオ

9

@KarlBielefeldtが指摘したように、このような問題に対する機能的なアプローチは、以前の状態から新しい状態を返すと見なすことです。関数自体は情報を保持しないため、常に状態mを状態nに更新します。

新しい状態を計算している間、以前の状態をメモリに保持する必要があると仮定するため、この非効率性を見つけると思います。完全に新しい状態を記述するか、古い状態を所定の位置に書き直すかの選択は、関数型言語の観点からの実装の詳細であることに注意してください。

たとえば、100万個の整数のリストがあり、10単位を1単位増やしたいとします。10番目の位置に新しい番号を付けてリスト全体をコピーするのは無駄です。ただし、これは操作を言語コンパイラーまたはインタープリターに説明する概念的な方法にすぎません。コンパイラまたはインタープリターは、最初のリストを自由に取得して、10番目の位置を上書きするだけです。

この方法で操作を記述することの利点は、多くのスレッドが異なる位置で同じリストを更新したい状況についてコンパイラーが推論できることです。操作が「この位置に移動して、見つけたものを上書きする」と記述されている場合、上書きが衝突しないことを確認するのは、コンパイラーではなくプログラマーです。

以上のことから、Haskellでも、「状態を維持する」ことが問題に対するより直感的な解決策である状況をモデル化するのに役立つStateモナドがあります。しかしデータベースへの書き込みのように本質的にステートフルな問題には、Datomicのような不変のソリューションあることに注意してください。これは概念であり、必ずしもその実現ではないことを理解するまで驚くかもしれません。


4
大きなリストの更新に関するスニペットは誤解を招くと思います。実際に最適化を実行するコンパイラーは知りません。コンパイラーがそれを実行できたとしても、リストの以前のバージョンを保持していない場合にのみ可能です。真の解決策は、単一の要素を変更するために全体をコピーする必要はありませんリストデータ構造を使用することです。
ドーバル

@Doval:「コンパイラーがそれを行うことができたとしても、リストの以前のバージョンを保持しない場合にのみ可能です。」:これは、Cleanの一意の型を思い出させます。
ジョルジオ

4

適切なメンタルモデルをサブスクライブすると、状態をよりよく考えて管理できます。私の考えでは、最高のメンタルモデルはフリップブックです。これをクリックすると、FPは世界の状態をキャプチャする永続的なデータ構造に大きく依存し、関数を使用してその状態をまったく変更することなく遷移することがわかります。

リッチヒッキーはこれらのアイデアを明らかにします。

他の講演もあります、これはあなたを正しい方向に導くでしょう。


3

大規模および中規模のアプリケーションを作成するとき、アプリケーションのステートフルなセクションとステートレスなセクションを区別すると便利なことがよくあります。

ステートフルセクションのクラス/データ構造は、アプリケーションのデータを格納し、このセクションの関数は、アプリケーションのデータの暗黙的な知識で動作します。

ステートレスセクションのクラス/データ構造/関数は、アプリケーションの純粋なアルゴリズムの側面をサポートするためにあります。アプリケーションのデータに関する暗黙の知識はありません。彼らは純粋に機能的な性質で動作します。アプリケーションのステートフル部分では、アプリケーションのステートレスセクションで関数を実行する副作用として、状態の変化が発生する場合があります。

最も難しいのは、どのクラス/関数をステートレスセクションに配置し、どのクラス/関数をステートフルセクションに配置するかを把握し、それらを別のファイル/ライブラリに配置する規律を持たせることです。


これは質問にどのように答えますか?(非
投票

@ kravemir、OOPを使用してアプリケーションを作成する場合でも、FPを使用してアプリケーションを作成する場合でも、アプリケーションの状態がどこにあるかを理解する必要があります。
Rサフ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.