Elmを本番運用するためのツールあれこれ

昨年アメリカのベイエリアに引っ越し・転職して入った会社が、ちょうどElmを本格的に本番運用し始めようとしているところでした。それから1年半弱たった今、フロントエンドで新規に書くコードはほぼすべてElm、総行数はテストを含め5万行に達し、その過程で運用がスムーズにいかない場面も経験しました。そこで、

* Elm、よさそうだけど実戦で使うためのツールは揃ってるの？
* 実戦で使い始めたけど他の人はこの辺どうしてるのか知りたい

という読者を想定して、自動テスト・デプロイ・チーム内コミュニケーションに使っているツールややってることを簡単に紹介します。

目次

* 前提条件や環境など
* コーディングスタイルの統一
* テスト
* 依存パッケージの管理
* アセット管理
* CIビルドの高速化のための小技
* コード分析
* 人事採用

# 前提条件や環境など

* 自社プロダクトを作っている会社です
* 1つのページもしくは機能ごとに1つのElmアプリという構成です (サイトの機能全部が1つのアプリに入っている、いわゆるSPAではありません)
  * 現在全部で32個
* Elm 0.16で動いているページとElm 0.18で動いているページが半々くらいの割合です
* バックエンドはRuby on Railsです

  [elm-ops-tooling]: https://github.com/NoRedInk/elm-ops-tooling

## コーディングスタイルの統一

### コードを自動でフォーマットする elm-format

#### 問題

コーディング規約の中でも、インデントや改行の仕方などソースコードのレイアウトに関するものは自動化したいものです。エディタが整形してくれる場合もありますが、実装や設定によって微妙に結果が違ってきたりもします。

#### 解決

elm-formatは、単一のバイナリ + 各種エディタ用プラグインという形をとった自動コード整形ツールです。

これをCIビルド時にバリデーションモードで実行し、成功しなければビルドを失敗させます。

```bash
elm-format --validate --elm-version=0.18 src tests
```

また、開発時にテストを実行するためのスクリプト内で自動的に全てのElmコードを整形しています。

```bash
bin/elm-format-$ELM_FORMAT_VERSION --elm-version=0.18 src --yes > /dev/null
bin/elm-format-$ELM_FORMAT_VERSION --elm-version=0.18 tests --yes > /dev/null
```

  [elm-format-plugin-guidelines]: https://github.com/avh4/elm-format/issues/104

### モジュールの命名規則を明文化する

#### 問題

何をどう分割してどういうディレクトリ配置にするか、については言語仕様上何の制限もないので、散らかりがちです。

#### 解決

普通ですがガイドラインを決めて[共有][elm-style-guide]しています。

ディレクトリ配置に限っていえば、今はこんな感じです:

- `src/Page/` - 各ページ用のアプリたち
- `src/Nri/` - 使い回せるコンポーネント群（使い回せるといっても自社プロダクト内での使い回しに限る）。「Nri」は自社プロダクトの名前の略です
- `src/Data/` - 使い回せる型定義と関連した便利関数など
- `src/` - オープンソース化してしまえそうなくらい使い回しのきくモジュール

  [elm-style-guide]: https://github.com/NoRedInk/elm-style-guide

### 使い回せるコンポーネントを一覧できるページを作る

#### 問題

コードをざっと眺めただけでは、特定のコンポーネントがどういう見た目に対応しているのかを即座に判断できないかもしれません。今作っているページのこのアイコンはもう誰かが実装済みなのかを簡単に知るには？

#### 解決

使い回しのきくコンポーネントを一覧できる内部向けページをRailsアプリに追加しました。ここにあると役立つ何かを書いたときには手動で追加します。

中身は標準的なElmアプリです。エッセンスを抜きだすとこんな感じ：

```elm
type alias Model = ...

{-| 1つのコンポーネントをあらわすレコード -}
type alias StyleModule =
    { filename : String -- どのファイルか
    , importing : String -- コピペしてすぐ使えるインポート文
    , migration : Maybe String -- Elm 0.16 版のソースコードはどこにあるか
    , content : List (Html Msg) -- このコンポーネントを色々な状態でレンダリング
    }

styleModules : Model -> List StyleModule
styleModules model =
    ...

view : Model -> Html Msg
view model =
    List.map viewStyleModule (styleModules model)
```

## テスト

### 単体テストフレームワークelm-testと周辺パッケージ

