晩ご飯決定Rubyワンライナー改めメルセンヌ・ツイスタ初期化されてないと思ったらされてたので動作は最新ソースで再確認しよう

奥さんがいない日の晩ご飯は外食にしちゃいがち。
自分は吉野家(Y)かニラレバ定食(N)を出す中華屋さんに行く事が多いのですが
優柔普段なので、どちらを食べるか決めるのに時間がかかってしまいます。
そうすると余計に空腹状態の時間が増えてしまうYorN問題が発生します。


そこで自分は以下のようなワンライナーを書いて解決していました。

$ ruby1.9 -ve 'p [:Y,:N].choice'
ruby 1.9.0 (2008-06-20 revision 17482) [i486-linux]
:Y


今日は吉野家に行きます。


(続きは食後に書く)
(吉野家で牛すき鍋定食大盛りを食べてきたので続きを書く)


最近、Ubuntuを8.10にしたのですが、その時に
Rubyのバージョンが1.8.6から1.8.7に上がりました。
1.8.7は1.9のメソッドがバックポートされていて、
1.8系でもArray#choiceメソッドが使えるゼッと
とか言って上記ワンライナーを実行してみたら...

$ ruby1.8 -ve 'p [:Y,:N].choice'
ruby 1.8.7 (2008-08-11 patchlevel 72) [i486-linux]
:N


ニラレバか...
あれ?

ver = 1.8
100.times {
  IO.popen("ruby#{ver} -e 'p [:Y,:N].choice'") { |io|
    puts io.gets
  }
}


結果

:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N
:N


ニラレバだ!
何度やってもニラレバになる!


これでは吉野家の人気が下がり、ニラレバを出す中華料理屋の上がってしまう!
ご飯屋さんはあくまで味と価格とサービスで闘うべきであって
関係の無いスクリプト言語の挙動で人気が左右されてしまっては困る訳です。
Rubyのソース引っ張ってきて調査せざるを得ない。


(調べたら書く)


ruby-talk:303898 でchoiceが変だよーと言っている人がいましたが
何を言っているかわからないよーと言われており、
再度説明するもそこまでで発言が埋もれていました。残念。
しかも、rand()を一回実行することで回避できるという事も
おっしゃられています。


http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/303898



ここでちょっと横道にそれますが、
Ruby1.9では戻り値が1つであるchoiceメソッドはなくなって
戻り値の個数を指定できる(指定しなければ1つになる)
sampleメソッドが追加になっています。
1.8.7は1.9に追従したつもりが、結果、1.8.7限定メソッドになっとるw


参考:
Ruby 1.8.7での新メソッド更新(Array#choice→Array#sample) - http://rubikitch.com/に移転しました
http://d.hatena.ne.jp/rubikitch/20081107/1225995198
[ruby-talk:319272]
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/319272



で、横道から戻ってソースを見ることにします。
1.8.7のArray#choiceメソッド。

static VALUE
rb_ary_choice(ary)
    VALUE ary;
{
    long i, j;

    i = RARRAY(ary)->len;
    if (i == 0) return Qnil;
    j = rb_genrand_real()*i;
    return RARRAY(ary)->ptr[j];
}


配列の長さをiに入れて
それが0だったらnilを返して
[0,1)の乱数に配列の長さをかけてjに入れて
配列にjでアクセスする。
非常にシンプルですね。


怪しいのはrb_genrand_real()な訳で。
他にrb_genrand_real()を使っている人は...rb_ary_shuffle_bang()関数。
Rubyではshuffleとshuffle!メソッドだ。

ver = 1.8
10.times {
  IO.popen("ruby#{ver} -e 'p [1,2,3].shuffle'") { |io|
    puts io.gets
  }
}
$つ ruby shuffle.rb 
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]


ズコー。毎回、shuffleされる結果が同じやんか。
rand()を1回実行してやってからshuffleすれば大丈夫というのは
choiceと同じみたい。(実行結果は省略)


以下、1.8.7と1.9.0のソースを見比べて書く。



Rubyの乱数生成はメルセンヌ・ツイスタというアルゴリズムを使って生成しています。
メルセンヌ・ツイスタについて
数学的な難しいところは良くわかっていませんが
一般に困らないくらい長い周期や均等に分布するのが特徴らしいです。


参考:メルセンヌ・ツイスタについて
What & how is MT?
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/what-is-mt.html

http://ja.wikipedia.org/wiki/メルセンヌ・ツイスタ


実際、Rubyのコード、つまりrand()メソッドでは
簡単に言うと以下の2種類のステップで乱数を生成しています。


メルセンヌ・ツイスタで乱数を発生させるとこは
種から生成するのですが種が同じ値だと毎回同じ値が生成されます。
なので、メルセンヌ・ツイスタに渡す種を作るのにまた乱数みたいなものが必要で
Rubyでは可能であれば/dev/urandomを読み出して(Windowsでは読めないような??)
それに時間やらpidやら自動変数のアドレスやらでゴチャゴチャとして
毎回変わるような種を作っています。(random.cのrandom_seed())


で、1.8.7のArray#choiceとArray#shuffleを実行しても
random_seed()を通らないので毎回同じ乱数が生成されているということがわかりました。


1.9ではどうかというと別件にて
random.cにInit_RandomSeed()という関数が追加されていて
これがrandom_seed()を通る。そして、インタプリタ初期化時に実行されるように
inits.cのrb_call_inits()からInit_RandomSeed()をcallするようにしているので
問題無いのでした。(最後、駆け足)


追記:
1.8の件をruby-devに投げたら、既に直っていますよと言われ
安定版スナップショット(ruby 1.8.7 (2008-11-22 revision 0))で
確認したら直っていました。orz
何かあったらちゃんと最新のソースで再確認すべしという基本に立ち戻りました。