4

stdGenIO を使用せずに、呼び出しごとに異なる値を返す関数が必要です。unsafePerformIO次のコードとして、を使用しようとしました。

import System.IO.Unsafe
import System.Random

myStdGen :: StdGen
myStdGen = unsafePerformIO getStdGen

しかしmyStdGen、ghci を呼び出そうとすると、常に同じ値が返されます。私は虐待しましたunsafePerformIOか?または、私の目標を達成するための他の方法はありますか?

編集 申し訳ありませんが、質問をもっと正確に説明する必要があると思います。

実際には、特別な「マージ」操作が必要な、treap データ構造のバリエーションを実装しています。償却されたO(log n)の予想時間の複雑さを保証するために、ある程度のランダム性に依存しています。

(Tree, StdGen)トラップごとに乱数発生器を保持するようなペアを使用しようとしました。Treap に新しいデータを挿入するときはrandom、新しいノードにランダムな値を与えてから、ジェネレーターを更新します。しかし、問題が発生しました。empty空の treap を返す関数myStdGenを呼び出しました。上記の関数を使用して、この treap のランダム ジェネレーターを取得しました。ただし、空の treap が 2 つある場合、それらStdGenは同じになります。したがって、両方の trap にデータを挿入した後、それらをマージしたい場合、それらのランダム値も同じになります。そのため、頼りにしていたランダム性を失いました。

そのため、呼び出しごとに異なる結果を生成する、何らかの形で「グローバルな」乱数発生器がStdGen必要なため、空の Treap ごとに異なるStdGen.

4

3 に答える 3

8

私は虐待していますかunsafePerformIO

はいはい!「純関数の際立った特徴」は、

  • 副作用なし
  • 参照透過性、つまり、結果の後続の各 eval は同じものを生成する必要があります。

実際、「目標」を達成する方法はありますが、その考えは間違っています。

myStdGen :: () -> StdGen
myStdGen () = unsafePerformIO getStdGen

これはCAFIOではなく (役に立たない) 関数呼び出しであるため、各呼び出しで個別にアクションを評価します。

それでも、コンパイラはそれを最適化するのにかなり自由だと思うので、絶対に依存しないでください。

編集しようとすると、特定のプロセス内で使用されると常に同じジェネレーターが提供されることに気付きました。そのgetStdGenため、上記がより合理的なタイプを使用しても機能しません。


アルゴリズムなどで疑似ランダム性を正しく使用するために、あらゆる場所で IO を必要としないことに注意してください。この場合、プログラム全体では常に同じ結果が得られますが、内部的には、有効に機能するために必要なすべての異なる乱数が含まれています。StdGensplit

別の方法として、IO からジェネレーターを取得することもできますが、それでもアルゴリズムを ではなく純粋なランダム モナドで記述しますIO


完全に純粋なアルゴリズムで「ランダム性」を取得する別の方法があります。入力をHashable!にする必要があります。次に、任意の関数引数をランダム シードとして効果的に使用できます。これは少し奇妙な解決策ですが、treap アプリケーションではうまくいくかもしれません (ただし、一部の人々はこれをもはや Treap として分類せず、特殊な種類のハッシュマップとして分類すると思います)。

于 2014-11-20T14:38:15.880 に答える
5

はい、虐待しunsafePerformIOました。を使用する正当な理由はほとんどありませんunsafePerformIO。たとえば、C ライブラリとのインターフェイスの場合などです。少数のコア ライブラリの実装にも使用されています (そのうちの 1 つだと思いSTます)。要するに、自分unsafePerformIOが何をしているのか本当に確信がない限り、使用しないでください。乱数を生成するためのものではありません。

Haskell の関数は純粋でなければならないことを思い出してください。つまり、入力のみに依存するということです。これは、「乱数」を生成する純粋な関数を持つことができることを意味しますが、その数はそれに渡す乱数ジェネレーターに依存します。次のようなことができます

myStdGen :: StdGen
myStdGen = mkStdGen 42

それからあなたはすることができます

randomInt :: StdGen -> (Int, StdGen)
randomInt g = random

StdGenただし、この関数から返された new を先に使用する必要があります。そうしないと、 から常に同じ出力が得られrandomIntます。


では、に頼らずに乱数をきれいに生成するにはどうすればよいのでしょうIOか。答えはStateモナドです。に似ている

newtype State s a = State { runState :: s -> (a, s) }

そしてそのモナドインスタンスは次のようになります

instance Monad (State s) where
    return a = State $ \s -> (a, s)
    (State m) >>= f = State $ \s -> let (a, newState) = m s
                                        (State g)     = f a
                                    in g newState

初めて見ると少し混乱しますが、基本的に状態モナドが行うことはすべて凝った関数合成です。詳細な説明については、LYAHを参照してください。ここで注意すべき重要なことは、 のタイプはsステップ間で変更されず、aパラメーターのみが変更される可能性があるということです。

これは、 と を使用して関数 によくs -> (a, s)似ていることに気付くでしょう。つまり、私たちがやった場合StdGen -> (Int, StdGen)s ~ StdGena ~ Int

randomIntS :: State StdGen Int
randomIntS = State randomInt

その後、私たちはすることができます

twoRandInts :: State StdGen (Int, Int)
twoRandInts = do
    a <- randomIntS
    b <- randomIntS
    return (a, b)

次に、初期状態を指定して実行できます。

main = do
    g <- getStdGen
    print $ runState twoRandInts g

StdGenまだ から出てきますIOが、すべてのロジック自体は純粋に状態モナド内で発生します。

于 2014-11-20T14:37:49.270 に答える