#### 問題

静的型チェックのおかげで、「`elm-make`が通れば動く」という安心感がElmにはありますが、その動作が意図通りかは保証してくれないので、やはりテストを書かないと枕を高くして寝られません。

#### 解決

単体テストを書くための基本的なパッケージは揃っていると思います。（最近フロントエンドよりインフラ寄りなので、断言は差し控えます！）

* [elm-test] - 単体テストフレームワーク。入力値を自動生成するファズテストもできる
* [elm-html-test] - ビュー関数から返ってきたDOMに対してアサートが書ける
* [elm-testable] - アップデート関数から返ってきたCmdなどに対してアサートが書ける
* [elm-doc-test] - 関数のドキュメンテーション内にテストを書ける

  [elm-test]: https://github.com/elm-community/elm-test
  [elm-testable]: https://github.com/avh4/elm-testable
  [elm-html-test]: https://github.com/eeue56/elm-html-test
  [elm-doc-test]: https://github.com/stoeffel/elm-doc-test


### 自家APIとの整合性をチェックするための JSON Schema

  [ajv]: https://github.com/epoberezkin/ajv
  [json-schema]: https://github.com/ruby-json-schema/json-schema

#### 問題

<!--
自社プロダクトのElmアプリは、単純化するとすべて「バックエンドからデータをもらい、ユーザーの入力に応じて変換したデータをバックエンドに返す」ということの繰り返しです。
-->

Elm内にいる限り型チェックのおかげで何もびっくりするようなことは起こりませんが、外部世界とのやりとりとなるとびっくりするようなこと（== エラー）が起こる可能性があります。

とくに、バックエンドとフロントエンド間で期待しているデータ形式にずれが発生するタイミングはたくさんあります。それぞれコードを書きおこす人が違っていたり、後からバックエンドだけちょちょっと修正したり。そのための統合テストではあるのですが、実行速度に難があります。

#### 解決

リクエストとレスポンスの形式を[JSON Schema][json-schema]で記述し、Elm側が生成するリクエスト、バックエンド側が生成する初期データおよびレスポンスの形式がスキーマに合致するかを単体テストで検証しています。

バックエンド側で使っているバリデーターは[json-schema][json-schema-gem]、フロントエンド側は[ajv][]をNativeコードでラップしてelm-testでテストしています。

こんな感じ：

```ruby
    after do
      expect(assigns(:page_data).to_json).
        to match_json_schema "page/learn/page-data.schema.yaml"
    end
```

```elm
    assertMatchesJsonSchema validator "page/learn/answer.schema.yaml" answerData
```

（JSONは手で書くには冗長なので、YAMLで書いたものをJSONに変換してからバリデーターに食わせてます。）

  [json-schema-gem]: https://github.com/ruby-json-schema/json-schema
  [ajv]: https://github.com/epoberezkin/ajv

#### 今後

JSON Schemaだけではどんな内部APIがあるのか一覧しにくかったり冗長だったりするので、[raml]か？Haskell + [servant-swagger]か[elm-export]か？などと悩んでいます。

  [raml]: http://raml.org/
  [elm-export]: https://github.com/krisajenkins/elm-export
  [servant-swagger]: https://github.com/haskell-servant/servant-swagger

## 依存パッケージの管理

### 依存パッケージのバージョンを固定する

依存パッケージの新規インストール時にはelm-packageが自動で`elm-package.json`に追加してくれますが、許容範囲が `"5.0.0 <= v < 6.0.0"` のように「メジャーバージョンが同じ最新のものを」という指定になります。

`elm-package publish`の仕様上、メジャーバージョンが同じであればコンパイルは通ることが保証されていますが、期待通りの動作をするかまでは保証の範囲外です。「CI時点では問題なかったけど、デプロイまでの間にちょうどバージョンが上がって不具合が出た」などの予想外の事態を避けるため、依存パッケージのバージョンは `"5.0.0 <= v <= 5.0.0"` のように固定し、CI時にバージョンが固定されているか自動でチェックしています。


### テストコード用依存ライブラリのバージョンずれをチェックする elm_deps_check.py と、バージョンを同期する elm_deps_sync.py

[elm-ops-tooling][]の一部です。

  [elm-test-setup]: https://github.com/elm-community/elm-test#running-tests-locally

#### 問題

elm-packageの仕様上、テストコード用の`elm-package.json`をメインの`elm-package.json`とは別に保守する必要があります。手動管理は間違いのもとです。

