私はHaskellで [〜#〜] uct [〜#〜] アルゴリズムの実装に取り組んでいますが、これにはかなりの量のデータジャグリングが必要です。詳細に立ち入ることなく、これはシミュレーションアルゴリズムであり、各「ステップ」で、検索ツリーのリーフノードがいくつかの統計プロパティに基づいて選択され、そのリーフに新しい子ノードが構築され、それに対応する統計情報が表示されます。新しいリーフとそのすべての祖先が更新されます。
そのすべてのジャグリングを考えると、私は検索ツリー全体を素敵な不変のデータ構造にする方法を理解するのに十分なほど鋭敏ではありません 岡崎 。代わりに、私はST
モナドを少しいじって、可変のSTRef
sで構成される構造を作成してきました。不自然な例(UCTとは無関係):
import Control.Monad
import Control.Monad.ST
import Data.STRef
data STRefPair s a b = STRefPair { left :: STRef s a, right :: STRef s b }
mkStRefPair :: a -> b -> ST s (STRefPair s a b)
mkStRefPair a b = do
a' <- newSTRef a
b' <- newSTRef b
return $ STRefPair a' b'
derp :: (Num a, Num b) => STRefPair s a b -> ST s ()
derp p = do
modifySTRef (left p) (\x -> x + 1)
modifySTRef (right p) (\x -> x - 1)
herp :: (Num a, Num b) => (a, b)
herp = runST $ do
p <- mkStRefPair 0 0
replicateM_ 10 $ derp p
a <- readSTRef $ left p
b <- readSTRef $ right p
return (a, b)
main = print herp -- should print (10, -10)
明らかに、この特定の例はST
を使用せずに作成する方がはるかに簡単ですが、うまくいけば、これをどこに使用するかが明確になります...この種のスタイルをUCTのユースケースに適用する場合は、頭がおかしい?
誰かが 同様の質問 ここで数年前に質問しましたが、私の質問は少し違うと思います...必要に応じてモナドを使用して可変状態をカプセル化することに問題はありませんが、それは「適切な場合」です私を取得する句。ゲッターとセッターを備えたオブジェクトがたくさんあるオブジェクト指向の考え方に時期尚早に戻ってしまうのではないかと心配しています。正確には慣用的なHaskellではありません...
一方、それがisである場合、いくつかの問題のセットに対して妥当なコーディングスタイルである場合、私の質問は次のようになります。この種のコードを読みやすく、保守しやすくしますか?私は、すべての明示的な読み取りと書き込みにうんざりしています。特に、STRef
モナド内のST
ベースの構造から、外部の同形であるが不変の構造に変換する必要があるため、うんざりしています。
私はSTをあまり使用しませんが、それが最善の解決策である場合もあります。これは多くのシナリオで発生する可能性があります。
ST(および他のモナド)を使用するときは、次の一般的なガイドラインに従おうとします。
STRef s (Map k [v])
のようなものをよく使用します。マップ自体は変更されていますが、手間のかかる作業の多くは純粋に行われます。IORef
sをSTRef
sに置き換え、IO
sをST
sに置き換える方がはるかに簡単でした。コード化されたハッシュテーブルの実装は、おそらくより高速でした。最後にもう1つ注意してください-明示的な読み取りと書き込みで問題が発生した場合は、 その周りに があります。
突然変異を利用するアルゴリズムとそうでないアルゴリズムは異なるアルゴリズムです。前者から後者への翻訳を維持するという単純な限界がある場合もあれば、難しいものもあれば、複雑さの限界を維持しないものだけがある場合もあります。
論文をざっと見ると、突然変異を本質的に利用しているとは思わないことがわかります。したがって、潜在的に本当に気の利いた怠惰な機能的アルゴリズムを開発できると思います。しかし、それは説明されているものとは異なりますが、関連するアルゴリズムになります。
以下に、そのようなアプローチの1つを説明します。必ずしも最良または最も賢いわけではありませんが、非常に簡単です。
これが私が理解しているセットアップです-A)分岐ツリーが構築されますB)ペイオフがリーフからルートにプッシュバックされ、任意のステップでの最良の選択が示されます。 しかしこれは高価であるため、代わりに、ツリーの一部のみが非決定性の方法で葉まで探索されます。さらに、ツリーをさらに探索するたびに、以前の探索で学んだことによって決定されます。
そこで、「段階的な」ツリーを記述するコードを作成します。次に、部分的な報酬の見積もりとともに部分的に探索されたツリーを定義する別のデータ構造があります。次に、ランダムシードと部分的に探索されたツリーが与えられたrandseed -> ptree -> ptree
の関数があり、ツリーのさらなる探索に着手し、ptree構造を更新します。次に、空のシードptreeに対してこの関数を繰り返すだけで、ptree内でますます多くのサンプルスペースのリストを取得できます。次に、指定されたカットオフ条件が満たされるまで、このリストをたどることができます。
これで、すべてがブレンドされる1つのアルゴリズムから、3つの異なるステップに移行しました。1)状態ツリー全体をゆっくりと構築し、2)構造のサンプリングを使用して部分的な探索を更新し、3)いつ決定するかを決定します。十分なサンプルを収集しました。
STの使用が適切であるかどうかを判断するのは非常に難しい場合があります。 STありとSTなしで行うことをお勧めします(必ずしもこの順序である必要はありません)。非STバージョンはシンプルにしてください。 STの使用は最適化と見なされるべきであり、必要であることがわかるまでそれを実行したくありません。
Haskellのコードが読めないことを認めなければなりません。ただし、ツリーの変更にSTを使用する場合は、次の理由により、多くを失うことなく、これを不変のツリーに置き換えることができます。
新しいリーフの上のすべてのノードを変更する必要があります。不変ツリーは、変更されたノードの上のすべてのノードを置き換える必要があります。したがって、どちらの場合も、タッチされたノードは同じであるため、複雑さは何も得られません。
例: Javaオブジェクトの作成はミューテーションよりもコストがかかるので、ミューテーションを使用することで、ここHaskellで少し利益を得ることができるかもしれません。しかし、これは確かではありません。次のポイントのために。
新しい葉の評価は、おそらくツリーを更新するよりもはるかに費用がかかります。少なくともこれは、コンピューターGoのUCTの場合です。
STモナドの使用は、通常(常にではありませんが)最適化として行われます。最適化には、同じ手順を適用します。
私が知っている他のユースケースは、州のモナドの代替としてです。主な違いは、状態モナドでは保存されるすべてのデータのタイプがトップダウンで指定されるのに対し、STモナドではボトムアップで指定されることです。これが役立つ場合があります。