ElmとGraphQLの出会い - さようならJson Decoder !

## はじめに

昨年にGitHubがGraphQLのAPIを公開して大きな話題になりました。GraphQLに関する詳細は[こちらの記事](https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad)をご覧になっていただければと思います。最近ではBaaS(Backend as a Service)として手軽に利用できるGraphQLサーバー([Scaphold](https://docs.scaphold.io/)、[Hasura](https://hasura.io/)、[AWS Appsync](https://aws.amazon.com/jp/appsync/)等）も増えてきています。

JavaScriptで使われているメジャーなクライアントライブラリは[Relay](https://facebook.github.io/relay/)と[Apollo](https://www.apollographql.com/)でしょうか、大きなプロジェクトでの採用実績も多くあるそうです。ElmからPorts経由でこれらのライブラリの利用することも可能とは思いますが、キャッシングやオフラインのサポート、サブスクリプションで同期だとか高度な事を望まなければ、所詮はJSONデータのやり取りなので、ElmからHttpで直接GraphQLサーバーを叩いて利用する事も可能です。

Elmのコードから、変数を**文字列**で書かれたGraphQLのクエリに埋め込み、さらにJSON形式にエンコードしてGraphQLサーバーに送ります。

```json
{"operationName":null,"variables":{},"query":"{\n  country(code: \"JP\") {\n    code\n    name\n  }\n}\n"}
```

結果として返される以下の様なJSON形式の**文字列**をデコードしてElmの型に戻します。

```json
{"data":{"country":{"code":"JP","name":"Japan"}}}
```

残念なことに、この方法ではElmとGraphQLがそれぞれ持つ型チェックの機能が活用されていません。また、規模が大きくなって複雑になってくると、この方法では破綻するのが容易に想像できます。

## GraphQLサーバーとスキーマ

今回はオープンソースで公開されているシンプルなスキーマのGraphQL APIサーバー（[Graphiqlコンソール](https://countries.trevorblades.com/)と[リポジトリ](https://github.com/trevorblades/countries)）を例にします。そこで定義されているスキーマの内容は下記の通りです。

```
type Continent {
  code: String
  name: String
}

type Country {
  code: String
  name: String
  native: String
  phone: String
  continent: Continent
  currency: String
  languages: [Language]
  emoji: String
  emojiU: String
}

type Language {
  code: String
  name: String
  native: String
  rtl: Int
}

type Query {
  continents: [Continent]
  continent(code: String): Continent
  countries: [Country]
  country(code: String): Country
  languages: [Language]
  language(code: String): Language
}
```

typeで定義されている部分はGraphQLではオブジェクト型と呼ばれ、複数のフィールドを持つことができます。各フィールドには戻り値としての型が合わせて定義されています。詳しくは[こちら](https://graphql.org/learn/schema/)を見て下さい。なんとなくElmの型に似ていますね。実際に全て以下の様なレコード型の別名で書くことができます。

```elm
type alias Continent =
    { code : Maybe String
    , name : Maybe String
    }


type alias Country =
    { code : Maybe String
    , name : Maybe String
    , native : Maybe String
    , phone : Maybe String
    , continent : Maybe Continent
    , currency : Maybe String
    , languages : Maybe (List (Maybe Language))
    , emoji : Maybe String
    , emojiU : Maybe String
    }


type alias Language =
    { code : Maybe String
    , name : Maybe String
    , native : Maybe String
    , rtl : Maybe Int
    }

{-- 実際にはこの部分はElmのレコード型としては使いません
type alias Query =
    { continents: Maybe (List (Maybe Continent))
    , continent: Maybe String -> Maybe Continent
    , countries: Maybe (List (Maybe Country))
    , country: Maybe String -> Maybe Country
    , languages: Maybe (List (Maybe Language))
    , language: String -> Maybe Language
    }
--}
```

> うーん`Maybe`だらけ…これは先のGraphQLのスキーマで、全てのフィールドがnullを許すとして定義されているためです。本来はスキーマ側でもっと制約をかけるべきなのでしょうが、スキーマを変更できない場合には受け入れる他ありません。

この2つの世界の間を出来るだけ**楽をしながら**かつ**型安全**に橋渡しする方法が欲しいのです。graphqlをキーワードとしてElm 0.18のパッケージを検索すると11個、Elm 0.19のパッケージでは淘汰されたのか4個が結果として得られます。
![Screen Shot 2018-12-03 at 0.08.53.png](https://qiita-image-store.s3.amazonaws.com/0/13655/f03a717b-52a1-6a61-26ea-4b228a5140b4.png)
現時点で、この4個の中で最も活発に使われていると思われるのが、一番上の[`dillonkearns/elm-graphql`](https://package.elm-lang.org/packages/dillonkearns/elm-graphql/latest/)になります。全てを試したわけではありませんが、ほとんどは`Json.Decode`と`Json.Encode`をGraphQL用に合わせるアプローチで、フィールド名を文字列として書かなければならず、その点において型安全が失われてしまっています。

一方、@dillonkearnsのアプローチでは、外部ツールを導入することにより、GraphQLサーバーのイントロスペクション機能を利用してGraphQLのスキーマを解析し、Elmのコードを自動生成してくれます。生成されたコードは、主にGraphQLオブジェクトとそのフィールドをデコードするための関数で構成されます。これにより型安全なコードを書けることを保証してくれます。

### 環境の準備

以前に[書いた](https://qiita.com/kyasu1/items/44e67b8755f1adcfef67)parcel-bundlerとElmを組み合わせる内容をボイラープレートにした物を用意したのでそちらを利用します。最初に、GraphQLサーバーからElmコードを生成してくれるツールをnpmでインストールし、必要なElmのパッケージもインストールしておきます。

```bash
git clone git@github.com:kyasu1/elm-parcel-boilerplate.git
cd elm-parcel-boilerplate
npm install --save-dev @dillonkearns/elm-graphql
elm install dillonkearns/elm-graphql
elm install elm/json
```

### スキーマから`Elm`のコードを生成

```bash
npx elm-graphql https://countries.trevorblades.com/ --base Country --output src/elm
```

とすると以下のようなファイルが自動生成されます。
![Screen Shot 2018-12-03 at 0.37.42.png](https://qiita-image-store.s3.amazonaws.com/0/13655/58aa1440-974a-0882-63e1-7d2bf069e3e2.png)

今回はシンプルなスキーマなので使用するのは下記のファイルのみとなります：

- `Country/Query.elm`はスキーマ中の`type Query`で定義されたフィールドをデコードする関数の集まり
- `Country/Object.elm`はスキーマ中で定義されたオブジェクトをElm上での表現するためのカスタム型の集まり
- `Country/Object/Continent.elm`はスキーマ中の`type Continent`で定義されたフィールドをデコードする関数の集まり
- `Country/Object/Country.elm`はスキーマ中の`type Coutnry`で定義されたフィールドをデコードする関数の集まり
- `Country/Object/Language.elm`はスキーマ中の`type Language`で定義されたフィールドをデコードする関数の集まり

## Elmアプリを書いてみる

まずは必要なモジュールをインポートします。

```elm:src/elm/Main.elm
import Country.Object
import Country.Object.Continent as Continent
import Country.Object.Country as Country
import Country.Object.Language as Language
import Country.Query as Query
import Graphql.Http
import Graphql.Operation exposing (RootQuery)
import Graphql.OptionalArgument exposing (OptionalArgument(..))
import Graphql.SelectionSet exposing (SelectionSet, succeed, with)
```

`Graphql`で始まるモジュールには実際にクエリを構築したり送信するためのヘルパー関数が含まれています。

### シンプルなクエリ

先のスキーマを見ると`code`を引数にとり、返り値として`Country`を返すクエリ`country`が定義されています。国コードを指定してその国の情報を取得することができます。

```:graphql
query {
　　　　....
  country(code: String): Country
  ....
}
```

結果として得られる`Country`オブジェクトには多くのフィールドが定義されていますが、この中から`code`と`name`のみを取得するシンプルなクエリ（`SelectionSet`）をElmで構築してみます。

GraphQLのクエリで書くと次のようになります。

```:graphql
query {
  country(code: "JP") {
    code
    name
  }
}
```

自分の理解では`SelectionSet`はGraphQLクエリにおけるフィールドの集まりだと思っています。`query`オブジェクトは`country`フィールド一つで構成される`SelectionSet`を含みます。`country`オブジェクトは`code`と`name`の二つののフィールドで構成される`SelectionSet`を含むといった感じです。この階層に沿って分解して`SelectionSet`を構成していきます。

まずは最終的に欲しいElmの型を定義します。先のスキーマではnull値を返す可能性があるため、全てのフィールドに`Maybe`を付ける必要があります。

```elm:src/elm/Main.elm
type alias Response =
    { countryRowMaybe : Maybe CountryRow
    }


type alias CountryRow =
    { code : Maybe String
    , name : Maybe String
    }


query : String -> SelectionSet Response RootQuery
query code =
    succeed Response
        |> with (Query.country (\arg -> { arg | code = Present code }) countrySelection)


countrySelection : SelectionSet CountryRow Country.Object.Country
countrySelection =
    succeed CountryRow
        |> with Country.code
        |> with Country.name
```

どっかで見たような。そうです、[`NoRedInk/elm-json-decode-pipeline`](https://package.elm-lang.org/packages/NoRedInk/elm-json-decode-pipeline/latest/)の記法と同じです。[^1]

`RootQuery`はGraphQLのスキーマ定義にあった`type query {}`オブジェクトに相当し、`with`以下では必要なフィールドを取得してデコードし、最終的に`Response`型にして返すよう指示します。同様に、`Country.Object.Country`は`type Country {}`オブジェクトに相当し、`with`以下で必要なフィールドだけを取得・デコードし、最終的に`CountryRow`型にして返すようするよう指示しています。

`Query.country`は国コードを指定するための引数を取りますが、GraphQLのスキーマでは`null`でも良いと定義されてしまっているため、`(\arg -> { arg | code = Present code })`という形式で引数を渡す必要があります。もし`identity`を渡すと引数に`null`を指定したことになります。[^2]

実際にこのクエリをサーバーに投げる場合には`GraphQL.Http`を使用します。

```elm:src/elm/Main.elm
execQuery : Cmd Msg
execQuery =
    query "JP"
        |> Graphql.Http.queryRequest "https://countries.trevorblades.com/"
        |> Graphql.Http.send GotRespponse
```

###　ちょっと複雑なクエリ

GraphQLでは複数のオブジェクトや関連するオブジェクトを一度に取得できます。

```
query {
  continents {
    code
    name
  }
  countries {
    country {
    code
    name
    native
    phone
    continent {
      code
      name
    }
    currency
    languages {
      code
      name
      native
      rtl
    }
    emoji
    emojiU
  }
}
```

このクエリでは大陸の一覧と国の一覧を同時に取得しています。このクエリを発行する`SelectionSet`を構築すると以下のようになります。

```elm
type alias Response =
    { continents : List Continent
    , countries : List Country
    }

query : SelectionSet Response RootQuery
query =
    succeed Response
        |> with
            (Query.continents continentSelection
                |> SelectionSet.nonNullOrFail
                |> SelectionSet.nonNullElementsOrFail
            )
        |> with
            (Query.countries countrySelection
                |> SelectionSet.nonNullOrFail
                |> SelectionSet.nonNullElementsOrFail
            )


continentSelection : SelectionSet Continent Country.Object.Continent
continentSelection =
    succeed Continent
        |> with (Continent.code |> SelectionSet.nonNullOrFail)
        |> with (Continent.name |> SelectionSet.nonNullOrFail)


countrySelection : SelectionSet Country Country.Object.Country
countrySelection =
    succeed Country
        |> with
            (Country.code
                |> SelectionSet.nonNullOrFail
            )
        |> with
            (Country.name
                |> SelectionSet.nonNullOrFail
            )
        |> with Country.native
        |> with Country.phone
        |> with
            (Country.continent continentSelection
                |> SelectionSet.nonNullOrFail
            )
        |> with Country.currency
        |> with
            (Country.languages languageSelection
                |> SelectionSet.nonNullOrFail
                |> SelectionSet.nonNullElementsOrFail
            )
        |> with Country.emoji
        |> with (Country.emoji |> SelectionSet.withDefault "")


languageSelection : SelectionSet Language Country.Object.Language
languageSelection =
    succeed Language
        |> with
            (Language.code
                |> SelectionSet.nonNullOrFail
            )
        |> with Language.name
        |> with Language.native
        |> with Language.rtl
```

ここで、`SelectionSet.nonNullOrFail`や`SelectionSet.nonNullElementsOrFail`がパイプに追加されていますが、スキーマ上ではnullが許されているフィールドを、**nullにならないはず**と仮定して、Elmに渡す際に強制的にMaybeを外した値にしてくれます。万が一nullが返された場合にはデコードエラーとなります。

本来は、nullになることがないフィールドであれば、スキーマの設計の際にnon-nullなフィールドと定義するべきなのですが、パブリックなスキーマなどで、変更ができない場合にリスクを覚悟で使用します。また、nullになる可能性がありデフォルト値が存在する場合には、`SelectionSet.withDefault`をパイプに追加して処理することも可能です。この場合にはエラーとなりません。

このスキーマ使った簡単な例を作りましたので参考にしてみて下さい。

動くデモ　[http://kyasu1.github.io/](http://kyasu1.github.io/)
リポジトリ　[https://github.com/kyasu1/elm-graphql-example](https://github.com/kyasu1/elm-graphql-example)

## おわりに

- JSONデコーダを書いていく作業と`SelectionSet`の構築は似ています（というか同じ記法になるように合わせている）。前者でなんとなくデコーダを書いてコンパイルが通っても、フィールド名にタイポがあったり、自分の想定外のJSONが返されると実行時にデコードに失敗ます。こういったエラーの原因の追求には結構時間が取られたりします。一方、後者ではElmの型チェックによりコンパイル時にエラーが発生します。逆にコンパイルが通ればGraphQLスキーマに対しては実行時エラーが発生しないことを保証してくれます。
- 冒頭にも書いたように、当初はGraphQLクエリを文字列として書いてました。レコードに含まれるフィールドが微妙に異なれば、JSONデコーダも別々に用意する必要がありました。スキーマに変更があればその都度手書きで変更…とても苦痛でした。この辺の作業がほとんど自動化されてしまうのでとても楽ができて素晴らしいです。
- 紹介したパッケージの使い方を理解するには、`Json.Decode`の[使い方が理解](https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978)できているのが前提です。Json Decoderが不要になるわけではありません:laughing:
- 偶然にもちょっとだけ[貢献](https://github.com/dillonkearns/elm-graphql/issues/97)しました :blush: 

[^1]: この記事を書いている間にアップデートされてこの記法に変わりました( https://github.com/dillonkearns/elm-graphql/blob/master/CHANGELOG-ELM-PACKAGE.md#200---2018-12-02 )。`Json.Decode`同じ様に`mapN`を用いた記法も用意されています。
[^2]: もっとわかりやすい方法が検討されているようです( https://discourse.elm-lang.org/t/optional-key-records-syntax-proposal/2634 )。