ElmとJSONの話

最近、複雑なJSONをElmで扱いたい的なことがあり、JSONとElmについて知ったのでまとめる。
(思いついたまま書いたので、二番煎じ以降だったらごめんなさい)

## 前置き

### ElmでJSONを扱うには

とりあえず公式の、
https://package.elm-lang.org/packages/elm/json/latest/
を使いましょう。

#### 公式でDecodeできる型

- String
- Bool
- Int
- Float
- List,Array
- Dict
- null

これとその他の関数を組み合わせて、あらゆる(正しい)JSONをパースして、必要な情報を取り出すことができる。
ただし、取り出すときに、取り出したい情報のkeyと型がすでに**わかっていなければならない**。

##本題

ここで思いついてしまった。
**「あらかじめ取り出したい値のkeyや型がわからない場合にも、とりあえずElmの世界に持っていきたい」**

### やりたいこと

あらゆる(正しい形式の)JSONを、Elmの世界に持っていきたい

### とりあえず型を定義する

```elm:Main.elm
type ValueType
    = String String
    | Int Int
    | Float Float
    | Bool Bool
    | Null
    | Object (List ( String, ValueType ))
    | List (List ValueType)
```
JSONで表現できるならなんでも許すような感じで書いた。
JSONはvalueとなれるものがそのままトップレベルに来ることができる。
さらにObject(DictとかMapとか色々な表現があるが、この記事ではObjectに統一する)やList(Arrayなど色々な(ry )は、valueの型が再帰している。

なんか難しそう。

### それに合わせてDecoderを書いてみた

```elm:Main.elm
import Json.Decode as D

valueTypeDecoder : D.Decoder ValueType
valueTypeDecoder =
    D.oneOf
        [ D.string
            |> D.andThen (\str -> D.succeed (String str))
        , D.int
            |> D.andThen (\num -> D.succeed (Int num))
        , D.float
            |> D.andThen (\num -> D.succeed (Float num))
        , D.bool
            |> D.andThen (\bool -> D.succeed (Bool bool))
        , D.lazy
            (\_ ->
                D.list valueTypeDecoder
            )
            |> D.andThen (\list -> D.succeed (List list))
        , D.lazy
            (\_ ->
                D.keyValuePairs valueTypeDecoder
            )
            |> D.andThen (\object -> D.succeed (Object object))
        , D.nullable D.value
            |> D.andThen (\_ -> D.succeed Null)
        ]
```

https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#oneOf
*oneOf*で、列挙したどれかでデコードできるのなら、デコードしてくれるらしい。
とりあえず型全てに対して行いたいので、列挙していった。
*andThen*が実行されるのは成功した場合、つまりそのときには必ず型が決まっているため、*succeed*で定義した型に合わせるように書いた。
再帰が必要なObjectとListに関しては*lazy*で遅延させて評価させる。
この辺りは
https://qiita.com/ymtszw/items/a10229de887b38c7a65b
とかを参考にした。
*nullable*は基本的にどの型のnullを許容するかみたいに書くが、*value*に対してnullを許容するといった形で書けば綺麗に読みやすくなった気がする。

### DecoderがあるならEncoderもあるよね

なんかEncoderではないらしい？ただElmの型にあるものをEncode.Valueに変換していって、それをEncode.encodeに渡せばいいようだ。

```elm:Main.elm
import Json.Encode as E

valueTypeEncode : ValueType -> E.Value
valueTypeEncode vt =
    case vt of
        String str ->
            E.string str

        Int num ->
            E.int num

        Float num ->
            E.float num

        Bool bool ->
            E.bool bool

        Object object ->
            E.dict identity valueTypeEncode (Dict.fromList object)

        Null ->
            E.null

        List list ->
            E.list valueTypeEncode list
```

*case of*でパターンマッチさせて、それぞれの型で変換するルールを書いただけ。

### ここまででできたもの

あらゆるJSONのDecodeとEncodeができるようになった。

## 作ったもの

さて、これを作ったからには、何かに活かせないかなととりあえず書いて公開したものがこちら。
https://goryudyuma.github.io/json-elm/index.html

ただJSONを入れると整形して出してくれるだけの、ggればいくらでも出てきそうなアプリケーション。

ソースコードはこちら。
https://github.com/Goryudyuma/json-elm

もちろん言うまでもないが、中では一度Elmの世界に持っていっている。

## まとめ

このように、あらかじめ入ってくるJSONの型がわからなくても、Elmは対応できることがわかった。

### 感想

途中紆余曲折があったが楽しかった。
最初はnullをMaybeで表現しようとして無駄な表現を書いていたり、標準のJSONパッケージではできないかもと思いParserで頑張るか〜とか思ってたけど、終わってみればめちゃくちゃ綺麗でわかりやすい形になっていた。
ElmとJSONと、少し仲良くなれた気がしました。