Firebaseで保存したDictionary(連想配列)なJSONをElmでテーブル表示してみた

[大量 or 同時に届くWebhookをサーバーレスで手軽に確認する](https://qiita.com/niso1985/items/356a8aee5496aea37975)の続き。

#はじめに
本稿は自分がElmのmapやJson.Decoderに慣れるお勉強のための投稿になります。
初のElmコーディング+投稿なので、不備があれば指摘ください。
なお、筆者は最近Scalaをメインで使ってるため、一部Scala的な表現での理解を書いていきます。

#ニーズ
大量なWebhookの通知内容をFirebaseのUIで確認するのは非常に面倒だったので確認用のViewを作った。
(あと、Elm使ってみたかった)

#ソース全体
elm-architecture-tutorialの06-json.elmがそのまま使えそうだったので、そちらを改良。
https://github.com/niso1985/elm-firebase-realtimedatabase-view/blob/master/src/Main.elm

もし使ってみたい場合は
`url = "replace your firebase realtime database end point with secret"`
の箇所を自分の環境のものに置き換えてみてください。
前回の記事のWebhook通知用URLと同じものでOKです。

#辞書形式(連想配列)のJSON Decoder
まずハマったところは、Dictinaryな形のJSON Decoderを作ること。
前回記事に書いたWebhook(要はPOST)でRealtime Databaseにデータを作ると、keyが任意の形で登録されます。
![firebasedata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223153/0db5590d-e3c7-0769-ff00-a3aacf3dcc4c.png)

これを一覧表示したいのでGETすると、下記のような辞書形式でJSONがFirebaseから返ってきます。
```sh
curl -X GET -H "Content-Type: application/json" https://test-app-75f30.firebaseio.com/message.json?auth=YourSecret
```

```json
{
	"-Lf-tYRiW-53aNymactG": {
		"bar": 1111,
		"foo": "aaaa",
		"maybeFoo": "AAAA"
	},
	"-Lf05qQkl3Kq5a_8Keol": {
		"bar": 2222,
		"foo": "bbbb",
		"maybeBar": "BBBB"
	}
}
```

Decoderについては[[Elm] Decoder a からいろいろ理解ってしまおう](https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978)を見て、わかったようなわからないような・・・といった感じだったので、Dictionary形式についてはさらに理解できませんでした。

[elm-guideのJSON](https://guide.elm-lang.jp/effects/json.html)を見ると最後にdictというのがあったので、[Json.Decoder.dictのリファレンス](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#dict)を見てみて、そこから真似てみた結果こうなりました。

```elm
type alias PostBody =
    { foo : String
    , bar : Int
    , maybeFoo : Maybe String
    , maybeBar : Maybe String
    }


getPostBody : Cmd Msg
getPostBody =
    Http.get
        { url = "replace your firebase realtime database end point with secret"
        , expect = Http.expectJson GotPostBody postBodyDecoder
        }


postBodyDecoder : Decoder (Dict.Dict String PostBody)
postBodyDecoder =
    Json.Decode.map (Dict.map dbRecordToPostBody) (dict bodyDecoder)


bodyDecoder : Decoder PostBody
bodyDecoder =
    map4 PostBody
        (field "foo" string)
        (field "bar" int)
        (maybe (field "maybeFoo" string))
        (maybe (field "maybeBar" string))


dbRecordToPostBody : String -> PostBody -> PostBody
dbRecordToPostBody _ body =
    body
```

まず、今回欲しいレコード自体は`type alias PostBody`として定義。
それに従って、`bodyDecoder`を定義し直してみました。
`Maybe(Optional)`な型については`maybe`をつければ良いだけのようである。なんとも簡単。

`getPostBody`についてはtutorialのサンプルを今回の形に合わせるように書き換えてみました。
この部分に関してはまだ理解がちゃんと及んでいないが、TEAが良しなにやってくれるのだろうといい感じに理解することにします。


##Dictのシグネチャに悩む
```elm
postBodyDecoder : Decoder (Dict.Dict String PostBody)
postBodyDecoder =
    Json.Decode.map (Dict.map dbRecordToPostBody) (dict bodyDecoder)
```
この部分のコードは[Json.Decode.dict](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#dict)のリファレンスから理解できてないまま持ってきたので難しかったです。

### (Dict.Dict String PostBody)
`postBodyDecoder`のシグネチャは上記のとおりだが、はじめはタプルかと思ってたが違った。
どうやら`Dict.Dict`は[二引数をコンストラクタに取るレコード](https://package.elm-lang.org/packages/elm/core/latest/Dict#Dict)であり、`Decoder`は一つの型パラメータ(名前あってる？)しか取らないため`()`で括ってるという塩梅だった。
`Dict.Dict`の第一引数はkeyで第二引数がvalueを表す、まさしくDictionaryなコンストラクタだったわけです。
今回のデータ例でいうと、`-Lf-tYRiW-53aNymactG`となっている箇所が`String`でkeyとなり、

```json
{
	"bar": 2222,
	"foo": "bbbb",
	"maybeBar": "BBBB"
}
```
の部分がvalueとなる。

なので、`(Dict.Dict String PostBody)`は他の言語で言う、key-valueのMapであり、**Scalaで言うと、Map[String, PostBody]**と同じである。

なお、`Dict.Dict`とかかずとも、`Dict`でよいので、以降は単に`Dict`とする。

### 関数本体
ここまでわかったところで関数本体に入っていく。

```elm
postBodyDecoder =
    Json.Decode.map (Dict.map dbRecordToPostBody) (dict bodyDecoder)
```

`Json.Decode.map`に関しては、[前述の記事内](https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978#231-map)が詳しいのでそちらにお任せ。
要は`dict bodyDecoder`で得られた内容を`Dict.map dbRecordToPostBody`で変換して、`Decoder (Dict.Dict String PostBody)`として返す、ということである。

一応`Json.Decoder.map`のシグネチャは、
`map : (a -> value) -> Decoder a -> Decoder value`
であるが、Scalaでは`map`が`List(1,2,3).map(_ + 1) == List(2,3,4)`のように、
**左側にある呼び出し元オブジェクトの要素一つずつに対して、右側にあるmap内の関数を適用する**
という理解だったので、
**`Decoder a`に対して、`a -> value`な関数を適用する**
ということを理解するのに謎にハマってしまいました。[^1]

それではそれぞれ`map`の適用元となる`(dict bodyDecoder)`と、それを変換する関数`(Dict.map dbRecordToPostBody)`を見ていきます。

[^1]: `List.map (_ + 1) List(1,2,3)`という書き方に慣れていなかったということ。

### dict bodyDecoder
`dict`は[Json.Decode.dict](https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode#dict)です。
シグネチャは`dict : Decoder a -> Decoder (Dict String a)`

`bodyDecoder`自体は、

```elm
bodyDecoder : Decoder PostBody
bodyDecoder =
    map4 PostBody
        (field "foo" string)
        (field "bar" int)
        (maybe (field "maybeFoo" string))
        (maybe (field "maybeBar" string))
```

という形で、JSONから`PostBody`型を生成する関数。

これらから、`dict bodyDecoder`は**JSONから`Decoder (Dict String PostBody)`を生成する関数**ということになります。
Firebaseから受け取るDictionaryなJSONをここに渡すと、**`Decoder (Map[String, PostBody])`[^2]な形に変換してくれる関数**になっているということです。

[^2]: ScalaとElmの表現が混じっているので逆にわかりづらいかも？


### Dict.map dbRecordToPostBody
それでは`dict bodyDecoder`で得られた`Decoder (Dict String PostBody)`に対して、どのような変換がなされるのか見ていきます。

`Dict.map`はDictionaryに対しての`map`ですね。シグネチャは次の通り。
`map : (k -> a -> b) -> Dict k a -> Dict k b`
ですので、`Dict`に含まれているkeyの数、`dbRecordToPostBody`が呼ばれるということになります。
辞書形式のJSONが２つ受け取っていれば、`dbRecordToPostBody`が２回よばれるということですね。

`dbRecordToPostBody`は何をしているかと言うと、

```elm
dbRecordToPostBody : String -> PostBody -> PostBody
dbRecordToPostBody _ body =
    body
```

第一引数の値を無視して、第二引数の値のみを返しています。(アンダースコアは使わない値に対して使用します)
`dbRecordToPostBody`の第一引数は`Dict`のkeyで、第二引数は`Dict`のvalueです。
今回、辞書形式で得られるkeyの値はFirebaseが勝手につける値なだけで、使用することはないので、必要な`PostBody`の部分だけ返している、ということになります。

これより`Dict.map dbRecordToPostBody`は**`Dict String PostBody`に対して、`Dict String PostBody`を返す関数**ということになります。

##### なんか違和感・・・
ありません？

**`Dict String PostBody`に対して、`Dict String PostBody`を返す関数**
って同じやん！って感じじゃないですか？

ここでもう一度`Dict.map`を見てみると、`Dict.map`は最終的に`Dict k b`を返します。
`map`なので、`Dict`の中身は変換されますが、`Dict`は`Dict`のままです。
それでは`dbRecordToPostBody`は`Dict`の中身を変換するかと言うと、実はそのまま返しているだけで変換していません。
`Dict.map`のシグネチャを見てみても、`(k -> a -> b) -> Dict k a -> Dict k b`となっており、**`dbRecordToPostBody`で返した値をそのkeyの新しいvalueにするよ！**というものなので、**valueの値が変わらなければ、mapする意味なし**ということになります。


####ということで整理します
もう一度この関数に戻ります。

```elm
postBodyDecoder : Decoder (Dict.Dict String PostBody)
postBodyDecoder =
    Json.Decode.map (Dict.map dbRecordToPostBody) (dict bodyDecoder)
```
これはあくまで、[Json.Decode.dict](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#dict)から取得してきたコードなので、きちんと考えて作っているわけではありません。
慣れてきたので、整理しましょう！

まず、前述の通り、`(Dict.map dbRecordToPostBody)`は何もしないので、不要になります。
それでは`Json.Decode.map`の第一引数である変換関数はどうすればいいでしょうか？

実はこれも不要です。
JSONを取得して、Dictionaryのパラメータを変換したり整形したりする必要があれば、`dict bodyDecoder`で得られた値を変換するために、`Json.Decoder.map`が必要なのです。
今回の場合、`dict bodyDecoder`の時点で、`Decoder (Dict String PostBody)`の型で、なおかつ必要なパラメータが完全に得られているので、最終的にはこうなります。

```elm
postBodyDecoder : Decoder (Dict.Dict String PostBody)
postBodyDecoder =
    dict bodyDecoder
```

途中の余計な説明なんなんやったん・・・。
`dict bodyDecoder`だけの説明だけでええやん・・・。

そしてこれって、
[[Elm] Decoder a からいろいろ理解ってしまおう](https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978)
に書いてあることやん！！
やっと理解できました。

#得られたデータをテーブル表示する
それではJSONを`PostBody`として得たテーブル表示してみます。
Elmでテーブル書くときはこんな感じ。

```elm
table [ style "border" "solid thin" ]
  [tr []
      [ th [ style "border" "solid thin" ] [ text "foo" ]
      , th [ style "border" "solid thin" ] [ text "bar" ]
      , th [ style "border" "solid thin" ] [ text "maybeFoo" ]
      , th [ style "border" "solid thin" ] [ text "maybeBar" ]
      ],
   tr []
      [ td [ style "border" "solid thin" ] [ text "1111" ]
      , td [ style "border" "solid thin" ] [ text "2222" ]
      , td [ style "border" "solid thin" ] [ text "3333" ]
      , td [ style "border" "solid thin" ] [ text "" ]
      ]
   ]
```
ふむふむ。
`tr td`タグを動的に増やしてあげればいけそうや。

調べると、[stack overflowでこんな投稿](https://stackoverflow.com/questions/37806531/elm-list-to-table-rows)が見つかったので、`List.concat`で結合してみるが、うまくいかず・・・。
コメントに書いてある`::`で結合してみると・・・。

```elm
table [ style "border" "solid thin" ]
  (tr []
      [ th [ style "border" "solid thin" ] [ text "foo" ]
      , th [ style "border" "solid thin" ] [ text "bar" ]
      , th [ style "border" "solid thin" ] [ text "maybeFoo" ]
      , th [ style "border" "solid thin" ] [ text "maybeBar" ]
      ]
      :: List.map toTableRow (Dict.values bodies)
  )

toTableRow : PostBody -> Html Msg
toTableRow body =
    tr []
        [ td [ style "border" "solid thin" ] [ text body.foo ]
        , td [ style "border" "solid thin" ] [ text <| String.fromInt body.bar ]
        , td [ style "border" "solid thin" ] [ text <| Maybe.withDefault "" body.maybeFoo ]
        , td [ style "border" "solid thin" ] [ text <| Maybe.withDefault "" body.maybeBar ]
        ]
```

![table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223153/8cf0c5cd-b82d-8550-a7be-c5f9217b64ed.png)

できた！！

#感想
今回、はじめてElmを使ってみたが、シグネチャの読み方と、Scalaのコレクション関数にあたる関数の使い方を軽く覚えれば、もともとのScalaの経験を活かして、かなり簡単にフロントがかける気がした。
個人的に、HTMLは今更やる気しないし、JavaScriptは変態言語だしで、直接それらを学ぶ意欲があまりわかない中で、関数型のパラダイムでこうやってプログラムが書けるのはやはり楽しい。

まだまともにElm guideも基礎からわかるElmも読んでいないので、もう少し読み進めると更に理解が深まりそうで、これからが楽しみです！