#### 解決

CIビルド時に、`elm_deps_check.py`でテストコードの依存パッケージ群とメインの依存パッケージ群のバージョンが一致するかを確認しています。一致しなければビルドが失敗します。

```bash
elm-ops-tooling/elm_deps_check.py \
    src/elm-stuff/exact-dependencies.json \
    tests/elm-stuff/exact-dependencies.json \
    --exact --quiet
```

また、開発時にテストを実行するためのスクリプト内で、自動的にテストコードの依存パッケージを更新するようにしています。

```bash
elm-ops-tooling/elm_deps_sync.py src/elm-package.json tests/elm-package.json
```

### 野良Nativeモジュール入りパッケージをインストールする native_package_install.py

[elm-ops-tooling][]の一部です。

#### 問題

Nativeモジュール[^1]を含むパッケージはElm 0.17以降、公式パッケージレジストリには[新規登録できなくなりました][no-more-native]。「Elmの発展のためにはElmパッケージは原則としてElmで書かれているべきである。足りないところはElmが公式にAPIを提供する」という方針になったためです。

  [^1]: JavaScriptで実装されたモジュール。`import Native.Rollbar`のようにElmでインポート・利用できる。

そうはいっても、ブラウザとは関係の薄い領域（テストで便利なnodeモジュールなど）や、Nativeを使わないとコードが冗長になる（metaタグの内容を読みたいとか）など、Nativeモジュールがほしい場面はゼロではありません。自社コード内でNativeモジュールを実装すれば済みますが、再利用性の高いものはオープンソースで公開したいものです。


  [no-more-native]: https://groups.google.com/forum/#!topic/elm-dev/1JW6wknkDIo

#### 解決

`native_package_install.py`で野良Nativeパッケージをインストールできます。パッケージとそのバージョンを指定したJSONファイルを作り、

```json:native-elm-package.json
{
  "NoRedInk/elm-asset-path" : "2.0.0"
}
```

ダウンロード先を指定してインストールします。

```bash
elm-ops-tooling/native_package_install.py elm-native-package.json \
    --elm-config elm-package.json \
    --vendor vendor/
```

結果、ダウンロードしたモジュールのディレクトリが各々`--elm-config`で指定した`elm-package.json`の`"source-directories"`に追加され、Elmアプリから利用できるようになります。

### 通信が不安定でも確実にパッケージをインストールする with_retry.rb

[elm-ops-tooling][]の一部です。

#### 問題

elm-packageはGitHubから[各パッケージのzipファイル][zipurl]をダウンロードしてインストールしますが、ダウンロードに失敗することがたまにありました。何度も`elm-package install`をおこなうCI環境では、みんなのビルドがランダムに失敗する原因になるため、とくに頭痛の原因になります。

#### 解決

[バグ報告][elm-package-163]しつつ、引数にあたえられたコマンドを成功するまで実行しなおすスクリプトを通して`elm-package install`を実行するようにしました。

```bash
elm-ops-tooling/with_retry.rb node_modules/.bin/elm-package install --yes
```

#### 今後

elm-packageが利用しているHTTPライブラリで関係がありそうなバグが解決されたことなどから、現在 (Elm 0.17以降) では[必要ないかもしれません][elm-package-no-more-flake]。近々`with_retry.rb`を外してCIをぶんまわしてみる予定です。

  [zipurl]: https://github.com/elm-lang/elm-package/blob/1a364bccd08f2859f8572d63c2ad5029b6c70ffe/src/Install/Fetch.hs#L132-L135
  [elm-package-163]: https://github.com/elm-lang/elm-package/issues/163
  [elm-package-no-more-flake]: https://github.com/elm-lang/elm-package/issues/163#issuecomment-220515151


## アセット管理

### 画像ファイルその他をCDNにのせやすくする elm-assets-loader

  [elm-assets-loader]: https://github.com/NoRedInk/elm-assets-loader
  [elm-webpack-loader]: https://github.com/rtfeldman/elm-webpack-loader
  [webpack]: https://github.com/webpack/webpack
  [file-loader]: https://github.com/webpack/file-loader

#### 問題

CDN上にキャッシュされているファイルを更新したいタイミングで更新する手法として、ファイル名もしくはクエリ文字列にファイルのバージョンを示す文字列を入れ込むというものがあります。Elmで普通に画像のパスを指定してコンパイルするだけではそうしたことはやってくれません。

