はじめに
この記事で乱数を使ったのですが、Elmの独特な乱数の取り扱いについて全く解説できずに終わってしまいました。
この記事で解説という名のリベンジをしてみようと思います。
最低限、Model、Update、View、CmdといったThe Elm Architectureの要素を理解している方が対象です。
Random.map
やRandom.andThen
について解説している記事はどうやらなさそうなので、多分多少の新規性はあるでしょう、きっと・・・
Elmは純粋関数型言語と言われており、純粋な関数、すなわち引数が同じなら、常に同じ戻り値を返すような関数しか定義することができません。
OSの時計へのアクセスや、HTTP(Ajax)通信のような外の世界へのアクセスを、関数内で行うことはできません。(Debugモジュールのことは一旦無視しましょう。)
乱数の生成はどう見ても外の世界へのアクセスを伴います。 処理の途中で乱数を使うような関数は、引数が同じなら、常に同じ戻り値を返す関数では決してありません。 (同じなら乱数使う意味ないですよね)
しかし、Elmはその純粋性と、外の世界へのアクセスを伴う処理を両立しています。
それ故、多少扱いがいわゆる一般的な手続き言語とは異なっており、初めて取り組む方にとってはちょっととっつきにくいかもしれません。(が、多分すぐ慣れますし、慣れたら簡単です。)
この記事では以下の実装を行います。
1. 乱数を生成してみる
2. 偶数の乱数を生成してみる
3. リストを受け取り、リストをシャッフルするような関数を作ってみる
4. 乱数を生成し、その数だけ偶数の乱数を生成してみる(=偶数のリストを生成する)
5. 懐かしのズンドコキヨシ
これだけできたら多分困らないでしょう・・・
次の記事では(ちゃんと書きます)トランスパイル後のJavaScriptで処理が実際にどのように行われているかを見ていきます。
本編
基本的な構成について
ベースのコードは以下の通りとします
また、ソースはここに置いておきました。
module Main exposing (Flags, Model, Msg(..), main)
import Browser exposing (element)
import Html as H
import Random
type alias Model =
{ partOne : Int
, partTwo : Int
, partThree : List String
, partFour : List Int
, partFive : String
}
type Msg
= GotOne Int
| GotTwo Int
| GotThree (List String)
| GotFour (List Int)
| GotFive String
type alias Flags =
()
main : Program Flags Model Msg
main =
element
{ init =
always
( { partOne = 0, partTwo = 0, partThree = [], partFour = [], partFive = "" }
, Cmd.none
)
, view = view
, update = update
, subscriptions = always Sub.none
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotOne int ->
( { model | partOne = int }, Cmd.none )
GotTwo int ->
( { model | partTwo = int }, Cmd.none )
GotThree intList ->
( { model | partThree = intList }, Cmd.none )
GotFour stringList ->
( { model | partFour = stringList }, Cmd.none )
view : Model -> H.Html Msg
view m =
let
concanatedPartFour =
List.map String.fromInt m.partFour |> String.join ", "
in
H.table []
[ H.tr
[]
[ H.td [] [ H.text "Part1" ]
, H.td [] [ H.text <| String.fromInt m.partOne ]
]
, H.tr
[]
[ H.td [] [ H.text "Part2" ]
, H.td [] [ H.text <| String.fromInt m.partTwo ]
]
, H.tr
[]
[ H.td [] [ H.text "Part3" ]
, H.td [] [ H.text <| String.join ", " m.partThree ]
]
, H.tr
[]
[ H.td [] [ H.text "Part4" ]
, H.td [] [ H.text concanatedPartFour ]
]
, H.tr
[]
[ H.td [] [ H.text "Part5" ]
, H.td [] [ H.text <| m.partFive ]
]
]
ModelやMsgが、各問題の答えに対応するようにしています。
あと、elm/random をインストールしてください。
1.乱数を生成してみる
まずは乱数を生成してみます。 ここでは1〜10の整数を生成してみましょう。
partOne : Random.Generator Int
partOne =
Random.int 1 10
Random.Generatorという謎の型が出てきたり、Intが戻り値じゃなかったり、Random.intというなんとなく用途は分かりそうな関数が出てきました。 一つずつ解説していきます。
まずRandom.intの型は以下の通りです。
Random.int : Int -> Int -> Random.Generator Int
生成する乱数の最低値と最高値を引数にとり、Random.Generator Int
という謎の値を返していますね。
では次に、Random.Generator
とはなんでしょうか?
ここでは、乱数を生成する指示書としておきましょう。 「また訳のわからない例えを使いやがって・・・」と思われる方も多数いらっしゃるかもしれません。 しかしこれはあながち例えでもないのです・・・ この指示書の正体は次の記事でちゃんと説明します。
Random.Generator
は型引数を一つ取ります。 なんとなくお察しかもしれませんが、この型引数は、指示書によって最終的に生成される値の型を示しています。
つまり、partOne
は指示書です。 乱数値そのものではありません。 今回のpartOne
はただの値ですが、今後引数をとって、指示書を返すような関数を作っていきます。 しかし、その場合でも、どんな引数を与えたとしても、返ってくるのは引数に対応した同じ指示書です。
つまり、乱数を使う処理であっても、純粋な関数として定義できるのです!
最初にこれを聞くとなんか屁理屈のように感じるかもしれませんが、実際そうなっているのです。
ところで、実際に乱数の値を受け取るにはどうすればよいのでしょうか?
まずこの関数を使います。
Random.generate : (a -> msg) -> Random.Generator a -> Cmd msg
型引数が多数使われていますが、今回の場合はこうなるでしょう
Random.generate : (Int -> Msg) -> Random.Generator Int -> Cmd Msg
つまり、「生成される乱数値を引数にとって、Msgにする関数」、および指示書を受け取って、Cmdを返す関数です。
Cmdはinit関数で戻り値にしたり、update関数で戻り値にするんでしたね。
そして、update関数の引数として生成された乱数を受け取ります。
今回はinit関数に渡してみましょう
main : Program Flags Model Msg
main =
element
{ init =
always
( { partOne = 0, partTwo = 0, partThree = [], partFour = [], partFive = "" }
, Random.generate GotOne partOne -- ← これ!!!!
)
-- 以下続く・・・
ということで渡してみました。
update関数で乱数値を受け取るところを再掲します。(ちょっと変数名を変えています)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotOne randomValue -> --このrandomValueが生成された乱数
( { model | partOne = randomValue }, Cmd.none )
-- 以下続く
GotOneという値に詰められた値として乱数を受け取ることができます。
乱数をModelに格納することができましたので、あとはview関数で好きに表示してください。
2. 偶数の乱数を生成してみる
というわけで無事に乱数を生成することができました。
今後は指示書からCmdを作り、initで返し、update関数で値を受け取る手順は省略し、指示書を生成するところまでを解説することにします。
また、以後は指示書という言葉は基本的に使わず、正式な名であるRandom.Generator
と呼ぶことにします。
今回は偶数の乱数を生成するRandom.Generator
を生成してみます。
しかし、公式パッケージのドキュメントには、偶数を生成するRandom.Generator
を返す関数など定義されていません。
そこで、乱数値を生成し、その値を2倍することで偶数の乱数を生成することにしましょう。
ではどうやるのでしょうか。 先ほどのように生成された乱数をupdateで受け取り、その値を2倍してからModelに格納する方法が思いつきます。
今回はそれでも良さそうです。 しかし、例えば正の整数の乱数を3つ生成し、その値を全て足した値〜全てかけた値の範囲で新たに乱数を生成、最初の3つの数のうち、新たな乱数と一番近い数と足した値を表示する
という処理だった場合は?
Cmdを発行 → Updateで受け取り という手順を何回も繰り返さないといけません。 しかも、最初の3つの乱数をModelに保存する必要もあるでしょう。 かなりめんどくさそうです。
大丈夫です、Elmにはこのような複雑な乱数処理を一発で行う方法があります。 つまり、最終的に欲しい値を得られるようなRandom.Generator
を作る方法があるのです。
カギはRandom.map
とRandom.andThen
とRandom.constant
です。(が、この章ではRandom.map
しか使いません。)
では今回のコードです。 今回は1〜100の間で偶数を生成することにします。
partTwo : Random.Generator Int
partTwo =
let
gen =
Random.int 1 50
double =
\n -> n * 2
in
Random.map double gen
(もちろんこれくらい1行でかけるのですが、今回はわかりやすさのため、冗長であってもこのように書きます。)
genは1~50の整数を生成するRandom.Generator
、doubleは値を2倍する関数ですね。
ではRandom.map
とはなんでしょうか? なんとなく察しがつくかもしれませんが、この関数を使うことで、Random.Generator
によって生成される予定の値に、関数を適用することができます!
Random.map
の型は以下の通りです。 型を見ればだいたい挙動がわかるのはElmのいい特徴です。
(a -> b) -> Random.Generator a -> Random.Generator b
戻り値もRandom.Generator
であることに注意してください。
あくまで処理の中で作ることができるのは、指示書だけです。 実際の値ではないのです。
指示書の例えで言うと、今回の関数はこんな感じでしょうか
# 指示書1
1. 1〜50の整数を生成する
2. 1の値を返す
# 指示書2
1. 指示書1を実行する
2. 1の値を2倍する
3. 2の値を返す
3. リストを受け取り、リストをシャッフルするような関数を作ってみる
実はこの関数はelm-community/random-extraにあるので、実際に使うときはこちらを使いましょう。
しかし学習のためなら、車輪の再発明だって許されるはずです。
シャッフル処理はいろいろ考えられるのですが、今回は以下の手順でやってみようと思います。
-
["a", "b", "c", "d"]
というリストがあるとする - リストと同じ長さの乱数列を生成する
- 最初のリストと2.の乱数列をList.map2でくっつけ、以下のようなリストにする
[("a", 32), ("b", 100), ("c", 123), ("d", 99)]
(数は例えです) - List.sortBy で、リストの各要素のタプルの2番目の要素でリストをソートする
-
List.map Tuple.first
で、余計な乱数を取り除く
関数内では乱数を直接扱えず、Random.Generator
として表現するElmでは、
結構ややこしそうな処理に見えます。 が、できます。
とりあえず関数全体を載せてしまいます
partThree : List a -> Random.Generator (List a)
partThree list =
let
randomNumbers : Random.Generator (List Int)
randomNumbers =
Random.list (List.length list) <| Random.int Random.minInt Random.maxInt
zipWithList : List Int -> List ( a, Int )
zipWithList intList =
List.map2 Tuple.pair list intList
listWithRandomNumbers : Random.Generator (List ( a, Int ))
listWithRandomNumbers =
Random.map zipWithList randomNumbers
sortedGenerator : Random.Generator (List ( a, Int ))
sortedGenerator =
Random.map (List.sortBy Tuple.second) listWithRandomNumbers
in
Random.map (List.map Tuple.first) sortedGenerator
関数の型については特に説明は不要かと思います。 そのまんまです。
ではletで定義した変数(変数って呼称いいんでしょうか?)について説明していきます。
まずrandomNumbers
です。2. リストと同じ長さの乱数列を生成する
に相当します。
Random.list
という関数が目につきますが、これの型は
Int -> Random.Generator a -> Random.Generator (List a)
で、第一引数で与えた長さだけ、第二引数のRandom.Generator
で生成される乱数のリストを生成します。
例えば第二引数に先ほどのpartTwo
、つまり偶数を生成するRandom.Generator
を指定すると、偶数の乱数リストが生成されます。 ここでは、整数のRandom.Generator
を指定しました。
Random.minInt
は、システムで乱数として生成できる最小の値を指します。Random.maxInt
も同様に最大の値を指します。 つまり、できる限り大きな振れ幅で整数の乱数値を生成します。
第一引数は普通にリストの長さですね。
では次にzipWithList
とlistWithRandomNumbers
です。
3. 最初のリストと2.の乱数列をList.map2でくっつけ(略)
に相当します。
まず、zipWithList
ですが、これは
[32,100,123,99・・・]
のような、randomNumbers
で生成されるであろう整数のリストを受け取って、最初のリストとくっつけ、
``[("a", 32), ("b", 100), ("c", 123), ("d", 99)] のようにする関数です。 では、この関数を使って、最初のリストと、乱数の整数のリストをくっつけましょう。 しかし、乱数の整数リストは
List Int`ではなく、`Random.Generator (List Int)`です。
こういう`Random.Generator`の中に入ってしまった値に関数を適用したいときは?
そう、`Random.map`を使います。(実際にコードを書くときは、指示書がどうこう・・・と考えるのではなく、`Random.Generator`の中に入ってしまった値に関数を適用する、とかそういう風に考えた方が手っ取り早いのではないかと思います。)
というわけで、以下のようにして、「最初のリストと乱数をくっつけたリスト」が完成しました。
型を見ればどんな値なのかよくわかりますね。
listWithRandomNumbers : Random.Generator (List ( a, Int ))
listWithRandomNumbers =
Random.map zipWithList randomNumbers
ではこの数を並べ替えます。 並べ替えには、List.sortBy
と、Tuple.second
を使います。
これがどんな関数なのかの解説は、この記事の主旨とは外れるため割愛します。
しかし、公式ドキュメントを見ればわかるのではないでしょうか。
List.sortBy
Tuple.second
今回も、Random.Generator
の中の値をならべかえるため、Random.map
を使っています。
大活躍ですね。
というわけで、並べ替えが以下のようにして完成しました。
sortedGenerator : Random.Generator (List ( a, Int ))
sortedGenerator =
Random.map (List.sortBy Tuple.second) listWithRandomNumbers
あとは、リストの各要素にTuple.first
を適用して、余計なソート用の整数を排除します。
リストの各要素に関数を適用するわけですから、もちろんList.map
を使います。
しかしリストは案の定Random.Generator
の中にありますので・・・
あとはわかりますね?
in
Random.map (List.map Tuple.first) sortedGenerator
というわけで完成です。 めでたしめでたし。 Random.map
、有能ですね。
4. 乱数を生成し、その数だけ偶数の乱数を生成してみる(=ランダムな長さの、ランダムな偶数のリストを生成する)
では次はこれです。 なんか簡単になりましたね。 リストの長さは5〜10、リストの要素である偶数は先ほど同様1〜100としましょう。
まずはリストの長さのRandom.Generator
と、偶数のRandom.Generator
を定義しましょう。
partFour : Random.Generator (List Int)
partFour =
let
listLengthGen : Random.Generator Int
listLengthGen =
Random.int 5 10
evenGen : Random.Generator Int
evenGen =
Random.int 1 50 |> Random.map ((*) 2)
そのまんまです。 evenGenは見た目こそ違いますが、partTwoの内容と全く同じです。
では、リストの生成です。 公式ドキュメントには、以下の関数があると書いてあります。
list : Int -> Random.Generator a -> Random.Generator (List a)
わかりますでしょうか? リストの長さと、要素を生成するRandom.Generator
とを渡せば、
その長さだけの乱数のリストが返ってきます。
では、リストの長さを受け取って、ランダムな偶数のリストを生成するようなRandom.Generator
を返す関数を定義してみます。
-- 先ほどのletの中です
listGen : Int -> Random.Generator (List Int)
listGen len =
Random.list len evenGen
そのままですね。
リストの長さを生成するRandom.Generatorは先ほど上で定義したので、さっきと同じようにRandom.map
して・・・
in
Random.map listGen listLengthGen
part4完!と言いたいところですが、このコードはコンパイルが通りません。
Something is off with the body of the `partFour` definition:
119| Random.map listGen listLengthGen
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This `map` call produces:
Random.Generator (Random.Generator (List Int))
But the type annotation on `partThree` says it should be:
Random.Generator (List Int)
型がRandom.Generator (Random.Generator (List Int))
になってしまっている、
と言っていますね。
Random.mapの型は
(a -> b) -> Random.Generator a -> Random.Generator b
でした。 第一引数に取る関数は、あくまで(Random.Generator
ではない)普通の値をとって、普通の値を返すような関数です。 ここに、Random.Generator
な値を返す関数を渡してしまうと、
Random.Generator
の中にRandom.Generator
が入ってしまう、という二重構造になってしまいます。
このような場合に使うのが、Random.andThen
です。
型は、
Random.andThen : (a -> Generator b) -> Generator a -> Generator b
です。
第2引数に、aな値のRandom.Generator
、第1引数に、aをとってbのRandom.Generator
を返すような関数をとります。
最終的には第1引数が返すRandom.Generator
の値が返ってきます。今回のケースにぴったりですね。
この説明だと引数は逆の方がしっくりきそうですが、この関数は|>
と同時に使うことを想定しています。
とりあえず使ってみましょう。
in
listLengthGen |> Random.andThen listGen
はい!今度こそpart4完です!
とこれで終わってはあっさりしすぎなので、ちょっとだけandThen
について補足説明させてください。
この関数の捉え方はいろいろあるのですが、ひとまず、上記の説明通り捉えておけば良いのではないかと思います。 強いて例えを使うなら、Random.Generator
と、値をとってRandom.Generator
を返す関数をチェーンすることができる、ぐらいでしょうか。
第一引数の関数では、第二引数のRandom.Generator
が生成する値が、あたかも本物の値のように扱えるのがミソです。
こういうとRandom.map
も同じじゃないかと思われそうですが、Random.andThen
は、(Random.constant
と組み合わせることで)、Random.map
より遥かに強力な汎用性を持ちます。
実はRandom.map
や(説明していませんがRandom.map2, 3, 4・・・
)は、すべてRandom.andThen
を使って再実装できます。
5.ズンドコキヨシ
さあラスボス、ズンドコキヨシです。 昔流行りましたね。
この問題の概要はこちらの記事に書いてあります。
これこそ乱数を発行して受け取って、Modelに格納、停止判定をしてからまた乱数を発行・・・
というループでもいいような気がしますが、まあ今回は練習のため、一発で生成してみましょう。
早速問題に取り掛かる・・・前に下準備です。
type Zundoko
= Zun
| Doko
showZundoko : Zundoko -> String
showZundoko zundoko =
case zundoko of
Zun ->
"ズン"
Doko ->
"ドコ"
zundokoLast =
List.reverse [ Zun, Zun, Zun, Zun, Doko ]
isZundokoLast : List Zundoko -> Bool
isZundokoLast list =
List.take 5 list == zundokoLast
Zundoko
型を定義し、文字列化する関数を定義しています。 あとはリストの末尾が「ズン・ズン・ズン・ズン・ドコ」となっているかどうかを判定する関数です。
(と言っていますが、思いっきり「リストの先頭が、ドコ・ズン・ズン・ズン・ズン」になっているか判定する関数になっていますね。 今回は「ズンかドコ」の乱数を生成するたび、リストの末尾ではなく、先頭に追加していくような実装になっています。したがって、判定部分も通常と逆になっています。)
例によって例のごとく、とりあえずコードを貼り付けます。
partFive : Random.Generator String
partFive =
let
zundokoGen : Random.Generator Zundoko
zundokoGen =
Random.uniform Zun [ Doko ]
loop : Random.Generator (List Zundoko) -> Random.Generator (List Zundoko)
loop acc =
acc
|> Random.andThen
(\list ->
if isZundokoLast list then
Random.constant list
else
Random.map (\newElem -> newElem :: list) zundokoGen
|> loop
)
finalize : List Zundoko -> String
finalize zundokoList =
zundokoList
|> List.map showZundoko
|> (\x -> "キヨシ!" :: x)
|> List.reverse
|> String.join "・"
in
loop (Random.constant []) |> Random.map finalize
まずはzundokoGenです。Random.uniform
には変な引数を渡していますが、型を見ればわかる通り、Zun
かDoko
をランダムに生成するRandom.Generator
です。
ここで使っているRandom.uniform
のドキュメントへのリンクを念のため貼っておきます。
次、今回の実質的なメイン関数であるloop
です。 else部分を見てわかる通り、再帰関数になっていますね。 この関数は、これまでに生成されたズンドコリストをaccという引数で受け取ります。
そして、先ほどのRandom.andThen
を使っていますね。
この関数は、本来accとしてRandom.Generator
の中に入っている、これまで生成されたズンドコリストを、第一引数の関数の中で(今回はラムダ式ですね)あたかも本物の値のように扱えるんでしたね。
つまり、ラムダ式の引数であるlist
は、これまでに生成されたズンドコリストです。
ではこのラムダ式の中身を見ていきましょう。
まずif式で、終了判定をしています。頭が「ドコ・ズン・ズン・ズン・ズン」になっているかどうかを見ているんですね。
終了判定がTrueの場合、このままlist
を返せばいい・・・ わけではありません。 Random.andThen
の引数は、(普通の)値を受け取って、Random.Generator
な値を返す関数である必要があります。
そこで、Random.constant
の出番です。
この関数の型は、
constant : a -> Random.Generator a
です。
引数の値を、そのままRandom.Generator
の中に入れて返します。 指示書の例えを使うならば、
常に引数の値を返すような乱数生成指示書を作る
と言ったところでしょうか。
Random.constant 1
で返ってくるRandom.Generator
は常に1を生成しますし、Random.constant "hoge"
で返ってくるRandom.Generator
は常に"hoge"を生成します。
意味なさそうな関数ですが、今回の用途にはうってつけです。
ではelse部を見ていきます。
Random.map (\newElem -> newElem :: list) zundokoGen
|> loop
またしてもRandom.map
を使い、リストの先頭に新たなズン・ドコ乱数を追加、新リストを生成した上で、
loopに渡しています。 再帰していますね。
finalize関数は、Zun
やDoko
をString
にしたり、「キヨシ!」を追加したり、
リストをひっくり返したり、リストを「・」でつないで一つの文字列にしたりしています。
色々やっていますがあまり重要なポイントではないでしょう。
では最後、inの部分です。
in
loop (Random.constant []) |> Random.map finalize
loop
を実行するためには、リストの初期値を与える必要があります。
もちろん初期値なので空リストが適切なのですが、与える型は、Random.Generator (List Zundoko)
でないといけません。
というわけで、先ほどと同じようにRandom.constant
を使い、空リストのRandom.Generator
を作り、引数にして渡します。
loop
の結果をfinalize
関数に渡したいのですが、loop
の結果はRandom.Generator
なのでmap(略)
ということで、ズンドコキヨシもできました。 Random.map
、Random.andThen
、Random.constant
があれば、割と複雑そうな処理でも書けてしまうのがわかりましたでしょうか?
次の記事では、コンパイルによって出力されたJSを見てRandom.Generator
の正体を見ていきます。
Random.map
などが実際には何をしているのかも、併せて見ていきます。
記事が書けたらリンクを貼ります。
おまけ
Random.map2
本文では紹介できなかったRandom.map2
について書いておきます。
Random.map
は、Random.Generator
の中の値(Random.Generatorによって生成されるであろう値)に、関数を適用し、Random.Generator
を返すというものでした。
当然、その関数は1引数の関数であるはずです。 では2引数の関数では?
以下のような関数があると仮定して考えて見ましょう
add : Int -> Int -> Int
add a b = a + b
もし片方がRandom.Generator
、もう片方が普通の値だった場合は、以下のようにすればいいはずです。
本文中でも何回かこのやり方を使っています。
addOne : Int -> Random.Generator Int -> Random.Generator Int
addOne normalValue genValue = Random.map (add normalValue) genValue
add
にnormalValue
を部分適用して、Int -> Int
な関数を作ってから、Random.map
を使っています。 今回は部分適用だけでスッキリ書けましたが、場合によってはラムダ式を使うこともあるでしょう。
では両方の値が、Random.Generator
だった場合は?
Random.map2
を使います。 なんとなく名前で察しがつくような気がしますが、見ていきましょう。
型は以下の通りです。
Random.map2 : (a -> b -> c) -> Random.Generator a -> Random.Generator b -> Random.Generator c
2引数の関数と、2つのRandom.Generator
を受け取って、関数適用、値をRandom.Generator
として返す関数ですね。
使ってみましょう。
addTwo : Random.Generator Int -> Random.Generator Int -> Random.Generator Int
addTwo genOne genTwo = Random.map2 add genOne genTwo
まあなんというか、とても普通ですね。
3引数の関数のために、Random.map3
が、4引数の関数ためにRandom.map4
が、5引数の関数のために、Random.map5
があります。 それ以上引数をとる関数の場合、elm-community/random-extraのRandom.Extra.andMapなんかが使えると思いますが、ここでの説明は割愛します。
そもそもそんなに引数をたくさんとる関数作ります?
Random.andThen
とRandom.constant
で色々再実装する
第4章にて、
実は
Random.map
や(説明していませんがRandom.map2, 3, 4・・・
)は、すべてRandom.andThen
を使って再実装できます。
と書きました。 ここで再実装をやってみましょう。
まずはRandom.map
から
myMap : (a -> b) -> Random.Generator a -> Random.Generator b
myMap f gen =
gen |> Random.andThen (\val -> f val |> Random.constant)
ほら簡単にできました。
Random.andThen
の引数にする関数の中では、第2引数のRandom.Generator
の中の値をあたかも本物の値のように扱えるんでしたね。 ならば関数適用だって普通にできます。 このラムダ式は、Random.Generator
を返さないといけないので、Random.constant
を使っておしまいです。
次にRandom.map2
をやってみます。
myMap2 : (a -> b -> c) -> Random.Generator a -> Random.Generator b -> Random.Generator c
myMap2 f gen1 gen2 =
gen1
|> Random.andThen
(\val1 ->
gen2
|> Random.andThen
(\val2 -> f val1 val2 |> Random.constant)
)
ほらできた! ・・・とはいえちょっとややこしいですね。
最初のRandom.andThen
の引数のラムダ式の中で、さらにRandom.andThen
を使っています。
このようにRandom.andThen
をネストさえれば、map3だろうがなんだろうが実装できそうです。
ややこしくて、本当にこれであっているのか?と思ってしまいますが、型があっていてコンパイルが通るんだからきっと合ってるんでしょう。
理解が難しいかもしれませんが、ただただややこしいだけですし、無理して理解する必要はないと思います。 おまけですし・・・
説明が雑な気がしますが、まあおまけですし・・・
おしまい。