2008年11月22日土曜日

Haskell でランダムな値を生成する System.Random

1. System.Random のクラスと型

Haskell の標準ライブラリには、ランダムな値を生成するパッケージがある。

Haskell/Understanding monads – Wikibooks によると、

There is much research on constructing good pseudo-random number generators, but fortunately, the Haskell standard library module System.Random already implements a ready-to-use generator for us.

SnapCrab_NoName_2012-7-21_1-27-0_No-00System.Random には、二つのクラスと一つの型がある。

RandomGen は、ランダムを生成するジェネレータのためのインターフェイスを表す。Random は、特定の型から値を抽出する方法を定義している。

StdGen は、RandomGen クラスのインスタンス。

 

2. ランダムな数値を得る

a. 1 ~ 6 までのランダムな数値を得る例
System.RandomgetStdRandom 関数の説明に、「1 ~ 6 までのランダムな数字を得る」方法が書かれている。
import System.Random

rollDice :: IO Int
rollDice = getStdRandom (randomR (1,6))

main :: IO ()
main = rollDice >>= print 

Haskell は純粋関数型言語。関数を呼び出すと、必ず同じ値が返ってくる。しかし、不思議なことに main 関数を実行する度に、異なった値が返される。 (@_@)

 

b. getStdRandom と randomR 関数の型

使われている関数 randomR, getStdRandom の型は

randomR 関数の第1引数にタプルを与えると、

g –> (a, g)

である型の関数が返される。この型に対応した関数が getStdRandom の第1引数で示されている関数の型に対応している。

SnapCrab_NoName_2012-7-21_1-21-6_No-00

型について確認すると、

  1. 型 g –> (a,g) における g は、RandomGen クラスのインスタンスでなければならない。
  2. これに対して、StdGen 型は RandomGen クラスのインスタンスとして宣言されている。
  3. randomR 関数の第1引数に任意のタプルを与えると、g –> (a, g) である型の関数が返される。
  4. 型 g –> (a, g) は、getStdRandom 関数の第1引数に対応している。

… ということだろうか?

 

c. RandomGen クラス

実際に、randomR にランダムな範囲を表わすタプルを与えてみると、

Prelude System.Random> randomR (1,6)

