ビンゴカード作成問題をお題に Elm で TDD の練習1.5：マスの値を生成する(改)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">おれの使命はこの世からList Listのビンゴカードを廃滅することだ</p>&mdash; 猫背ステッチ（みやも） (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/1145288724617371650?ref_src=twsrc%5Etfw">2019年6月30日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

やられる前にやらないと… (殺伐) 。

みやもさん (@miyamo_madoka) の案を基に [ビンゴカード作成問題をお題に Elm で TDD の練習1：マスの値を生成する](https://qiita.com/ossan_pg/items/2458219d2994b1631f63) の設計と実装をもりっと変更します。


## マスの値生成に関する仕様 (再掲)

- B、I、N、G、O の 5列が存在する。
- 各列には 5個のマスが存在する。
- 各列の各マスに次の範囲の値がランダムに設定される。
  - B列：1-15
  - I列：16-30
  - N列：31-45
  - G列：46-60
  - O列：61-75 
- 同じ値のマスは複数存在しない。
- 中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス 。


## ビンゴカードの型

ビンゴカードは 5個のマスからなる列が 5個あるので、~~ひとまず `List` の `List` にしましょう~~

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">ほうほう<br><br>わたしこうかな<br>type BColumn = BColumn Int Int Int Int Int</p>&mdash; 猫背ステッチ（みやも） (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/1132814738172153858?ref_src=twsrc%5Etfw">2019年5月27日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">IColumとNColumとGColumnとOColumnも作るよ<br>NColumnは真ん中開いてるから数字が4個だね<br>それぞれに乱数生成器作って、ビンゴ判定がんばって作ってで大体終わるのかな</p>&mdash; 猫背ステッチ（みやも） (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/1132815904406851584?ref_src=twsrc%5Etfw">2019年5月27日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

個別にカスタム型を用意しましょう：

```elm:ビンゴカードの型
type BColumn
    = BColumn Int Int Int Int Int


type IColumn
    = IColumn Int Int Int Int Int


type NColumn
    = NColumn Int Int Int Int


type GColumn
    = GColumn Int Int Int Int Int


type OColumn
    = OColumn Int Int Int Int Int


type BingoCard
    = BingoCard BColumn IColumn NColumn GColumn OColumn
```

今回 FREEマスについては表示する側で追加してもらう想定で行きます。


## テストコードとプロダクトコード

仕様を基に次のことを検証します。

- ~~ビンゴカードは 5x5 のマスから構成される。~~
  - マスの個数は各型で固定されているので検証不要です。
- B列には 1 以上 15 以下の値が設定される。
- I列には 16 以上 30 以下の値が設定される。
- N列には 31 以上 45 以下の値が設定される。
- G列には 46 以上 60 以下の値が設定される。
- O列には 61 以上 75 以下の値が設定される。
- 同じ値のマスは 1個のみ存在する。
- ~~中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス。~~
  - `BingoCard` には FREEマスを含めないので検証不要です。  


### ○列には △ 以上 ▽ 以下の値が設定される

色々大変なので経緯 (テストコードのテストとか) については省略です。
まずテストコードをどーん (解説は後述)：

```elm:tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import Main exposing (..)
import Random exposing (Generator)
import Test exposing (..)


columnFuzzer : Generator column -> Fuzzer column
columnFuzzer columnGenerator =
    let
        intToColumn : Int -> column
        intToColumn =
            Random.initialSeed >> Random.step columnGenerator >> Tuple.first
    in
    Fuzz.map intToColumn Fuzz.int


bColumnFuzzer : Fuzzer BColumn
bColumnFuzzer =
    columnFuzzer bColumnGenerator


iColumnFuzzer : Fuzzer IColumn
iColumnFuzzer =
    columnFuzzer iColumnGenerator


nColumnFuzzer : Fuzzer NColumn
nColumnFuzzer =
    columnFuzzer nColumnGenerator


gColumnFuzzer : Fuzzer GColumn
gColumnFuzzer =
    columnFuzzer gColumnGenerator


oColumnFuzzer : Fuzzer OColumn
oColumnFuzzer =
    columnFuzzer oColumnGenerator


verifyColumn : Int -> Int -> List Int -> Expectation
verifyColumn min max column =
    column
        |> List.all (\n -> min <= n && n <= max)
        |> Expect.true "範囲外の値があります。"


suite : Test
suite =
    describe "Main module"
        [ describe "bColumnGenerator"
            [ fuzz bColumnFuzzer "B列には 1 以上 15 以下の値が設定される" <|
                \(BColumn n1 n2 n3 n4 n5) -> verifyColumn 1 15 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "iColumnGenerator"
            [ fuzz iColumnFuzzer "I列には 16 以上 30 以下の値が設定される" <|
                \(IColumn n1 n2 n3 n4 n5) -> verifyColumn 16 30 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "nColumnGenerator"
            [ fuzz nColumnFuzzer "N列には 31 以上 45 以下の値が設定される" <|
                \(NColumn n1 n2 n3 n4) -> verifyColumn 31 45 [ n1, n2, n3, n4 ]
            ]
        , describe "gColumnGenerator"
            [ fuzz gColumnFuzzer "G列には 46 以上 60 以下の値が設定される" <|
                \(GColumn n1 n2 n3 n4 n5) -> verifyColumn 46 60 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "oColumnGenerator"
            [ fuzz oColumnFuzzer "O列には 61 以上 75 以下の値が設定される" <|
                \(OColumn n1 n2 n3 n4 n5) -> verifyColumn 61 75 [ n1, n2, n3, n4, n5 ]
            ]
        ]
```

テストコードでは変更前と同様に `Fuzzer` を用意しています。
今回は `BingoCard` ではなく各列の型 (`BColumn`、`IColumn`、…) の値をテストごとに取得するようにしたため、`Fuzzer` も列ごとに用意します。
各列の `Fuzzer` で異なるのは使用する `Generator` (`Main.elm` で定義) だけなので、`Generator` を受け取る共通関数 `columnFuzzer` を作成しておきました。

あとは `Fuzzer` を `fuzz` へ渡し、取得した列のマスの値をパターンマッチで取り出します。
取り出したマスの値はリストにまとめ (`[ n1, n2, ... ]`) 、`下限値`、`上限値` とともに `verifyColumn` へ渡して検証します。

マスの値の範囲についてのテストコードは以上です。

プロダクトコードをどーん：

```elm:src/Main.elm
module Main exposing
    ( BColumn(..), bColumnGenerator
    , IColumn(..), iColumnGenerator
    , NColumn(..), nColumnGenerator
    , GColumn(..), gColumnGenerator
    , OColumn(..), oColumnGenerator
    )

{-|

@docs BColumn, bColumnGenerator
@docs IColumn, iColumnGenerator
@docs NColumn, nColumnGenerator
@docs GColumn, gColumnGenerator
@docs OColumn, oColumnGenerator

-}

import Random exposing (Generator)
import Random.List as RandomList


type BColumn
    = BColumn Int Int Int Int Int


type IColumn
    = IColumn Int Int Int Int Int


type NColumn
    = NColumn Int Int Int Int


type GColumn
    = GColumn Int Int Int Int Int


type OColumn
    = OColumn Int Int Int Int Int


type BingoCard
    = BingoCard BColumn IColumn NColumn GColumn OColumn


columnGenerator : Int -> Int -> (Int -> Int -> Int -> Int -> Int -> column) -> Generator column
columnGenerator min max toColumn =
    let
        listToColumn : List Int -> column
        listToColumn ints =
            case ints of
                n1 :: n2 :: n3 :: n4 :: n5 :: ns ->
                    toColumn n1 n2 n3 n4 n5

                _ ->
                    -- こちらが実行される場合は呼び出し元にバグがある
                    toColumn 0 0 0 0 0
    in
    List.range min max
        |> RandomList.shuffle
        |> Random.map listToColumn


bColumnGenerator : Generator BColumn
bColumnGenerator =
    columnGenerator 1 15 BColumn


iColumnGenerator : Generator IColumn
iColumnGenerator =
    columnGenerator 16 30 IColumn


nColumnGenerator : Generator NColumn
nColumnGenerator =
    let
        listToColumn : List Int -> NColumn
        listToColumn ints =
            case ints of
                -- N列の数字マスは 4個
                n1 :: n2 :: n3 :: n4 :: ns ->
                    NColumn n1 n2 n3 n4

                _ ->
                    -- こちらが実行される場合は呼び出し元にバグがある
                    NColumn 0 0 0 0
    in
    List.range 31 45
        |> RandomList.shuffle
        |> Random.map listToColumn


gColumnGenerator : Generator GColumn
gColumnGenerator =
    columnGenerator 46 60 GColumn


oColumnGenerator : Generator OColumn
oColumnGenerator =
    columnGenerator 61 75 OColumn
```

`module` とか `@docs` とかの話は @miyamo_madoka さんの [[Elm]elm-formatでexposeする関数を整理する](https://qiita.com/miyamo_madoka/items/e9e26a1746c767415472) を見ましょう。

型の定義は先述のまんまです。

各列を作成するための `Generator` を定義します。
それぞれの列で `下限値`、`上限値`、`列の型` 以外は同じなので、処理を `columnGenerator` にまとめています。
N列だけは数字マスの個数が他の列と違うため、`nColumnGenerator` は独自に定義しています。


### 同じ値のマスは 1個のみ存在する

テストコード (関連箇所のみ) どーん：

```elm:tests/Tests.elm
import List.Extra as ListEx

bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


suite : Test
suite =
    describe "Main module"
        :
        , describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "同じ値のマスは 1個のみ存在する" <|
                \(BingoCard (BColumn b1 b2 b3 b4 b5) (IColumn i1 i2 i3 i4 i5) (NColumn n1 n2 n3 n4) (GColumn g1 g2 g3 g4 g5) (OColumn o1 o2 o3 o4 o5)) ->
                    [ b1, b2, b3, b4, b5 ]
                        ++ [ i1, i2, i3, i4, i5 ]
                        ++ [ n1, n2, n3, n4 ]
                        ++ [ g1, g2, g3, g4, g5 ]
                        ++ [ o1, o2, o3, o4, o5 ]
                        |> ListEx.allDifferent
                        |> Expect.true "同じ値が複数存在します。"
            ]
        ]

```

`bingoCardFuzzer` は変更前と同じです。
この `Fuzzer` から取得した `BingoCard` 内の各値をパターンマッチで取り出してリストにまとめ、`List.Extra.allDifferent` で全要素が異なるかを判定しています。
大まかな流れは特に変わりません。

プロダクトコードどーん：

```elm:src/Main.elm
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.map5 BingoCard
        bColumnGenerator
        iColumnGenerator
        nColumnGenerator
        gColumnGenerator
        oColumnGenerator
```

各 `Generator` で作った値から `BingoCard` を作成すればいいので、`Random.map5` を呼んで終わりです。
お手軽簡単。


## 完成

<details><summary>最終的なコード</summary><div>

```elm:tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random exposing (Generator)
import Test exposing (..)


columnFuzzer : Generator column -> Fuzzer column
columnFuzzer columnGenerator =
    let
        intToColumn : Int -> column
        intToColumn =
            Random.initialSeed >> Random.step columnGenerator >> Tuple.first
    in
    Fuzz.map intToColumn Fuzz.int


bColumnFuzzer : Fuzzer BColumn
bColumnFuzzer =
    columnFuzzer bColumnGenerator


iColumnFuzzer : Fuzzer IColumn
iColumnFuzzer =
    columnFuzzer iColumnGenerator


nColumnFuzzer : Fuzzer NColumn
nColumnFuzzer =
    columnFuzzer nColumnGenerator


gColumnFuzzer : Fuzzer GColumn
gColumnFuzzer =
    columnFuzzer gColumnGenerator


oColumnFuzzer : Fuzzer OColumn
oColumnFuzzer =
    columnFuzzer oColumnGenerator


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : Int -> Int -> List Int -> Expectation
verifyColumn min max column =
    column
        |> List.all (\n -> min <= n && n <= max)
        |> Expect.true "範囲外の値があります。"


suite : Test
suite =
    describe "Main module"
        [ describe "bColumnGenerator"
            [ fuzz bColumnFuzzer "B列には 1 以上 15 以下の値が設定される" <|
                \(BColumn n1 n2 n3 n4 n5) -> verifyColumn 1 15 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "iColumnGenerator"
            [ fuzz iColumnFuzzer "I列には 16 以上 30 以下の値が設定される" <|
                \(IColumn n1 n2 n3 n4 n5) -> verifyColumn 16 30 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "nColumnGenerator"
            [ fuzz nColumnFuzzer "N列には 31 以上 45 以下の値が設定される" <|
                \(NColumn n1 n2 n3 n4) -> verifyColumn 31 45 [ n1, n2, n3, n4 ]
            ]
        , describe "gColumnGenerator"
            [ fuzz gColumnFuzzer "G列には 46 以上 60 以下の値が設定される" <|
                \(GColumn n1 n2 n3 n4 n5) -> verifyColumn 46 60 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "oColumnGenerator"
            [ fuzz oColumnFuzzer "O列には 61 以上 75 以下の値が設定される" <|
                \(OColumn n1 n2 n3 n4 n5) -> verifyColumn 61 75 [ n1, n2, n3, n4, n5 ]
            ]
        , describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "同じ値のマスは 1個のみ存在する" <|
                \(BingoCard (BColumn b1 b2 b3 b4 b5) (IColumn i1 i2 i3 i4 i5) (NColumn n1 n2 n3 n4) (GColumn g1 g2 g3 g4 g5) (OColumn o1 o2 o3 o4 o5)) ->
                    [ b1, b2, b3, b4, b5 ]
                        ++ [ i1, i2, i3, i4, i5 ]
                        ++ [ n1, n2, n3, n4 ]
                        ++ [ g1, g2, g3, g4, g5 ]
                        ++ [ o1, o2, o3, o4, o5 ]
                        |> ListEx.allDifferent
                        |> Expect.true "同じ値が複数存在します。"
            ]
        ]
```

```elm:src/Main.elm
module Main exposing
    ( BColumn(..), bColumnGenerator
    , IColumn(..), iColumnGenerator
    , NColumn(..), nColumnGenerator
    , GColumn(..), gColumnGenerator
    , OColumn(..), oColumnGenerator
    , BingoCard(..), bingoCardGenerator
    )

{-|

@docs BColumn, bColumnGenerator
@docs IColumn, iColumnGenerator
@docs NColumn, nColumnGenerator
@docs GColumn, gColumnGenerator
@docs OColumn, oColumnGenerator
@docs BingoCard, bingoCardGenerator

-}

import Random exposing (Generator)
import Random.List as RandomList


type BColumn
    = BColumn Int Int Int Int Int


type IColumn
    = IColumn Int Int Int Int Int


type NColumn
    = NColumn Int Int Int Int


type GColumn
    = GColumn Int Int Int Int Int


type OColumn
    = OColumn Int Int Int Int Int


type BingoCard
    = BingoCard BColumn IColumn NColumn GColumn OColumn


columnGenerator : Int -> Int -> (Int -> Int -> Int -> Int -> Int -> column) -> Generator column
columnGenerator min max toColumn =
    let
        listToColumn : List Int -> column
        listToColumn ints =
            case ints of
                n1 :: n2 :: n3 :: n4 :: n5 :: ns ->
                    toColumn n1 n2 n3 n4 n5

                _ ->
                    -- こちらが実行される場合は呼び出し元にバグがある
                    toColumn 0 0 0 0 0
    in
    List.range min max
        |> RandomList.shuffle
        |> Random.map listToColumn


bColumnGenerator : Generator BColumn
bColumnGenerator =
    columnGenerator 1 15 BColumn


iColumnGenerator : Generator IColumn
iColumnGenerator =
    columnGenerator 16 30 IColumn


nColumnGenerator : Generator NColumn
nColumnGenerator =
    let
        listToColumn : List Int -> NColumn
        listToColumn ints =
            case ints of
                -- N列の数字マスは 4個
                n1 :: n2 :: n3 :: n4 :: ns ->
                    NColumn n1 n2 n3 n4

                _ ->
                    -- こちらが実行される場合は呼び出し元にバグがある
                    NColumn 0 0 0 0
    in
    List.range 31 45
        |> RandomList.shuffle
        |> Random.map listToColumn


gColumnGenerator : Generator GColumn
gColumnGenerator =
    columnGenerator 46 60 GColumn


oColumnGenerator : Generator OColumn
oColumnGenerator =
    columnGenerator 61 75 OColumn


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.map5 BingoCard
        bColumnGenerator
        iColumnGenerator
        nColumnGenerator
        gColumnGenerator
        oColumnGenerator
```
</div></details>

<details><summary>テスト結果</summary><div>

```shell:テスト結果
$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 6 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 219572844618860 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1201 ms
Passed:   6
Failed:   0
```
</div></details>

テストも全て OK でした。完成です。


## 変更前との比較

`List` を使用した型定義と比較して、マスの数などが型で担保されるため検証事項が少なくなりました。
`view` でやることが増えてますが、全体としてはシンプルになったんじゃないかと思います。多分 (そもそも全体がまだできてない) 。

データ構造、大事ですね。


**おしまり**