はじめに
Web系の何がしかを弄りたいという気持ちと、関数型言語っていうのを試してみたいという気持ちが悪魔合体して、最近Elmで遊んでいる。TRPG好きならいじってしまう乱数生成で遊んでいたら思わず楽しかったので何番煎じかもはや数えてないけど記録。
まとめ
- elm/randomは直接乱数を生成するのではなく、乱数生成のレシピ(Random.Generator a)を作り、それをRandom.generateで実際に生成する二段構え
- 乱数をもとに計算したり、乱数を複数作ったりするときも、最終的にはRandom.Generator a型にしてgenerateに渡す(のが想定されているぽい)
- 最初はちょっとめんどくさいけど、やり方がわかるとわかりやすい気もする。多分
乱数を生成する
Randomの使い方は、Introduction to Elmにもページがあり、オンラインエディタで色々といじることができる。下にコードを抜粋する。
import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random
type alias Model =
{ dieFace : Int
}
init : () -> (Model, Cmd Msg)
init _ =
( Model 1
, Cmd.none
)
type Msg
= Roll
| NewFace Int
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Roll ->
( model
, Random.generate NewFace (Random.int 1 6)
)
NewFace newFace ->
( Model newFace
, Cmd.none
)
Rollメッセージを呼ぶとRandom.generate~のコマンドが呼び出されて、レシピに従った乱数がNewFaceメッセージに送られるしくみ。Random.generateでいきなりModelの中身に乱数の結果をぶち込むことはできず、いったん別のメッセージを挟むことになる。あまり関数型のエッセンスをまだ理解していないので詳細な解説はできないけど、乱数をさらに加工したいときはNewFaceメッセージの中で何やらすることもできそう。
ここからは、どんな乱数生成ができるか、Generatorを色々紹介したい。
整数値の乱数
上の例でも書かれている整数値の乱数を出すGenerator。下の例だと1から6までの整数を出力してくれる。
dice6D : Random.Generator Int
dice6D = Random.int 1 6
小数の乱数
整数とやることは変わらない。Referenceを見ると、生成した乱数に対してp < 0.4
などとチェックをすることで、40%の確率で何かをしたりしなかったりすることができますよ!という応用例が示されている。ちなみに、整数や小数の乱数の確率分布は一様分布になっている。
zeroToOne : Random.Generator Float
zeroToOne = Random.float 0 1
一様分布ではない乱数
weightedという関数を使うことで、グラサイのような確率に偏りのある乱数を生成することができる。
rollWeighted : Random.Generator Int
rollWeighted = Random.weighted (10, 1) [(20, 2), (30, 3), (40, 4), (50, 5)]
上の例だと5が最も出やすいが、6はないので出ない。ちなみに、確率の比率の合計が100になっていないが、Elmが勝手にそれぞれの確率を重み/合計
で計算してくれる。賢い。
定数
乱数ではないけど、
P(x) = \begin{cases}
1 & (x=a) \\
0 & (otherwise)
\end{cases}
という確率分布を考えていると思えばいい(と勝手に思っている、積分して1にならんけど)。
alwaysZero : Random.Generator Int
alwaysZero = Random.constant 0
乱数を加工する
上の関数群はただ乱数のレシピを作ってくれるだけで、その乱数をさらに加工するには一工夫する必要がある。例えば乱数を5倍したいなどという場合、Random.mapを使う。
make5x : Random.Generator Int
make5x = Random.map (\a -> a * 5) (Random.int 1 6)
2個の乱数を合わせて使いたい時はmap2。例えば2つの6面ダイスを振って値を合計したい時は
roll2D : Random.Generator Int
roll2D = Random.map2 (\a b -> a + b) (Random.int 1 6) (Random.int 1 6)
これでやっとソード・ワールドができるようになった。
Call of Cthulhuの新版では、ステータスを3D6*5で算出するようになった。3つの乱数にはmap3を使う。
makeStatus : Random.Generator Int
makeStatus = Random.map3 (\a b c -> (a + b + c) * 5) (Random.int 1 6) (Random.int 1 6) (Random.int 1 6)
以下map4、map5があるのだが、これは大変面倒だ。どうせ同じD6ダイスを使うのだし、任意個数のダイスを振って合計を出したい。そこで、乱数のリストを作成しよう。Random.listで、Int引数の数だけ乱数を持つリストを作れる。
rollND6 : Int -> Random.Generator (List Int)
rollND6 dice = Random.list dice (Random.int 1 6)
この乱数をmapで煮るなり焼くなり好きにできる!
おわりに
elm/randomは、乱数の出力に二段構えの構成で最初はとっつきにくいけど、関数は色々痒い所に手が届くものが多くて、欲しい乱数をmapやら何やらで出すのはまるでパズルみたいな感覚。他にも色々あるみたいなのでもう少しいじってみよう。