web-dev-qa-db-ja.com

STモナド==コードの臭い?

私はHaskellで [〜#〜] uct [〜#〜] アルゴリズムの実装に取り​​組んでいますが、これにはかなりの量のデータジャグリングが必要です。詳細に立ち入ることなく、これはシミュレーションアルゴリズムであり、各「ステップ」で、検索ツリーのリーフノードがいくつかの統計プロパティに基づいて選択され、そのリーフに新しい子ノードが構築され、それに対応する統計情報が表示されます。新しいリーフとそのすべての祖先が更新されます。

そのすべてのジャグリングを考えると、私は検索ツリー全体を素敵な不変のデータ構造にする方法を理解するのに十分なほど鋭敏ではありません 岡崎 。代わりに、私はSTモナドを少しいじって、可変のSTRefsで構成される構造を作成してきました。不自然な例(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ベースの構造から、外部の同形であるが不変の構造に変換する必要があるため、うんざりしています。

47
mergeconflict

私はSTをあまり使用しませんが、それが最善の解決策である場合もあります。これは多くのシナリオで発生する可能性があります。

  • 問題を解決するためのよく知られた効率的な方法はすでにあります。クイックソートはこれの完璧な例です。速度とインプレース動作で知られており、純粋なコードではうまく模倣できません。
  • 厳格な時間と空間の境界が必要です。特に遅延評価の場合(そしてHaskellは遅延評価があるかどうかさえ指定せず、厳密ではないというだけです)、プログラムの動作は非常に予測できない可能性があります。メモリリークがあるかどうかは、特定の最適化が有効になっているかどうかによって異なります。これは、変数の固定セット(通常)と定義された評価順序を持つ命令型コードとは大きく異なります。
  • 締め切りがあります。ほとんどの場合、純粋なスタイルの方がより適切でクリーンなコードですが、命令型の記述に慣れていてすぐにコードが必要な場合は、命令型から始めて後で機能に移行するのが完全に合理的な選択です。

ST(および他のモナド)を使用するときは、次の一般的なガイドラインに従おうとします。

  • Applicative スタイルを頻繁に使用します。これにより、コードが読みやすくなり、不変バージョンに切り替えた場合でも、変換がはるかに簡単になります。それだけでなく、Applicativeスタイルははるかにコンパクトです。
  • STだけを使用しないでください。 STのみでプログラムする場合、結果は巨大なCプログラムよりも良くなることはなく、明示的な読み取りと書き込みのためにおそらく悪くなります。代わりに、適用される場所に純粋なHaskellコードを散在させます。 STRef s (Map k [v])のようなものをよく使用します。マップ自体は変更されていますが、手間のかかる作業の多くは純粋に行われます。
  • 必要がない場合は、ライブラリを作り直さないでください。 IO用に記述された多くのコードは、クリーンかつかなり機械的にSTに変換できます。Data.HashTableですべてのIORefsをSTRefsに置き換え、IOsをSTsに置き換える方がはるかに簡単でした。コード化されたハッシュテーブルの実装は、おそらくより高速でした。

最後にもう1つ注意してください-明示的な読み取りと書き込みで問題が発生した場合は、 その周りに があります。

40
gereeter

突然変異を利用するアルゴリズムとそうでないアルゴリズムは異なるアルゴリズムです。前者から後者への翻訳を維持するという単純な限界がある場合もあれば、難しいものもあれば、複雑さの限界を維持しないものだけがある場合もあります。

論文をざっと見ると、突然変異を本質的に利用しているとは思わないことがわかります。したがって、潜在的に本当に気の利いた怠惰な機能的アルゴリズムを開発できると思います。しかし、それは説明されているものとは異なりますが、関連するアルゴリズムになります。

以下に、そのようなアプローチの1つを説明します。必ずしも最良または最も賢いわけではありませんが、非常に簡単です。

これが私が理解しているセットアップです-A)分岐ツリーが構築されますB)ペイオフがリーフからルートにプッシュバックされ、任意のステップでの最良の選択が示されます。 しかしこれは高価であるため、代わりに、ツリーの一部のみが非決定性の方法で葉まで探索されます。さらに、ツリーをさらに探索するたびに、以前の探索で学んだことによって決定されます。

そこで、「段階的な」ツリーを記述するコードを作成します。次に、部分的な報酬の見積もりとともに部分的に探索されたツリーを定義する別のデータ構造があります。次に、ランダムシードと部分的に探索されたツリーが与えられたrandseed -> ptree -> ptreeの関数があり、ツリーのさらなる探索に着手し、ptree構造を更新します。次に、空のシードptreeに対してこの関数を繰り返すだけで、ptree内でますます多くのサンプルスペースのリストを取得できます。次に、指定されたカットオフ条件が満たされるまで、このリストをたどることができます。

これで、すべてがブレンドされる1つのアルゴリズムから、3つの異なるステップに移行しました。1)状態ツリー全体をゆっくりと構築し、2)構造のサンプリングを使用して部分的な探索を更新し、3)いつ決定するかを決定します。十分なサンプルを収集しました。

11
sclv

STの使用が適切であるかどうかを判断するのは非常に難しい場合があります。 STありとSTなしで行うことをお勧めします(必ずしもこの順序である必要はありません)。非STバージョンはシンプルにしてください。 STの使用は最適化と見なされるべきであり、必要であることがわかるまでそれを実行したくありません。

9
augustss

Haskellのコードが読めないことを認めなければなりません。ただし、ツリーの変更にSTを使用する場合は、次の理由により、多くを失うことなく、これを不変のツリーに置き換えることができます。

可変ツリーと不変ツリーで同じ複雑さ

新しいリーフの上のすべてのノードを変更する必要があります。不変ツリーは、変更されたノードの上のすべてのノードを置き換える必要があります。したがって、どちらの場合も、タッチされたノードは同じであるため、複雑さは何も得られません。

例: Javaオブジェクトの作成はミューテーションよりもコストがかかるので、ミューテーションを使用することで、ここHaskellで少し利益を得ることができるかもしれません。しかし、これは確かではありません。次のポイントのために。

ツリーの更新はおそらくボトルネックではありません

新しい葉の評価は、おそらくツリーを更新するよりもはるかに費用がかかります。少なくともこれは、コンピューターGoのUCTの場合です。

2
ziggystar

STモナドの使用は、通常(常にではありませんが)最適化として行われます。最適化には、同じ手順を適用します。

  1. それなしでコードを書いてください、
  2. ボトルネックのプロファイルと特定、
  3. ボトルネックを段階的に書き直し、改善/リグレッションをテストします。

私が知っている他のユースケースは、州のモナドの代替としてです。主な違いは、状態モナドでは保存されるすべてのデータのタイプがトップダウンで指定されるのに対し、STモナドではボトムアップで指定されることです。これが役立つ場合があります。

2
dan_waterworth