コンユウメモ @kon_yu

作ったガラクタとか、旅行とかの話

Rubyで乱数のシードを設定しないでランダムな数値を取得できるかRubyのコードを調査した

概要

Rubyはなぜ乱数のシードを入れないで乱数を生成できるのかRubyの言語自体のソースコードを軽く読んで調査した。

乱数を生成するクラスは乱数のシードを引数に入れないと、乱数のシードを生成するメソッドを呼び出す、その乱数を生成するロジック内ではCPUクロック数やOSの乱数生成を呼び出して利用して乱数のシードを生成していた。

乱数のシードがUNIX timeだけに依存したりしていないので、高アクセスなサービスや並列化したバッチ処理などで同時刻に乱数を生成してもシード値が同じでバラけないってことはなさそうだ。

はじめに

Rubyのランダムな数値を生成する場合はこのような書き方をする。

r=Random.new
r.rand(10)
=> 5
引数に整数を入れると0-9までのランダムな数値(Integer)を出力する

引数を少数を入れるとFloat型でランダムな数値を出力する
r.rand(5.5)
=> 1.81617029321715

randメソッドの仕様

Method: Random.rand — Documentation for core (2.7.0)

Rubyで乱数を生成するは、擬似乱数列生成器にメルセンヌ・ツイスタを利用している。擬似乱数発生装置は乱数のシードと呼ばれる初期値が必要だ。(シードが不要な擬似乱数発生装置もあるかもしれないが今回の主題ではない)

コンパイラを利用するプログラミング言語では、乱数を生成する際には乱数のシードを用意するものが多くインタプリタの場合はRubyのように乱数のシードを使わなくても乱数を生成することができるものが多い印象がある。

Ruby以外の他の言語の乱数生成ロジック

乱数のシードが必要な例としてGo言語の場合

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().Unix())
    number:= rand.Int()
    fmt.Println(number)
}

乱数のシードが不要な例としてJavaScriptの場合

const number = Math.random()
console.log(number)

このJSのMath.random()の定義を見るとJavaScriptでは乱数のシードを入れられない逆にシードを入れさせないっていう仕様も珍しい

The implementation selects the initial seed to the random number generation algorithm; it cannot be chosen or reset by the user.

乱数のシードが不要な言語の場合、乱数のシードはどうしているのかRubyを例に調べてみる

Randomのコンストラクタの仕様を見ると Method: Random#initialize — Documentation for core (2.7.0)

Random.new_seedでランダムなシードの値を生成してインスタンス化しているしている。

Random.new_seedを実行してみるとこのような結果が返ってくる

Random.new_seed
=> 115032730400174366788466674494640623225

乱数のシード生成するのに乱数を生成している? ではその乱数を生成するシードはどう生成するというのだろうか?おそらく擬似乱数発生装置を利用しない方法で乱数を生成しているはずだ。

この対象のRubyのコード、Ruby本体のコードなのでC言語のコードで、Random.new_seedのコードは以下のように作られている。これを1行ずつ読み解く

Method: Random.new_seed — Documentation for core (2.7.0)

static VALUE
random_seed(VALUE _)
{
    VALUE v;
    uint32_t buf[DEFAULT_SEED_CNT+1];
    fill_random_seed(buf, DEFAULT_SEED_CNT);
    v = make_seed_value(buf, DEFAULT_SEED_CNT);
    explicit_bzero(buf, DEFAULT_SEED_LEN);
    return v;
}

1行目のVALUE v;は6行目return vで関数の返り値としているので生成されてたランダムな数値が格納される変数。

2行目uint32_t buf[DEFAULT_SEED_CNT+1];はDEFAULT_SEED_CNTより1大きいサイズの配列を宣言、固定値DEFAULT_SEED_CNTのサイズは4

3行目fill_random_seed(buf, DEFAULT_SEED_CNT); fill_random_seedで乱数のシードを作るための乱数のシードを作ってる。

このメソッドのロジックをざっくり読むと配列のサイズ4のseedの各要素にfill_random_bytesでランダムなバイト数値を入れ、その上にCPU時間からナノ秒やマイクロ秒を取得してXORで上書きしている用に見える。 fill_random_bytesではちらっと見る限りLinuxシステムコールで乱数を読んでるように見えが、今回は深追いしていない