:1:0:
    Ambiguous type variable `g' in the constraint:
      `RandomGen g'
        arising from a use of `randomR' at :1:0-12
    Probable fix: add a type signature that fixes these type variable(s)

RandomGen のインスタンスである g が決まらず、曖昧だとエラーが表示される。

一体、RandomGen クラスとは何なのか… (+_+)

The library provides one instance of RandomGen, the abstract data type StdGen. Programmers may, of course, supply their own instances of RandomGen.

自分で、RandomGen クラスのインスタンスを定義することはできる。しかし、ライブラリには RandomGen のインスタンスとして StdGen が用意されている。

randomR 関数に戻り、曖昧だと言われた関数の型を確認する。

Prelude System.Random> :t randomR (1,6)
randomR (1,6) :: (RandomGen g, Random t, Num t) => g -> (t, g)

返される関数のタプルの第 1 要素が、Num クラスのインスタンスだけではなく、Random クラスのインスタンスであることが分かる。理由は、randomRRandom クラスのメソッドであるため。

randomR :: RandomGen g => (a, a) -> g -> (a, g)

つまり、randomR 関数の型における型変数 a は、Random クラスのインスタンスでなくてはならない。

 

3. Random クラス

Random クラスのインスタンス を見ると、Bool, Char, Double, Float, Int, Integer 型が挙げられている。

Random クラス は、特定の型から値を抽出するためのクラス。Random クラスのインスタンスになることで、各々の型に応じたランダムな値を生成する。

SnapCrab_NoName_2012-7-21_1-53-18_No-00

a. randomR 関数

Bool, Char 型でランダムな値を生成してみる。

          getStdRandom (randomR (False, True)) >>= print
          getStdRandom (randomR ('A', 'Z')) >>= print

型に応じて、指定した範囲でランダムな値が返ってきた。

 

b. random 関数

範囲を指定しない random 関数もある。この関数は、型に応じたランダムな値が返る。

          (getStdRandom random :: IO Int) >>= print
          (getStdRandom random :: IO Bool) >>= print
          (getStdRandom random :: IO Char) >>= print
          (getStdRandom random :: IO Double) >>= print

random 関数では、型を指定する必要がある。

 

c. randoms 関数

do 記法を使う場合、次のようにしてランダムな値のリストを得ることができる。

main = do
  gen <- getStdGen
  print $ take 10 $ (randoms gen :: [Int])
  print $ take 10 $ (randoms gen :: [Double])
  print $ take 10 $ (randoms gen :: [Bool])

rondoms 関数の型は、

randoms :: RandomGen g => g -> [a]

追記 (2009.10.24) : ふつうのHaskellプログラミング (p.277) には、次のように書かれている。

import System.Random
main = do g <- getStdGen
          let ns = take 3 (randoms g)
          print $ (ns :: [Int])

 

4. わからない点

ランダムな値を生成する方法分かった。しかし、生成する仕組みが分からない。

特に randomR の返す関数の型と、それを利用してランダムな値を生成する getStdRandom の絡みが理解できない。(@_@;)

081122-004

randomR に第1引数を与えると、RandomGen のインスタンスが決まらないため、曖昧であるとエラーがでる。しかし、getStdRandom に与えると動作するのはなぜだろう?

 

5. global random generator

getStdRandom のドキュメントには、

Uses the supplied function to get a value from the current global random generator, and updates the global generator with the new generator returned by the function.
(太文字は引用者による)

global random generator というのは何だろう?これと、与えられた関数を使ってランダムな数値を作りだしているらしい。

The global random number generator によると、

There is a single, implicit, global random number generator of type StdGen, held in some global variable maintained by the IO monad. It is initialised automatically in some system-dependent fashion, for example, by using the time of day, or Linux's kernel random number generator.

暗黙の StdGen 型である global random number generator があり、 IO モナドによりグローバル変数で管理している? それがシステムに依存した形で日付などを元にして初期化されると。多分、これがランダムな数値を生成する元になるということのようだ。

 

6. ソースコード

ソースコードを見ると、

Haskell Code by HsColour

getStdRandom :: (StdGen -> (a,StdGen)) -> IO a
getStdRandom f = atomicModifyIORef theStdGen (swap . f)
  where swap (v,g) = (g,v)
theStdGen :: IORef StdGen
theStdGen  = unsafePerformIO $ do
   rng <- mkStdRNG 0
   newIORef rng
Data.IORef

atomicModifyIORef :: IORef a -> (a -> (a, b)) -> IO b

atomicModifyIORef の宣言で `a’ に相当する部分が、getStdRandom の中において、theStdGen を呼出すことにより StdGen 型となり、getStdRandom に渡した関数 g –> (a,g) の g が実際に決まるということなのかな?

 

getStdGen

Cookbook – HaskellWiki を参考にした例では、getStdGen 関数を利用した。この関数の型は、

getStdGen :: IO StdGen

StdGen が IO で包まれている。IO で包まれていると言うと、外部からの入力を受けいれる関数

getContents :: IO String

を思い出す。

この関数の動作もよくわらない。イメージとしては、IO でString が包まれて外部から運ばれてくる感じ。

StdGen もよくわからない。同じように、何かが IO で包まれて Haskell の世界にやってくると考えればよいのだろうか?先ほどの説明に照らして考えると、日時などを元にして StdGen の値が作られるので、この値が IO に乗ってやってくるということだろうか。

081122-005

関連記事

追記 (2009.10.24) : Haskell において「状態」を更新する方法について理解しておくと、getStdRandom の 第1引数である StdGen -> (a, StdGen) の意味がわかる。

参考サイト