タイトルの通りです。
いろいろな使用例を載せたので、頑張って使いこなせるようになりましょう。
私自身ちゃんと理解しているわけではなく、自分の勉強をかねて書いているので間違いなどあったら指摘してくださると助かります。
また、mwc-random以外のパッケージの関数も使用しますが、これらについてはあまり説明せず使用します(Data.Vector.Unboxed, Control.Monad.ST など)。
STモナドを使う例をあまり載せていませんが、IOのときと同じような感じで書けるはずです(たぶん...)。
記事の流れ
1. とりあえず簡単に乱数生成してみる
(createSystemRandom, uniform, uniformR)
2. 少し頑張って乱数列を生成してみる
(uniformVector)
3. 乱数を生成する関数を引数に取る関数を使う
(withSystemRandom, asGenIO)
4. 毎回同じ値を生成する
(toSeed, restore, initialize)
5. 乱数の種(seed)とベクトル
(toSeed, fromSeed)
6. 純粋な関数の中で使うためのインターフェースを作ってみる
(restore, save)
7. haskell stackでmwc-randomパッケージをインポートする方法
1. とりあえず簡単に乱数生成してみる
(createSystemRandom, uniform, uniformR使用方法)
IOモナド内で乱数を生成して、printしてみます。
(System.Random.MWCをimportするには、パッケージをダウンロードする必要があります。haskell stackを使っている場合は非常に簡単なので、方法を記事の最後に書きました。stackを使っていない場合は、頑張ってください......)
createSystemRandomは擬似乱数生成器(PRNG)を生成します。
またIOモナド内だけで使える関数です。
uniformは乱数をひとつ生成します(乱数範囲指定不可、型は指定する必要あり)。
サンプル1-1でのuniformの具体型は
生成する乱数の型はサンプル1-1のように指定する必要があります(:: IO Int)。
でないと、 m a の具体型が決まらないのでエラーになります(The type variable 'a' is ambiguous などと言われます)。
もちろん、IntではなくDoubleやBoolなどを指定しても構いません。
uniformRは範囲を指定して乱数を生成したいときに使います。
サンプル1-1の7行目を以下のように書き換えてみてください。
uniformRの型は、
上の例での具体型は、
話が戻りますが、createSystemRandomで生成したPRNGは、最初に一度だけ生成すれば使い回すことができます。
こんな感じです。
PRNGの生成は、乱数生成に比べて "expensive" (参考[2]より)なので、使いまわしたほうがいいようです。
2. 少し頑張って乱数列を生成してみる
(uniformVector)
mwc-randomでは、生成した乱数列はリストではなくVectorとして返されます。
(HackageのData.Vectorドキュメント(同じタブで開きます))
例えば以下のように使います。
リストのように見えますが、uniformVectorの型は以下のようになっています。
だから上のコード(サンプル2-1)のrandValueの型はVU.Vector Int となるわけです。
もしリストに変換したいなら、toListという関数を使って変換することができます。
3. 乱数を生成する関数を引数に取る関数を使う
(withSystemRandom, asGenIO)
ここまでは、PRNG(擬似乱数生成器)を生成するのに、毎回createSystemRandomを使ってきました。
これをwithSystemRandomという関数で書き換えてみます。
この関数の型は、
つまり引数に関数をとって、IOモナドに包まれた乱数を返します。
引数の関数は、PRNGを引数に取って乱数を返す関数です。
例えば、最初に紹介したuniformはここに入るぴったりの関数です。
こんな風に使います。
これを使わないと、GenIOなのかGenSTなのか決まらないためエラーになり、ambiguous と言われます。
しかし、以下のようにラムダ式を使えば、asGenIOを使わなくても書けます。
さらに、asGenIOとラムダ式を使えば、このような書き方もできます。
ではwithSystemRandomを使って、乱数列も生成してみます。
uniformは、引数にPRNGのみを取る関数だからぴったりでしたが、uniformVectorは引数をふたつ取る(PRNGとInt整数)ので、少し工夫が必要です。
こんな感じです(これはHackageのドキュメントにあるサンプルとほとんど一緒です)。
4. 毎回同じ値を生成する
(initialize, toSeed, restore)
決まった値をもとに乱数の種(seed)を初期化すれば、毎回同じ値を生成することができます。
initializeの型は、
つまり、与えられたベクトルの値をもとにPRNGを生成するので、毎回同じPRNGが生成されます。
したがって、生成される乱数も毎回同じです。
initializeの引数にベクトルを渡すため、fromListを使って、リストからVectorへ変換しています。
そのあとは、これまでと同じです。
サンプル3-1では、seedを初期化するのがわかりにくいので、以下のように書き換えてみます。
つまりinitializeは、restore . toSeedと同じことをする関数です。
一つずつ見てきます。
toSeedの型は、
restoreの型は、
つまり、initialize、あるいはrestore . toSeedは、ベクトルからPRNGを生成します。
言葉より図のほうがわかりやすいので、図を作ってみました。
Vector型、Seed型、Gen型の変換の模式図です。
Gen型は常にモナドに包まれていますが、省略しています。
図でみるとinitialize = restore . toSeedも一目瞭然ですね。
まだ説明していない関数も書いてありますが、後ほど説明します。
Hackageのドキュメント(参考[2])のinitializeの説明のところにこんな例があります。
つまり、genを一旦ベクトルまで戻してからまたgen'を生成(gen --> seed --> vector --> seed --> gen')しても、同じだということです。
実際に以下のコードで試してみました。
7行目で、先ほどの例のように、gen'を生成します。
10行目で、randとrand'が等しいか出力します。
確かに、Trueが出力されました。
ちなみにどうでもいいですがモナドを使う練習として、8 ~ 10行目は以下のように書き換えることができます。
そしてそれをprintへバインドしています。
まだ全然モナドを使いこなせませんが、覚えておこうと思います。
5. 乱数の種(seed)とベクトル
(toSeed, fromSeed)
上の図を見るとわかりますが、toSeedには逆関数fromSeedがあります。
型を比べるとよくわかります。
つまり、toSeedがベクトルからseedを生成するのに対し、fromSeedはseedをもとのベクトルへ戻します。
乱数を生成するためにわざわざベクトルからseedへ変換したのに、なぜまた元に戻す関数が存在するのか、私にはよくわかりません。
6. 純粋な関数の中で使うためのインターフェースを作ってみる
(restore, save)
だんだん面倒になってきましたが、とりあえず書きます。
restoreとsaveも逆関数の関係です。
型を見比べてください。
saveは、PRNGの状態を保存したい時に使います。
例として、参考[1]にあった例をパクらせていただきます、すみません(少し単純にしていますが)。
純粋な値(Seed)を受け取って、純粋な値((t, Seed))を返しています。
つまり、内部ではSTモナドを使用していますが、この関数は純粋な関数の中で呼び出すことができます。
同様にして、範囲指定する場合の関数も作ってみます。
この関数を使えば、純粋な関数内で範囲指定のある乱数生成ができます。
さらに、これらを使って、乱数列を生成する関数も作ってみます。
randomRsMWCは、範囲指定ありの乱数の無限リストを返します。
使い方は、こんな感じです。
ただし、おそらくrestoreとsaveは乱数生成に比べてコストが大きいので、乱数を一つ生成するたびにrestoreとsaveを繰り返すこれらの関数は、実用的ではないかもしれません(System.Randomの関数とどっちが速いか気になります)。
7. haskell stackでmwc-randomパッケージをインポートする方法
Haskell stackを使っている場合の方法を書きます。
まず、
$ stack new MWC-random new-templates
などとして、プロジェクトを作成してください(MWC-randomの部分はご自分の好きな名前でどうぞ)。
$ cd MWC-random
で、MWC-random/へ移動すると、MWC-random.cabalというファイルがあります。
このファイルを開き、build-dependsに"mwc-random"を追記します。
app/Main.hs内でインポートする場合は、executableのbuild-dependsへ追記してください。
src/Lib.hs内でインポートする場合は、libraryのbuild-dependsへ追記してください。
executableに追加する場合はこんな感じ
たったこれだけです。
これであとは、
$ stack build
するときに、勝手にダウンロードしてくれます。
参考
[1]Haskellの乱数事情(Qiita)(同じタブで開きます)
[2]https://hackage.haskell.org/package/mwc-random-0.13.4.0/docs/System-Random-MWC.html(これがドキュメント、同じタブで開きます)
[3]https://hackage.haskell.org/package/mwc-random(同じタブで開きます)
コメントを書いていただけると有難いです。
いろいろな使用例を載せたので、頑張って使いこなせるようになりましょう。
私自身ちゃんと理解しているわけではなく、自分の勉強をかねて書いているので間違いなどあったら指摘してくださると助かります。
また、mwc-random以外のパッケージの関数も使用しますが、これらについてはあまり説明せず使用します(Data.Vector.Unboxed, Control.Monad.ST など)。
STモナドを使う例をあまり載せていませんが、IOのときと同じような感じで書けるはずです(たぶん...)。
記事の流れ
1. とりあえず簡単に乱数生成してみる
(createSystemRandom, uniform, uniformR)
2. 少し頑張って乱数列を生成してみる
(uniformVector)
3. 乱数を生成する関数を引数に取る関数を使う
(withSystemRandom, asGenIO)
4. 毎回同じ値を生成する
(toSeed, restore, initialize)
5. 乱数の種(seed)とベクトル
(toSeed, fromSeed)
6. 純粋な関数の中で使うためのインターフェースを作ってみる
(restore, save)
7. haskell stackでmwc-randomパッケージをインポートする方法
1. とりあえず簡単に乱数生成してみる
(createSystemRandom, uniform, uniformR使用方法)
IOモナド内で乱数を生成して、printしてみます。
-- サンプル1-1 import System.Random.MWC main :: IO () main = do gen <- createSystemRandom randValue <- uniform gen :: IO Int print randValueこれだけです。
(System.Random.MWCをimportするには、パッケージをダウンロードする必要があります。haskell stackを使っている場合は非常に簡単なので、方法を記事の最後に書きました。stackを使っていない場合は、頑張ってください......)
createSystemRandomは擬似乱数生成器(PRNG)を生成します。
createSystemRandom :: IO GenIOより、これは引数を取りません。
またIOモナド内だけで使える関数です。
uniformは乱数をひとつ生成します(乱数範囲指定不可、型は指定する必要あり)。
uniform :: PrimeMonad m => Gen (PrimeState m) -> m aより、これはPRNGを引数に取って乱数を返します。
サンプル1-1でのuniformの具体型は
uniform :: Gen (PrimeState IO) -> IO Intとなっています(GenIOは、Gen (PrimeState IO)の別名です)。
生成する乱数の型はサンプル1-1のように指定する必要があります(:: IO Int)。
でないと、 m a の具体型が決まらないのでエラーになります(The type variable 'a' is ambiguous などと言われます)。
もちろん、IntではなくDoubleやBoolなどを指定しても構いません。
uniformRは範囲を指定して乱数を生成したいときに使います。
サンプル1-1の7行目を以下のように書き換えてみてください。
randValue <- uniformR (1.0, 10.0) gen :: IO Doubleこうすると、1.0 〜 10.0までの範囲のDouble型の乱数が生成されるはずです。
uniformRの型は、
uniformR :: PrimeMonad m => (a, a) -> Gen (PrimeState m) -> m aより、これは第1引数で乱数の最小値と最大値を指定し、第2引数にPRNGをとって乱数を返します。
上の例での具体型は、
uniformR :: (Double, Double) -> Gen (PrimeState IO) -> IO Doubleとなっています。
話が戻りますが、createSystemRandomで生成したPRNGは、最初に一度だけ生成すれば使い回すことができます。
こんな感じです。
-- サンプル1-2 import System.Random.MWC main :: IO () main = do gen <- createSystemRandom print =<< (uniformR (0, 99) gen :: IO Int) print =<< (uniformR (0, 99) gen :: IO Int) print =<< (uniformR (0, 99) gen :: IO Int) print =<< (uniformR (0, 99) gen :: IO Int) print =<< (uniformR (0, 99) gen :: IO Int)同じgenを何度も使用していますが、毎回異なる乱数が生成されるはずです。
PRNGの生成は、乱数生成に比べて "expensive" (参考[2]より)なので、使いまわしたほうがいいようです。
2. 少し頑張って乱数列を生成してみる
(uniformVector)
mwc-randomでは、生成した乱数列はリストではなくVectorとして返されます。
(HackageのData.Vectorドキュメント(同じタブで開きます))
例えば以下のように使います。
--サンプル2-1 import System.Random.MWC import qualified Data.Vector.Unboxed as VU main :: IO () main = do gen <- createSystemRandom randValues <- uniformVector gen 10 :: IO (VU.Vector Int) print randValuesこれをghci上で実行すると以下のように、10個の乱数のベクトルが返されます。
リストのように見えますが、uniformVectorの型は以下のようになっています。
uniformVector :: (PrimMonad m, Variate a, Vector v a) => (Gen PrimState m) -> Int -> m (v a)戻り値の型はモナドに包まれたベクトルです。
だから上のコード(サンプル2-1)のrandValueの型はVU.Vector Int となるわけです。
もしリストに変換したいなら、toListという関数を使って変換することができます。
let randValues' = VU.toList randValuesこれで、randValues'の型は[Int]となります。
3. 乱数を生成する関数を引数に取る関数を使う
(withSystemRandom, asGenIO)
ここまでは、PRNG(擬似乱数生成器)を生成するのに、毎回createSystemRandomを使ってきました。
これをwithSystemRandomという関数で書き換えてみます。
この関数の型は、
withSystemRandom :: PrimBase m => (Gen (PrimState m) -> m a) -> IO aとなっています。
つまり引数に関数をとって、IOモナドに包まれた乱数を返します。
引数の関数は、PRNGを引数に取って乱数を返す関数です。
例えば、最初に紹介したuniformはここに入るぴったりの関数です。
こんな風に使います。
main :: IO () main = do randValue <- withSystemRandom (asGenIO uniform) :: IO Int print randValueHaskellらしく、()を使わずに書くとこんな感じになります。
main :: IO () main = do randValue <- withSystemRandom . asGenIO $ uniform :: IO Int print randValueasGenIOは、Gen (PrimState m)をGenIOと指定するための関数です。
これを使わないと、GenIOなのかGenSTなのか決まらないためエラーになり、ambiguous と言われます。
しかし、以下のようにラムダ式を使えば、asGenIOを使わなくても書けます。
main :: IO () main = do randValue <- withSystemRandom $ \\gen -> uniform gen :: IO Int print randValue
さらに、asGenIOとラムダ式を使えば、このような書き方もできます。
main :: IO () main = do randValue <- withSystemRandom . asGenIO $ \\gen -> uniform gen print (randValue :: Int)どういう場合に型推論ができて、どういう場合に型推論ができないのかどうもよくわかりません。
ではwithSystemRandomを使って、乱数列も生成してみます。
uniformは、引数にPRNGのみを取る関数だからぴったりでしたが、uniformVectorは引数をふたつ取る(PRNGとInt整数)ので、少し工夫が必要です。
こんな感じです(これはHackageのドキュメントにあるサンプルとほとんど一緒です)。
main :: IO () main = do randValues <- withSystemRandom . asGenIO $ \\gen -> uniformVector gen 10 :: IO (VU.Vector Int) print randValues
4. 毎回同じ値を生成する
(initialize, toSeed, restore)
決まった値をもとに乱数の種(seed)を初期化すれば、毎回同じ値を生成することができます。
--サンプル3-1 import System.Random.MWC import Control.Monad.ST (runST) import qualified Data.Vector.Unboxed as VU main :: IO () main = do print pureRandValue pureRandValue :: Int pureRandValue = runST $ do gen <- initialize $ VU.fromList [1,3,8,20] randValue <- uniform gen return (randValue :: Int)今までは、createSystemRandomでPRNGを生成していましたが、ここではそれをinitializeで生成しています。
initializeの型は、
initialize :: (PrimMonad m, Vector v Word32) => v Word32 -> m (Gen (PrimState m))となっています。
つまり、与えられたベクトルの値をもとにPRNGを生成するので、毎回同じPRNGが生成されます。
したがって、生成される乱数も毎回同じです。
initializeの引数にベクトルを渡すため、fromListを使って、リストからVectorへ変換しています。
そのあとは、これまでと同じです。
サンプル3-1では、seedを初期化するのがわかりにくいので、以下のように書き換えてみます。
--サンプル3-2 import System.Random.MWC import Control.Monad.ST (runST) import qualified Data.Vector.Unboxed as VU main :: IO () main = do print pureRandValue' pureRandValue' :: Int pureRandValue' = runST $ do let seed = toSeed . VU.fromList $ [1,3,8,20] gen <- restore seed randValue <- uniform gen return (randValue :: Int)結果は同じです。
つまりinitializeは、restore . toSeedと同じことをする関数です。
一つずつ見てきます。
toSeedの型は、
toSeed :: Vector v Word32 => v Word32 -> Seedこれは、ベクトルからSeedを生成します。
restoreの型は、
restore :: PrimMonad m => Seed -> m (Gen (PrimState m))これは、SeedからPRNGを生成します。
つまり、initialize、あるいはrestore . toSeedは、ベクトルからPRNGを生成します。
言葉より図のほうがわかりやすいので、図を作ってみました。
Vector型、Seed型、Gen型の変換の模式図です。
Gen型は常にモナドに包まれていますが、省略しています。
図でみるとinitialize = restore . toSeedも一目瞭然ですね。
まだ説明していない関数も書いてありますが、後ほど説明します。
Hackageのドキュメント(参考[2])のinitializeの説明のところにこんな例があります。
gen' <- initialize . fromSeed =<< saveそしてこの時、gen' == genだと書いてありました。
つまり、genを一旦ベクトルまで戻してからまたgen'を生成(gen --> seed --> vector --> seed --> gen')しても、同じだということです。
実際に以下のコードで試してみました。
import System.Random.MWC import qualified Data.Vector.Unboxed as VU main :: IO () main = do gen <- initialize $ VU.fromList [1] gen' <- initialize . fromSeed =<< save gen rand <- uniform gen :: IO Int rand' <- uniform gen' :: IO Int print $ rand == rand'6行目でまずgenを生成します。
7行目で、先ほどの例のように、gen'を生成します。
10行目で、randとrand'が等しいか出力します。
確かに、Trueが出力されました。
ちなみにどうでもいいですがモナドを使う練習として、8 ~ 10行目は以下のように書き換えることができます。
print =<< (==) <$> (uniform gen :: IO Int) <*> (uniform gen' :: IO Int)ここでは、バインド(=<<)のようなことを2引数関数(==)でやっています。
そしてそれをprintへバインドしています。
まだ全然モナドを使いこなせませんが、覚えておこうと思います。
5. 乱数の種(seed)とベクトル
(toSeed, fromSeed)
上の図を見るとわかりますが、toSeedには逆関数fromSeedがあります。
型を比べるとよくわかります。
toSeed :: Vector v Word32 => v Word32 -> Seed fromSeed :: Seed -> Vector Word32ちょうど引数と戻り値の型が逆になっているのがわかります。
つまり、toSeedがベクトルからseedを生成するのに対し、fromSeedはseedをもとのベクトルへ戻します。
乱数を生成するためにわざわざベクトルからseedへ変換したのに、なぜまた元に戻す関数が存在するのか、私にはよくわかりません。
6. 純粋な関数の中で使うためのインターフェースを作ってみる
(restore, save)
だんだん面倒になってきましたが、とりあえず書きます。
restoreとsaveも逆関数の関係です。
型を見比べてください。
restore :: PrimMonad m => Seed -> m (Gen (PrimState m)) save :: PrimMonad m => Gen (PrimState m) -> m SeedrestoreはseedからPRNGを生成する関数、saveはPRNGからもとのseedを取り出す関数です。
saveは、PRNGの状態を保存したい時に使います。
例として、参考[1]にあった例をパクらせていただきます、すみません(少し単純にしていますが)。
import Control.Monad.ST (runST) import System.Random.MWC import qualified Data.Vector.Unboxed as VU main :: IO () main = do let (rand, seed) = randomMWC $ toSeed . VU.fromList $ [1] :: (Int, Seed) let (rand', _) = randomMWC seed :: (Int, Seed) print rand print rand' randomMWC :: Variate t => Seed -> (t, Seed) randomMWC seed = runST $ do gen <- restore seed randValue <- uniform gen seed' <- save gen return (randValue, seed')randomMWCの型をみてください。
純粋な値(Seed)を受け取って、純粋な値((t, Seed))を返しています。
つまり、内部ではSTモナドを使用していますが、この関数は純粋な関数の中で呼び出すことができます。
同様にして、範囲指定する場合の関数も作ってみます。
randomRMWC :: Variate t => Seed -> (t, t) -> (t, Seed) randomRMWC seed range = runST $ do gen <- restore seed randValue <- uniformR range gen seed' <- save gen return (randValue, seed')uniformのところをuniformRに変えて、あと引数に範囲を指定するタプルを追加しただけです。
この関数を使えば、純粋な関数内で範囲指定のある乱数生成ができます。
さらに、これらを使って、乱数列を生成する関数も作ってみます。
randomsMWC :: Variate a => Seed -> [a] randomsMWC seed = rand : randomsMWC seed' where (rand, seed') = randomMWC seed randomRsMWC :: Variate a => (a, a) -> Seed -> [a] randomRsMWC range seed = rand : randomRsMWC range seed' where (rand, seed') = randomRMWC range seedrandomsMWCは、範囲指定なしの乱数の無限リストを返します。
randomRsMWCは、範囲指定ありの乱数の無限リストを返します。
使い方は、こんな感じです。
import Control.Monad.ST (runST) import System.Random.MWC import qualified Data.Vector.Unboxed as VU main :: IO () main = do let rands = take 5 $ randomsMWC $ toSeed $ VU.fromList [1] :: [Double] let randRs = take 5 $ randomRsMWC (0, 9) $ toSeed $ VU.fromList [1] :: [Int] print rands print randRsこれで、純粋な関数内で乱数を一つ生成することもできるし、乱数列を生成することもできます。
ただし、おそらくrestoreとsaveは乱数生成に比べてコストが大きいので、乱数を一つ生成するたびにrestoreとsaveを繰り返すこれらの関数は、実用的ではないかもしれません(System.Randomの関数とどっちが速いか気になります)。
7. haskell stackでmwc-randomパッケージをインポートする方法
Haskell stackを使っている場合の方法を書きます。
まず、
$ stack new MWC-random new-templates
などとして、プロジェクトを作成してください(MWC-randomの部分はご自分の好きな名前でどうぞ)。
$ cd MWC-random
で、MWC-random/へ移動すると、MWC-random.cabalというファイルがあります。
このファイルを開き、build-dependsに"mwc-random"を追記します。
app/Main.hs内でインポートする場合は、executableのbuild-dependsへ追記してください。
src/Lib.hs内でインポートする場合は、libraryのbuild-dependsへ追記してください。
executableに追加する場合はこんな感じ
たったこれだけです。
これであとは、
$ stack build
するときに、勝手にダウンロードしてくれます。
参考
[1]Haskellの乱数事情(Qiita)(同じタブで開きます)
[2]https://hackage.haskell.org/package/mwc-random-0.13.4.0/docs/System-Random-MWC.html(これがドキュメント、同じタブで開きます)
[3]https://hackage.haskell.org/package/mwc-random(同じタブで開きます)
コメントを書いていただけると有難いです。
特に、間違いなどあったらご指摘いただけると幸いです。
よろしくお願いします。
コメント