シャッフルを実装するには、randomを使用し、 MonadRandomを使用することもできます。ここにいくつかの良い答えがあります
しかし、それは実際に運用されています。これが舞台裏で起こっていることです。
私。
ランダム性は、Haskell で最初に遭遇し、不純物を処理しなければならない場所の 1 つです。シャッフルとサンプルは非常に単純に見え、物理的な画面への出力や印刷にまとめられるべきではないように思われるため、不快に思えます。核兵器を発射しますが、多くの場合purity == referentially transparent
、参照的に透明なランダム性は役に立たないでしょう.
random = 9 -- a referentially transparent random number
したがって、ランダム性を純粋にするためには、ランダム性について別の考え方が必要です。
Ⅱ.
再現性を高めるために使用される科学的コードの典型的な「チート」 (非常に重要) は、実験のランダムシードを修正して、コードを実行するたびにまったく同じ結果が得られることを他の人が検証できるようにすることです。これこそまさに参照透過性です!試してみよう。
type Seed = Int
random :: Seed -> (Int, Seed)
random s = (mersenneTwisterPerturb s, splitSeed s)
ここmersenneTwisterPerturb
で、 はSeed
s から への疑似乱数マッピングでInt
あり、は s からs へのsplitSeed
疑似乱数マッピングです。これらの関数は両方とも完全に決定論的 (かつ参照透過的) であることに注意してください。Seed
Seed
random
randomStream :: Seed -> [Int]
randomStram s = mersenneTwisterPerturb s : randomStream (splitSeed s)
繰り返しますが、このストリームは値に基づいて決定論的ですSeed
が、シードではなくストリームのみを見るオブザーバーは、その将来の値を予測できないはずです。
III.
整数のランダムなストリームを使用してリストをシャッフルできますか? 確かに、剰余算術を使用することで可能です。
shuffle' :: [Int] -> [a] -> [a]
shuffle' (i:is) xs = let (firsts, rest) = splitAt (i `mod` length xs) xs
in (head rest) : shuffle' is (firsts ++ tail rest)
または、より自己完結型にするために、ストリーム生成関数を事前に構成して、
shuffle :: Seed -> [a] -> [a]
shuffle s xs = shuffle' (randomStream s) xs
別の「シードを消費する」参照透過的な「ランダム」関数。
IV.
したがって、これは繰り返される傾向のようです。実際、モジュールをブラウズすると、System.Random
上で書いたような多くの関数が表示されます (いくつかの型クラスを特殊化しました)。
random :: (Random a) => StdGen -> (a, StdGen)
randoms :: (Random a) => StdGen -> [a]
どこでRandom
は、ランダムに生成できるものの型クラスでありStdGen
、 の一種ですSeed
。これは、必要なシャッフル関数を記述するのに十分な実際の作業コードです。
shuffle :: StdGen -> [a] -> [a]
shuffle g xs = shuffle' (randoms g) xs
ランダムシードを作成できるIO
関数があります。newStdGen :: IO StdGen
main = do gen <- newStdGen
return (shuffle gen [1,2,3,4,5])
しかし、厄介なことに気付くでしょう:異なるランダム順列を作成したい場合は、gen を変化させ続ける必要があります。
main = do gen1 <- newStdGen
shuffle gen1 [1,2,3,4,5]
gen2 <- newStdGen
shuffle gen2 [1,2,3,4,5]
-- using `split :: StdGen -> (StdGen, StdGen)`
gen3 <- newStdGen
let (_, gen4) = split gen3
shuffle gen3 [1,2,3,4,5]
let (_, gen5) = split gen4
shuffle gen4 [1,2,3,4,5]
StdGen
これは、異なる乱数が必要な場合は、多くの簿記を行うか、IO にとどまる必要があることを意味します。これは、参照の透過性のために再び「理にかなっています」---一連の乱数は互いにランダムでなければならないため、各ランダムイベントから次のイベントに情報を渡す必要があります。
本当に面倒くさいですけどね。もっとうまくやれるでしょうか?
V.
一般的に必要なのは、関数がランダムなシードを取り込んで、「ランダム化された」結果と次のシードを出力する方法です。
withSeed :: (Seed -> a) -> Seed -> (a, Seed)
withSeed f s = (f s, splitSeed s)
結果タイプwithSeed f :: Seed -> (a, Seed)
はかなり一般的な結果です。名前を付けましょう
newtype Random a = Random (Seed -> (a, Seed))
Seed
で意味のあるs を作成できることがわかっているので、型を次IO
のように変換する明らかな関数があります。Random
IO
runRandom :: Random a -> IO a
runRandom (Random f) = do seed <- newSeed
let (result, _) = f seed
return result
そして今、何か役に立つものを手に入れたような気がします--- type のランダムな値の概念は、a
後の値がすべて同一にならないように次を返す s のRandom a
単なる関数です。ランダムな値を構成し、これを自動的に渡す機械を作成することもできますSeed
Seed
Random
Seed
sequenceRandom :: Random a -> Random b -> Random b
sequenceRandom (Random fa) (Random fb) =
Random $ \seed -> let (_aValue, newSeed) = fa seed in fb newSeed
でも、ただ捨てるだけなので、それは少しばかげています_aValue
。2 番目の乱数が実際には最初の乱数値に実質的に依存するように、それらを構成しましょう。
bindRandom :: Random a -> (a -> Random b) -> Random b
bindRandom (Random fa) getRb =
Random $ \seed -> let (aValue, newSeed) = fa seed
(Random fb) = getRb aValue
in fb newSeed
また、値に対して「純粋な」ことができることにも注意する必要がありRandom
ます。たとえば、乱数に 2 を掛けることです。
randomTimesTwo :: Random Int -> Random Int
randomTimesTwo (Random f) = Random $ \seed -> let (value, newSeed) = f seed
in (value*2, newSeed)
これを Functor インスタンスとして抽象化できます
instance Functor Random where
fmap f (Random step) = Random $ \seed -> let (value, newSeed) = step seed
in (f value, newSeed)
ブラウン運動のようなクールなランダム効果を作成できるようになりました
brownianMotion :: Random [Int]
brownianMotion =
bindRandom random $ \x ->
fmap (\rest -> x : map (+x) rest) brownianMotion
Ⅵ.
そして、これは私が書いてきた問題全体の核心になります。ランダム性はIO
モナドに完全に存在できますが、より単純なRandom
モナドとして単独で存在することもできます。インスタンスをすぐに書き込むことができます。
instance Monad Random where
return x = Random (\seed -> (x, seed))
rx >>= f = bindRandom rx f
これはモナドなので自由なdo
記法が得られます
brownianMotion' = do x <- random
rest <- brownianMotion'
return $ x : map (+x) rest
runRandom
また、モナド準同型を空想的に呼び出すこともできますが、それはまったく別のトピックです。
ということで、まとめますと
- 参照透過性言語のランダム性には
Seed
sが必要
- カート
Seed
は迷惑です
Seed
s を自然にルーティングするランダム値の「リフティング」および「バインド」には、共通のパターンがあります。
- そのパターンはモナドを形成します
そして、本当に短い答えは、あなたはおそらく乱数を使いたいと思うでしょう。それらは、一般的に「サンプリング」に役立ちます。
乾杯!