割り当てなしで状態を維持する


10

関数型プログラミングを学んでいて、割り当てを使用せずに特定のシナリオを実装する方法を理解できません。次の簡単な問題は、私の混乱をかなり要約しています。

特定のデータ構造の変更に関するイベントを受け取り、このデータ構造が特定の状態に達したときにイベントを発行するプログラムを記述します。

だから私は維持しているデータ構造のコピーを持っています

datastructure_copy::DataStructure 

変化したときに発生するイベントのストリームがあります。

datastructure_changes::Stream Change

データ構造に変更を適用して新しいコピーを返す関数があります。

apply_change::Change -> DataStructure -> DataStructure

そして、データの状態が目的の状態に達したかどうかをチェックする述語があります。

is_ready::DataStructure ->Boolean

つまり、ストリームで機能する「reduce」のようなものが必要です。

これを実装する1つの方法は、変更が到着するたびに状態を再計算することですが、これは実際的ではないようです。私はStateモナドで少し遊んでみましたが、別の問題を解決することを意図しているように見えます。

それを行う別の方法はありますか?

私の質問は純粋に概念的なものであり、Haskellにはあまり詳しくありません。


いずれにしても、Haskellでは「割り当て」(割り当てとバインディングの違いに注意)は表示されません。関数型言語"」。状態モナドはあなたが探しているものでなければなりません、あなたはそれを使う方法を学ぶ必要があるだけです。よろしければ、本日後半に、より包括的な回答を差し上げます。
Francesco Gramano

回答:


2

これを実装する1つの方法は、変更が到着するたびに状態を再計算することですが、これは実際的ではないようです。

イベントの発生時に適用された変更が分散的でない場合、何らかの形で、最終状態は初期状態とそれに続く変化に過ぎないため、イベントが発生するたびに状態を再計算する必要があります。また、変更が分散的である場合でも、通常、状態を次の状態に連続的に変換する必要があります。これは、特定の状態に達したらすぐにプロセスを停止し、次の状態を計算して次の状態を判断する必要があるためです。新しいのは指名手配状態です。

関数型プログラミングでは、状態の変化は通常、関数呼び出しや関数パラメーターによって表されます。

最終状態がいつ計算されるか予測できないため、非末尾再帰関数を使用しないでください。各状態が前の状態に基づいている状態のストリームは、良い代替案になる可能性があります。

だからあなたの場合、私はScalaで次のコードで質問に答えます:

import scala.util.Random

val initState = 0.0
def nextState(state: Double, event: Boolean): Double = if(event) state + 0.3 else state - 0.1 // give a new state
def predicate(state: Double) = state >= 1

// random booleans as events
// nb: must be a function in order to force Random.nextBoolean to be called for each  element of the stream
def events(): Stream[Boolean] = Random.nextBoolean #:: events()  

val states: Stream[Double] = initState #:: states.zip(events).map({ case (s,e) => nextState(s,e)}) // a stream of all the successive states

// stop when the state is >= 1 ;
// display all the states computed before it stopped
states takeWhile(! predicate(_)) foreach println 

たとえば、次のようになります(出力を簡略化しました)。

0.0
0.3
0.2
0.5
0.8

val states: Stream[Double] = ... 連続する状態が計算される行です。

このストリームの最初の要素は、システムの初期状態です。zip状態のストリームをイベントのストリームとマージして、各ペアが(状態、イベント)である要素のペアの単一のストリームにマージします。map各ペアを、古い状態と関連するイベントの関数として計算された新しい状態である単一の値に変換します。したがって、新しい状態は、以前に計算された状態と、その状態を「変更」する関連イベントです。

したがって、基本的に、潜在的に無限の状態のストリームを定義します。新しい状態はそれぞれ、最後に計算された状態の関数であり、新しいイベントです。Scalaではストリームが(特に)遅延しているため、オンデマンドでしか計算されないため、無駄な状態を計算する必要がなく、必要なだけ状態を計算できます。

述語を尊重する最初の状態のみに関心がある場合は、コードの最後の行を次のように置き換えます。

states find predicate get

取得するもの:

res7: Double = 1.1

マジックを行うラインについての洞察を提供していただけますか:val states: Stream[Double]...
ボビー・マリノフ2015年

承知しました。私の編集を見てください。
mgoeminne 2015年

1

あなたはあなたが2つの機能を持っていると言います:

apply_change::Change -> DataStructure -> DataStructure
is_ready::DataStructure ->Boolean

そして私があなたを正しく理解しているなら、それis_readyはかなり高価なので、すべての変更イベントに対して何度もそうしたくないでしょう。

必要なのは、関数が初期DataStructureを取り、それを単純な状態に凝縮し、凝縮状態、変更を取り、新しい凝縮状態を出力する関数です。

DataStructureがトリプレットでx,y,zあり、x、y、zが素数になるのを待っているとします。この場合、圧縮された状態は、x、y、zが素数ではないセットになる可能性があります。xを素数にする変更は、セットからxを削除します。xを素数にしない変更は、xをセットに追加します(存在しない場合)。DataStructureは準備ができており、セットは空です。

つまり、圧縮された状態を更新する方が、DataStructureを更新してis_readyを最初から計算するよりもはるかに安価です。

注:さらに優れた方法は、x、y、zのどれが素数であるかをチェックし、どこにあるかを追跡することです。すべての変更について、関連するフィールドにチェックされていないフラグを付けます。次にis_readyが呼び出されたら、確認して覚えます。xは複数回変更される可能性があり、素数を1回だけチェックするため、すべての変更の後にis_readyをチェックしない場合、これはより良い方法です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.