Edited at

Elmの乱数を使いこなす


はじめに

この記事で乱数を使ったのですが、Elmの独特な乱数の取り扱いについて全く解説できずに終わってしまいました。

この記事で解説という名のリベンジをしてみようと思います。

最低限、Model、Update、View、CmdといったThe Elm Architectureの要素を理解している方が対象です。

Random.mapRandom.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.mapRandom.andThenRandom.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にあるので、実際に使うときはこちらを使いましょう。

しかし学習のためなら、車輪の再発明だって許されるはずです。

シャッフル処理はいろいろ考えられるのですが、今回は以下の手順でやってみようと思います。

1. ["a", "b", "c", "d"]というリストがあるとする

2. リストと同じ長さの乱数列を生成する

3. 最初のリストと2.の乱数列をList.map2でくっつけ、以下のようなリストにする

[("a", 32), ("b", 100), ("c", 123), ("d", 99)] (数は例えです)

4. List.sortBy で、リストの各要素のタプルの2番目の要素でリストをソートする

5. 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も同様に最大の値を指します。 つまり、できる限り大きな振れ幅で整数の乱数値を生成します。

第一引数は普通にリストの長さですね。

では次にzipWithListlistWithRandomNumbersです。

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には変な引数を渡していますが、型を見ればわかる通り、ZunDokoをランダムに生成する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関数は、ZunDokoStringにしたり、「キヨシ!」を追加したり、

リストをひっくり返したり、リストを「・」でつないで一つの文字列にしたりしています。

色々やっていますがあまり重要なポイントではないでしょう。

では最後、inの部分です。

    in

loop (Random.constant []) |> Random.map finalize

loopを実行するためには、リストの初期値を与える必要があります。

もちろん初期値なので空リストが適切なのですが、与える型は、Random.Generator (List Zundoko)でないといけません。

というわけで、先ほどと同じようにRandom.constantを使い、空リストのRandom.Generatorを作り、引数にして渡します。

loopの結果をfinalize関数に渡したいのですが、loopの結果はRandom.Generatorなのでmap(略)

ということで、ズンドコキヨシもできました。 Random.mapRandom.andThenRandom.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

addnormalValueを部分適用して、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.andThenRandom.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だろうがなんだろうが実装できそうです。

ややこしくて、本当にこれであっているのか?と思ってしまいますが、型があっていてコンパイルが通るんだからきっと合ってるんでしょう。

理解が難しいかもしれませんが、ただただややこしいだけですし、無理して理解する必要はないと思います。 おまけですし・・・

説明が雑な気がしますが、まあおまけですし・・・

おしまい。