static void
fill_random_seed(uint32_t *seed, size_t cnt)
{
    static int n = 0;
#if defined HAVE_CLOCK_GETTIME
    struct timespec tv;
#elif defined HAVE_GETTIMEOFDAY
    struct timeval tv;
#endif
    size_t len = cnt * sizeof(*seed);

    memset(seed, 0, len);

    fill_random_bytes(seed, len, FALSE);

#if defined HAVE_CLOCK_GETTIME
    clock_gettime(CLOCK_REALTIME, &tv);
    seed[0] ^= tv.tv_nsec;
#elif defined HAVE_GETTIMEOFDAY
    gettimeofday(&tv, 0);
    seed[0] ^= tv.tv_usec;
#endif
    seed[1] ^= (uint32_t)tv.tv_sec;
#if SIZEOF_TIME_T > SIZEOF_INT
    seed[0] ^= (uint32_t)((time_t)tv.tv_sec >> SIZEOF_INT * CHAR_BIT);
#endif
    seed[2] ^= getpid() ^ (n++ << 16);
    seed[3] ^= (uint32_t)(VALUE)&seed;
#if SIZEOF_VOIDP > SIZEOF_INT
    seed[2] ^= (uint32_t)((VALUE)&seed >> SIZEOF_INT * CHAR_BIT);
#endif
}

4行目v = make_seed_value(buf, DEFAULT_SEED_CNT);でbufに格納されている乱数のシードから乱数を生成している make_seed_valueではbufに入っている配列の数値をrb_integer_unpackで結合して乱数を生成していると思う。

static VALUE
make_seed_value(uint32_t *ptr, size_t len)
{
    VALUE seed;

    if (ptr[len-1] <= 1) {
        /* set leading-zero-guard */
        ptr[len++] = 1;
    }

    seed = rb_integer_unpack(ptr, len, sizeof(uint32_t), 0,
        INTEGER_PACK_LSWORD_FIRST|INTEGER_PACK_NATIVE_BYTE_ORDER);

    return seed;
}

上記のロジックをまとめると、random_seedのロジックは配列を用意して、その配列にCPU時間など使ってランダムな数値を格納したあとに、配列を結合して乱数のシードを作っていると言える

まとめ

  • Rubyの乱数の生成でシードデータが必要か否かを考えると、Rubyのコードを読む限り不要だと言える。必要なパターンがある気がするが今のところ思いつかない
  • 普段何気なく使っている乱数がどうやって実装されているのか垣間見ることができた
  • C言語読むの5年ぶりぐらいな気がする

「C」の茶の国から「T」の茶の国へと、本当に違う種類の国へ行くことができるのかデータ分析

f:id:kon_yu:20200208162245p:plain

イントロダクション

バックパッカーのバイブル沢木耕太郎深夜特急で、ハナモチ氏という人物がお茶について語っているシーンが有る。

手元にある文庫版の第5巻を確認するとこんな一節である。

「彼らはTで始まるチャイを飲んでいる。でも、僕たちはCのチャイを飲んでいるのさ」

中略

私はトルコからギリシャに入ることで、アジアからヨーロッパへ、イスラム教圏からキリスト教圏へ、茶の国からコーヒーの国へ、「C」の茶の国から「T」の茶の国へと、違う種類の国へ来てしまっていたのだ。

というようにユーラシア大陸を東から西に行くとお茶の名前がChaからTeに変わるのだ。

北京語でお茶のことをチャーと発音するように、中国から陸路で伝わった茶はトルコまでCha系の発音で呼ばれることになる。

ではTeの方はというと、オランダがお茶を輸入した福建省あたりのビン南語ビン南語ではお茶のことをテーと発音するようで、そのオランダが海路で運んでいたため、オランダの植民地やヨーロッパ諸国ではお茶のことをティーやテーなどの発音表現する。

というわけでこのどこの国がCha系の国で、どこの国がTe系のなのか、テクノロジーを駆使して調べてみた。

調査方法

Googleスプレッドシートの翻訳関数

Googleスプレッドシートの関数に、GOOGLETRANSLATEというものがあり、日本語から英語へなど翻訳を行ってくれる。

例えばこんな風にセルにかいてあげると

=GOOGLETRANSLATE("tea","en","ja")

お茶と出力される

この関数の変数はそれぞれ

  1. 訳したい文字
  2. 翻訳元の言語2文字の略称
  3. 翻訳先の言語2文字の略称

となっている。つまり各言語のこの2文字のコードを片っ端から調べて、英語のteaから翻訳してやれば良いのだ。

