Elmでビンゴしようよ！！！！

こちらの[PureScriptでビンゴしようよ！！！！](https://qiita.com/KNaito/items/682b1909777ff90910c1)に触発され、みんな大好きElmで実装しようと思いました。

せっかくなので~~内容整理するのがめんどくさい~~実際の思考の順番で実装をどんどん追記していくような形式で書かせていただきます。

## 必要な機能をかんがえる

機能としては

- ビンゴカードを表すデータ型
    - 行ごとや列ごとに取り出せると嬉しい
- 乱数ジェネレータを使ってビンゴカードを作る機能
    - Bは1~15, Iは16~30, ...
    - Nの真ん中はFree
- そのデータ型から目的の文字列にフォーマットする関数

## 文字の表示までをやる

今回は乱数のためにCmdも利用するので[Browser.element](https://package.elm-lang.org/packages/elm/browser/latest/Browser#element)を使ってアプリを作ります。

とりあえずの雛形として以下のようなものを用意します。

```elm
module Main exposing (main)

import Browser
import Html exposing (Html, div, h1, text)


type alias Model =
    ()


init : () -> ( Model, Cmd Msg )
init flags =
    ( (), Cmd.none )


type alias Msg =
    Never


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    ( model, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none


view : Model -> Html Msg
view model =
    div []
        [ h1 []
            [ text "Elm Bingo" ]
        , div
            []
            [ text "これから実装するよ！" ]
        ]


main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }

```

実行結果
<img width="612" alt="スクリーンショット 2019-05-28 21.06.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158586/893a7697-0f13-50ea-bafa-806e5c0ff2dc.png">


## モデルの定義をする

とりあえず思いついたのは、Arrayの二次元配列です。

```elm
import Array exposing (Array)

type BingoCard = BingoCard (Array (Array Int))
```

表示を考えると行ずつに取り出したいので横方向に伸びる行を縦に積んだようなデータ構造を想定してます。ですが、上の定義だと縦の列を横にならべてるというようにも解釈できてしまいます。ここはカスタム型を使ってデータ構造に名前をつけちゃいます。

```elm
type Row
    = Row (Array Int)


type BingoCard
    = BingoCard (Array Row)
```

また、ビンゴカードの各マスは真ん中だけFreeというような構造です。真ん中だけFreeというデータ構造も表せないわけではないですが、かなり面倒なので「ビンゴのマスはFreeか数字のどちらかである」というような解釈をすることにします。

```elm
type Cell -- `Free` か `Cell Int` のどちらか
    = Free
    | Cell Int


type Row
    = Row (Array Cell)
```

これで一通りモデルの定義ができました。

ポイントは、小さなパーツを元におっきなパーツを作るって考え方です。行はセルの集まり、ビンゴカードは行の集まり、といった感じです。

## viewを書きたいのでとりあえず仮のデータを用意する

仮データとしてビンゴカードを適当に定義しちゃいます。`<|`こっち向きのパイプは`コンストラクタ <| コンストラクタに渡したい複雑な式`のような使い方だと読みやすくなります。


```elm
bingo : BingoCard
bingo =
    BingoCard <|
        Array.fromList
            [ Row <| Array.fromList [ Cell 1, Cell 16, Cell 31, Cell 46, Cell 61 ]
            , Row <| Array.fromList [ Cell 2, Cell 17, Cell 32, Cell 47, Cell 62 ]
            , Row <| Array.fromList [ Cell 3, Cell 18, Free, Cell 48, Cell 63 ]
            , Row <| Array.fromList [ Cell 4, Cell 19, Cell 34, Cell 49, Cell 64 ]
            , Row <| Array.fromList [ Cell 5, Cell 20, Cell 35, Cell 50, Cell 65 ]
            ]
```

## viewを書く

ビンゴカードを表示するためのの共通部分の処理をざっくり考えてみると

- Stringをスペース詰めながら右詰めにするやつ
- 両端にスペース入れる
- 文字列のリストの間に"|"を差し込む処理
- 文字列のリストの間に"\n"を差し込む処理

この関数たちをいい感じに組み合わせればフォーマット関数は出来上がりそうです。ポイントは頭がこんがらがらないようにちゃんと小さなパーツから作ることです。

```elm
showBingoCard : BingoCard -> String
showBingoCard (BingoCard rows) =
    let
        -- セルを文字列にする
        showCell : Cell -> String
        showCell cell =
            case cell of
                Free ->
                    ""

                Cell n ->
                    String.fromInt n

        -- 行を文字列のリストに変換する
        rowToStrList : Row -> List String
        rowToStrList (Row cells) =
            cells
                |> Array.toList
                |> List.map showCell

        -- 表もヘッダも同じフォーマッタを使いたいので共通化
        formatRow : List String -> String
        formatRow list =
            list
                |> List.map (String.pad 4 ' ')
                |> String.join "|"

        -- ヘッダの定義
        header : List String
        header =
            [ "B", "I", "N", "G", "O" ]
    in
    rows
        |> Array.toList
        |> List.map rowToStrList
        |> (::) header
        |> List.map formatRow
        |> String.join "\n"
```

複雑な処理をしてるはずなのになんだかスッキリしてて気持ちがいいですね！


ここまできたらあとは楽で、先ほど作った仮のビンゴカードにフォーマッタを適用してそれをtextとして表示するだけです。僕の環境だとpreを使っても表示が崩れてたので等幅フォントの指定をしておきます。

```elm
import Html.Attributes exposing (style)


view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text "Elm Bingo" ]
        , pre [ style "font-family" "Courier" ] [ text (showBingoCard bingo) ]
        ]
```

さて、実行結果は...
<img width="612" alt="スクリーンショット 2019-05-28 21.05.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158586/dbc8261e-8be8-84bd-d52c-89db3edb1222.png">


めちゃくちゃいい感じに表示できました！ちょろい！！

ここまでくればあとはビンゴカードの生成のロジックを書けばいいだけです。

## 乱数を使い、ビンゴカードを作成

ビンゴのカードの番号の詳細をもう一度把握しておきましょう

- B列は1~15, I列は16~30, ... といったように15刻みの範囲で数字が選ばれる
- 番号は重複しない
- 真ん中はFree

これを実現するためには

- aからbまでの整数のリストをシャッフルしたうち先頭5つを取り出す関数
- 列を横に並べたものを元に、行を縦に並べたものを作る関数
- この二次元配列っぽいデータをBingoCardに変換する関数

これらがあれば十分そうです。[elm-community](https://github.com/elm-community)の~~Extraってライブラリに便利関数がたくさんあったのでこれらを使って実装していきます。

```elm
import List.Extra
import Maybe.Extra
import Random exposing (Generator)
import Random.Extra
import Random.List

genBingoCard : Generator BingoCard
genBingoCard =
    let
        genColumn : Int -> Int -> Generator (List Cell)
        genColumn min_ max_ =
            List.range min_ max_
                |> List.map Cell
                |> Random.List.shuffle
                |> Random.map (List.take 5)

        mapToBingoCard : List (List Cell) -> BingoCard
        mapToBingoCard =
            List.map (Array.fromList >> Row) >> Array.fromList >> BingoCard
    in
    Random.Extra.sequence
        [ genColumn 1 15
        , genColumn 16 30
        , genColumn 31 45 |> Random.map (List.Extra.setAt 2 Free)
        , genColumn 46 60
        , genColumn 61 75
        ]
        |> Random.map List.Extra.transpose
        |> Random.map mapToBingoCard
```

なんて読みやすいんでしょうか。さすがElm、さすが僕。

難しい部分は全部終わりました。最後に、このビンゴカードジェネレータをどうにかしてviewに表示すれば良いでしょう。

## 今まで作ってきたものを組み合わせる

[Random.generate](https://package.elm-lang.org/packages/elm-lang/core/latest/Random#generate)の型は`(a -> msg) -> Generator a -> Cmd msg`という方になってます。aがビンゴ型、msgがアプリのメッセージの型になるように実装します。

ビンゴを作成したことを表すメッセージを作成して...

```elm
type Msg
    = GenerateBingo BingoCard
```

init時にビンゴジェネレータを走らせるように書きます。ビンゴカードはジェネレート前の状態が存在するため、`Maybe BingoCard`のようにしておきます。

```elm
type alias Model =
    Maybe BingoCard


init : () -> ( Model, Cmd Msg )
init _ =
    ( Nothong -- 最初はビンゴカードがない状態
    , Random.generate GenerateBingo genBingoCard
    )
```

作成したビンゴカードはupdateから受け取れるので、

```elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GenerateBingo card ->
            ( Just card, Cmd.none ) -- 生成されたcardでモデルを更新！
```

そして最後、モデルに入っているBingoCardをテキストで表示します

```elm
view : Model -> Html Msg
view model =
    let
        formattedBingoCard : String
        formattedBingoCard =
            model
                |> Maybe.map showBingoCard
                |> Maybe.withDefault "creating..."
    in
    div []
        [ h1 [] [ text "Elm Bingo" ]
        , pre [ style "font-family" "Courier" ] [ text formattedBingoCard ]
        ]
```

ようやく実行です。ここまではひたすら型パズルを解いていっただけです。特に関数の実装をよく読んだりなどはしてないので本当に仕様通りに行くか心配ですが...

<img width="612" alt="スクリーンショット 2019-05-28 21.03.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158586/dd886d1e-9810-0369-ec0c-aca29f582d8f.png">

どうやらうまく実装できたみたいです、ﾔｯﾀﾈ🎉

全体のソースは以下のようになりました

```elm:Main.elm
module Main exposing (main)

import Array exposing (Array)
import Browser
import Html exposing (Html, div, h1, pre, text)
import Html.Attributes exposing (style)
import List.Extra
import Random exposing (Generator)
import Random.Extra
import Random.List


type Cell
    = Free
    | Cell Int


type Row
    = Row (Array Cell)


type BingoCard
    = BingoCard (Array Row)


type alias Model =
    Maybe BingoCard


showBingoCard : BingoCard -> String
showBingoCard (BingoCard rows) =
    let
        -- セルを文字列にする
        showCell : Cell -> String
        showCell cell =
            case cell of
                Free ->
                    ""

                Cell n ->
                    String.fromInt n

        -- 行を文字列のリストに変換する
        rowToStrList : Row -> List String
        rowToStrList (Row cells) =
            cells
                |> Array.toList
                |> List.map showCell

        -- 表もヘッダも同じフォーマッタを使いたいので共通化
        formatRow : List String -> String
        formatRow list =
            list
                |> List.map (String.pad 4 ' ')
                |> String.join "|"

        -- ヘッダの定義
        header : List String
        header =
            [ "B", "I", "N", "G", "O" ]
    in
    rows
        |> Array.toList
        |> List.map rowToStrList
        |> (::) header
        |> List.map formatRow
        |> String.join "\n"


genBingoCard : Generator BingoCard
genBingoCard =
    let
        genColumn : Int -> Int -> Generator (List Cell)
        genColumn min_ max_ =
            List.range min_ max_
                |> List.map Cell
                |> Random.List.shuffle
                |> Random.map (List.take 5)

        mapToBingoCard : List (List Cell) -> BingoCard
        mapToBingoCard =
            List.map (Array.fromList >> Row) >> Array.fromList >> BingoCard
    in
    Random.Extra.sequence
        [ genColumn 1 15
        , genColumn 16 30
        , genColumn 31 45 |> Random.map (List.Extra.setAt 2 Free)
        , genColumn 46 60
        , genColumn 61 75
        ]
        |> Random.map List.Extra.transpose
        |> Random.map mapToBingoCard


type Msg
    = GenerateBingo BingoCard


init : () -> ( Model, Cmd Msg )
init _ =
    ( Nothing
    , Random.generate GenerateBingo genBingoCard
    )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GenerateBingo card ->
            ( Just card, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none


view : Model -> Html Msg
view model =
    let
        formattedBingoCard : String
        formattedBingoCard =
            model
                |> Maybe.map showBingoCard
                |> Maybe.withDefault "creating..."
    in
    div []
        [ h1 [] [ text "Elm Bingo" ]
        , pre [ style "font-family" "Courier" ] [ text formattedBingoCard ]
        ]


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }
```

## まとめ

実は実装途中でExtraにあるtransposeやtraverseなどの一般的な関数をゴテゴテ書いてたのですが、@miyamo_madokaさんからExtraあるよって教えてもらい、急遽そちらを使うようにしました。

- 車輪の再発明する前にちゃんとパッケージ読みましょう。
- でも再発明楽しかった、ぜひみんなにもやってほしい！