#### 解決

[webpack][] + [elm-webpack-loader][]でElmコードをコンパイルした後に[elm-assets-loader][] + [file-loader][]で画像のファイル名にファイルの中身のハッシュ値を入れ込む処理をさせています。

さらに[elm-asset-path][]をかませてRails側からCDNホストを指定することで、

```elm
    img [ src (AssetPath.url <| AssetPath "/assets/star.png") ] []
```

こんなElmコードを書くと

```html
<img src="https://0xcdasdf.cloudfront.amazonaws.com/assets/star-038a1253d7a9e4682deb72cd68c3a328.png"/>
```

こんなURLが生成されます。

またCI時に、CDNにのっていない画像パスを検知したらビルドが失敗するようにしています。

  [elm-asset-path]: https://github.com/NoRedInk/elm-asset-path


## CIビルドの高速化のための小技

<!--
CIビルドはかなりの確率で何もないところにレポジトリをチェックアウトして一からパッケージインストール、ビルドする、という処理を1日に何度もおこなうという点で開発環境と異なっています。ゆえに開発時には遭遇しなかった問題が出てくることがあります。
-->

### テスト用ディレクトリにメインのディレクトリから依存パッケージをコピーする

elm-test用`elm-package.json`の依存パッケージ群は、メインの依存パッケージ群にテスト用パッケージを追加したものなので、大部分が重複します。`rsync`すればGitHubから同じものをダウンロードしなくてすみます。

```bash
rsync -a ../elm-stuff .
```

### Travis向け: build-artifactsディレクトリをキャッシュする

Travis CIには、ビルド中にインストールしたライブラリなどをキャッシュし、次のビルドで自動的に使ってくれる機能があります。`elm-make`の中間ファイル置き場である`elm-stuff/build-artifacts`をキャッシュさせるとビルド時間を短縮できます。

```yaml
cache:
  directories:
    - tests/elm-stuff/build-artifacts
```

[ソース][build-artifacts-trick]

  [build-artifacts-trick]: https://8thlight.com/blog/rob-looby/2016/04/07/caching-elm-builds-on-travis-ci.html

### CIに限らない高速化: elm-makeを走らせる前に全Elmファイルをバッチコンパイルする

20ファイル単位でelm-makeにかけるようにしたところ、コンパイル時間が半分ほどになりました（2016年4月時点）。これはelm-makeの並列処理の仕方に関わると思われますが、具体的な理由はつかめていません。

疑似コードで書くとこんな感じ：

```
Main.elm以外のElmファイルを20個ごとにelm-make --yes --output /dev/null
Main.elmを1つずつelm-make
```

## コード分析

### SlackからElmコードについてクエリを投げられる slack-today-i-did

#### 問題

「Elm 0.16から0.17への移行状況を簡単に確認したい」「このページはどれくらい簡単に0.17に移行できるか確認したい」「chatopsいいよね」という気運が高まりつつありました。

#### 解決

メインのGitレポジトリを手元にクローンして、ちょっとした分析結果を答えてくれる[Slackボット][today-i-did]を運用しています。

  [today-i-did]: https://github.com/NoRedInk/slack-today-i-did

Elm 0.16で書かれたTabsコンポーネントをElm最新バージョンに対応させるのはどれくらい大変か[^2]を聞いてみた例：

```
@today-i-did how-hard-to-port Tabs

We have found the following filenames:
Here's the breakdown for the:
file Tabs.elm: total hardness 14
Ports and signals : 3 | Html stuff : 11
```

  [^2]: 「大変さ」は経験上の感覚を数値化したもの。

## 人事採用

### とくにElmの経験を必須としない

#### 問題
マイナーな言語を採用すると、プロジェクトに新規に関われる人がまったく見つからなくなってしまうのではないか？

#### 現状

新卒はもちろん、中途でもとくにElmの経験を必須条件とはしていません。「Elmやりたい！」という意欲があるかだけ確認しています。

そしてElmはまだマイナーではあれど、とっつきにくい言語ではありません。「はじめてのElm」というお題で面接でペアプロすることもありますが、みんな書けてしまうので適格不適格の情報を得にくく、お題としてはダメなのではないかという声さえあります。


----

以上です。参考になれば幸いです。「自分はここはこうしてる」などあればコメントでぜひ。