この機能を知ったきっかけはサンミンさん@gijigaeのこのツイート

このツイートを見て各言語ごとにChaかTeか調べることができると思ったのがこの調査を始めたきっかけである。

ISO 639-1 言語を2文字表現

さきほどの2文字のコードは何かというと、国際規格のISO 639-1であるようだ。 このISO 639-1のリストをWikipediaから引っ張ってきた。

ja.wikipedia.org

そして翻訳した結果はこちら docs.google.com

表の中で#VALUE! となっているのはGoogle翻訳が対応していない。すべてのISO 639-1で定義されている言語にGoogle翻訳は対応していないようだ。

この結果ISO 639-1の言語184言語中、101言語の翻訳できた。

というかGoogle翻訳って101言語対応してるの?すごない!!

ここまでの結果により翻訳に対応していない言語をフィルタリングできる。

Google翻訳ページ

Google翻訳のページでは、メジャーな言語はその翻訳とともに発音を再生してくれる機能が存在する。

またGoogle翻訳のページのURLは、ISO 639-1コードと翻訳したい文字をURLに含めることで動的に生成できる。

このようにセルを設定する

=concatenate("https://translate.google.com/?hl=ja&op=translate&sl=en&tl=", D77,"&text=tea")

するとGoogle翻訳のリンクを作ることができる

https://translate.google.com/?hl=ja&op=translate&sl=en&tl=ja&text=tea

forvo.com

forvo.comは各地の言語の単語の発音を登録できるサービスでありこちらもURLを動的に生成できる。

こちらもこのようにセルを設定する

=concatenate("https://forvo.com/word/",E77 ,"/#", D77)

するとforvo.comのリンクを作ることができる

https://forvo.com/word/お茶/#ja

それぞれの音声をすべて耳で聞いてChaかTeかを振り分け

それぞれの音声をGoogle翻訳で発音を再生できなかった場合はfevor.comで音声を確認した。 聞いた音声を元に下記のカラムを追加した。どちらにも音声がないものは今回は対象外とした。

  • 聞いた音声にフリガナ
  • ChaかTeかを分類
    • 音声がチャに近い音で発音されるか、テに近い音で発音されるかそれぞれ分けた
  • 話されている国をラベリング
    • 各言語が話されている国がどこかを荒く調べて、それぞれ独立したデータとした

※ 話されている国のラベリングは、南北のアメリカ大陸で話される英語やスペイン語ポルトガル語については茶の伝来について分布を見る際にノイズになるので英語はイギリス、スペイン語スペイン語ポルトガル語ポルトガルだけにしてある。

このあたりはデータ分析と言うよりは根性である。

振り分けした一例 f:id:kon_yu:20200208163654p:plain

結果

データを地図上にプロットすると以下のように、Chaの国、Teの国、 ChaでもTeでもないその他の国を作成した

Chaの国とTeの国

青がChaの国、赤がTeの国

f:id:kon_yu:20200210092211p:plain datastudio.google.com

グラフの濃淡は、対象の言語の種類が複数あると濃くなる

ChaでもTeでもないその他の国

f:id:kon_yu:20200210091842p:plain

datastudio.google.com

考察

ChaやTeについて

冒頭の深夜特急の引用文のように、トルコでChaの国が終わり、ギリシャからTeの国々になるのがデータをプロットした地図から分かる。

ヨーロッパはだいたいTe系統で、その他の国々はだいたいCha系統。

しかし東ヨーロッパの国々は結構Chaの国であった。これは意外でイメージだとギリシャでバッツリTeの国だと思っていた。

南インドやその近くのスリランカではなされるタミル語はお茶のことをテニーと発音し、インドはChaの国でありTeの国でもあることがわかった。

Teの国は海路で茶が伝わったとされるが、インドネシアや、南アフリカナミビアはかつてオランダの植民地だった影響でTeの国なのだと思われる。先程のタミル語が話されるスリランカもオランダの植民地だったのでTeの国であるのだろう。

Teの国だと思っていたヨーロッパの中でもポルトガルはChaの国

ポルトガル語はChaの国でお茶のことをチャと発音する。

ポルトガルは植民地のマカオから茶を輸入していたので、広東語の茶とおそらく同じ音で伝わったものだと思われる。 (これは友達の @Y_Hirano が教えてくれた。多謝)

ウィキペディアの茶の項目にも書かれている(出典は無いが) ja.wikipedia.org

Chaの国で話されるお茶の発音は大体チャイ

各言語の音を直接自分の耳で聞いてフリガナを振っていくと、Chaの国86ヶ国中、32ヶ国がチャイと発音し37%の国がお茶のことをチャイと発音する。

特に東西問わずアジア圏でチャイが多く、アジアのカフェやレストランではチャイと言うとお茶が出てくる可能性が高いのでぜひチャレンジしてみてほしい。

お茶をチャイと発音する国一覧

f:id:kon_yu:20200211180627p:plain datastudio.google.com

ChaでもTeでもない国が存在する

ChaでもTeでもない国がいくつかあった。

ポーランド語、リトアニア語、ソマリ語である。

ポーランド語はのお茶を表すherbata(ハルバータ)はラテン語herba theaが語源らしい herbata - ウィクショナリー日本語版

そのラテン語herba theaはそれぞれ

つまりラテン語ハーブティーが語源のようである。

リトアニア語のお茶を表すarbata(アルバータ)はリトアニアポーランドに隣接しているので文化圏が近いためポーランド語がなまったものだと思われる。

ソマリア語のお茶を表すshaahはfevor.comにもGoogle翻訳にも存在しなかったので特別にyoutubeで検索してみるとソマリア語で喋っている動画にshaahを発音しているものを見つけた。

https://www.youtube.com/watch?v=BAAt03W2z7g , https://www.youtube.com/watch?v=1lz87j-pKwM

これら2つの動画からどうやらshaahはシャーと発音するらしい。

今回はアラビア語の茶、”シャーイ”はChaの国と分けている。字面で見るとChaじゃないじゃないかと思うだろうが、音を聞くとTeかChaかどちらかで分類するとCha寄りだと判断した。

アラビア語のシャーイがなまってソマリア語はシャーであろう。広義の意味ではChaの国としても良いかも知れない。

その他、今回の調査でわかったこと

じゃーねー。ロケットビーバーイ

給湯器が家の外にある北限の町はどこなのか長年の疑問をGoogleストリートビューで調査

給湯器が家の外にある北限の町はどこなのか人生で一度は気になったことはあるよね?オレはある

オレの故郷の北海道は室内の洗面所にあり、住んだことのある東京や神奈川、茨城でも給湯器は外にあった。

おそらく積雪量も関係しているだろうが、雪の少ない太平洋側を対象に仙台からと青森からの南北から絞り込んで調査していった。

調査方法はGoogleストリートビューを駆使してなんの変哲もない住宅街を確認していくという根気のいる作業をしていった。

以降は探しながらツイッターに書いていたものをそのまま乗せるのでライブ感を楽しんでもらいたい。

まずは南から仙台。

仙台給湯器あり。前に聞いた話だと仙台のばあちゃんの家は中にあるけど、そいつの家の給湯器は外にあったような気がするって話。 https://t.co/xm7vXC8mxj

次に盛岡

盛岡でも外に給湯器があるのを発見した。 https://t.co/GHkAVYp4na

今度は逆に青森市から、無いのを探すのはけっこう大変だ、探すのを止め時がわからない

青森市まで行くと、給湯器が屋外になさそうだ。 https://t.co/Zot9UYlENl

だんだんストリートビューで給湯器を探すコツがわかってきたぞ 一軒家の給湯器はだいたい正面にので道路沿いの画像のストリートビューでは探しにくいが、アパートなどの集合住宅は正面のドア横辺りにある

っていう謎のコツを掴んできた。


八戸でもなかった。(外)盛岡 => x > (内)八戸 絞れてきた。 https://t.co/csoizwXVQz

二戸にもなかった。もう少し南下する

田舎のほうが家が家離れているのでストリートビューから家の裏の方を探しやすい https://t.co/QS1z0vClp2

八幡市にも給湯器は外にはなさそうだ。 っていうか外に置く灯油のタンクがあるところは灯油ストーブを焚くぐらい寒いから給湯器は外になさそう https://t.co/bkxuP6ym2v

八幡市の東にある岩手町にもなさそう https://t.co/TZ04CSvptv

やっぱも盛岡には給湯器がある https://t.co/EUlkHfVsME

盛岡には外に給湯器があった。

結論: 今回の調査では給湯器が家の外にある北限は岩手県盛岡市!!

この結論を盛岡仮説として学会で発表したいと思う次第である。