こんにちは。ForSchool事業部の石上です。お菓子はばかうけが好きです。今日はElmの話です。
背景
Studyplus for Schoolには、Elmで実装された画面アプリケーションがあります。こういうやつです。
【新機能リリース🎉】
— Studyplus for School(公式) (@Studyplus_FS) 2018年12月6日
本日、新機能として「入退室管理機能」がリリースされました!連携中の生徒さんならStudyplusのQRコードをかざすだけで入退室記録がつきます!
開発チームが塾の皆様にお役に立てるよう想いを持って作った機能になりますので、ぜひ皆様にご利用いただきたいです! pic.twitter.com/zy95D7AMiU
仕様はとても小さく、QRコード読み取る -> APIへ投げるという機能のみだったため、Elmでの実装が許されました。今回は、Elmを普段触っていないチームメンバーに「なんでElmなんて使ってるんだ...?」と思われないように、その良さを伝えておきたいと思います。
Elmの特徴
まずElmについて簡単に書いておきます。Elmの特徴は主に3つでしょう。
The Elm Architecture
The Elm Architectureを構成する要素は、ModelとViewとUpdateです。Modelはアプリケーションの状態を表すデータ構造、ViewはDOMを出力する関数、Updateはイベント1に対して状態を変更する関数です。
Redux経験があれば、あれとほぼ同じものと考えて良いと思います。個人的にReduxとTypeScriptを使う場合と比較して好きなのは、ReduxとTypeScriptだとアクションの型定義が面倒だったり工夫が必要だったりするところ、Elmでは type Msg = MyMsg Payload
のように書けて、記述しやすいのが好きです。
Runtime Errorが起きない
Elmは静的型付け言語です。基本的には2コンパイル時に不正な関数呼び出しを検出することができます。
コンパイルエラーが親切
コンパイルエラーが親切なのもElmの良いところです。たとえば、あるモジュール間で循環参照がある場合に、以下のようにわかりやすいエラーになります。
./src/Main.elm Error: Compiler process exited with error Compilation failed Compiling ...-- IMPORT CYCLE ---------------------------------------------------------------- Your module imports form a cycle: ┌─────┐ │ Note │ ↓ │ Main └─────┘ Learn more about why this is disallowed and how to break cycles here:<https://elm-lang.org/0.19.1/import-cycles>
Your module imports form a cycle
と言われて意味がわからなくても、この図を見れば、「ああ、MainがNoteをimportして、NoteがMainをimportしているからぐるぐるしちゃうんだな」というのがわかります。そして、必ずと言っていいほど最後に参考リンクが載っています。
SPA化
Elmに限らず、ウェブアプリケーションをSPA(シングルページアプリケーション)にするときには、リンクを少し拡張するような仕組みが必要になります。通常のHTMLのaタグであれば、リンクされたURLへ遷移する際、ドキュメント(HTMLなど)をすべてダウンロードしてブラウザに表示します。しかしSPAでは、それを行わずに、必要なリソースを必要なときに取得しつつ、画面の必要な部分を更新するというようなことをします。雑に書くと以下のようなイメージです。
<script> const handleSPALink = (e) => { e.preventDefault(); // 通常の遷移をしない updateState(); // 状態を更新 render(); // 表示 } </script> <a onclick="handleSPALink" href="/hoge">SPA Link</a>
ReactであればReact Routerが使われることが多いです。
Elmの場合、Browser.application という関数を使います。
- Browser.applicationの引数に以下のようにメッセージを設定
haskell Broser.application { ... , onUrlRequest = UrlRequest , onUrlChange = UrlChange }
- a要素の関数でa要素を表示
- a要素をクリックすると、 UrlRequestが発行される
- update関数でUrlRequestをつかまえる
- 内部リンク(
Browser.Internal
)の場合:Navigation.pushUrl
でURLを変更- UrlChangeが発行される
- URLに応じたページの初期化を行う
- 外部リンク(
Browser.External
)の場合:Navigation.load
で外部URLへ遷移
- 内部リンク(
なんだか面倒そうですね。しかしこれには良いところもあって、それはページ遷移してURLが変わったという変更がちゃんとTEA(The Elm Architecture)のなかに収まることです。ReactとReduxの組み合わせで同様のことをやろうとすると、またそれ用のredux middlewareを設定してあげたりする必要がありそうなので、Elmではこういうことが標準の機能として用意されているというのが安心感があります。
詳しくは:https://package.elm-lang.org/packages/elm/browser/latest/Browser#application をご覧ください。
Port
Studyplus for Schoolで実装したアプリケーションではカメラやオーディオを扱う必要があったため、Elmだけでは実装が完結しませんでした。そこで、Portという機能を使う必要がありました。
Portとは、Elmの世界とJavaScriptの世界をきれいに分けて実装する仕組みです。公式のガイドには、localStorageを扱う例が書かれています。
JavaScript側
var app = Elm.Main.init({ node: document.getElementById('elm') }); app.ports.cache.subscribe(function(data) { localStorage.setItem('cache', JSON.stringify(data)); });
Elm側
port module Main exposing (..) import Json.Encode as E port cache : E.Value -> Cmd msg
上記のコードは、Elm側で起こったイベントを、JavaScript側で処理しています。これを使うには、Elm側のupdate関数のなかで以下のようにCmd.batch
という関数にこのcache
関数の返り値を指定します。
update msg model = case msg of Cache value -> (model, Cmd.batch [cache value])
実装がElmだけで完結せず、JavaScriptの依存(package.jsonのことです)がたくさん入ってしまうのは残念なところですが、作りとしてはElmとJavaScriptがしっかり分けられるのは良いことだと思います。
テスト
Elmには、https://github.com/elm オーガニゼーションで管理されている標準のライブラリのほかに、https://github.com/elm-explorations で管理されている準標準ライブラリのようなものがあります3。そこにテストライブラリもあるので、基本的にはそれが使えます。
まとめると?
ごちゃごちゃと書きましたが、まとめるとどういうところが良いのでしょう。
- SPAをつくるために必要なものがちゃんと標準ライブラリ(あるいは準標準ライブラリ)に入っている
- 実行時エラーが起きにくい
逆に良くないところは、以下の2つです。
- localStorageやAudioなどブラウザのAPIを直接触れないところが多く、そういうのが必要なところはJSのコードを書かないといけない
- 学習コスト。特に型の読み方に慣れるまで大変。
アプリケーション実装にElmを使わなくても、Elmを学ぶことで恩恵はあると思います。たとえば、Elmを学ぶ前は、サーバーから来たデータをJSON.parseして型をanyにしてしまうことに、疑問を持ったことなどありませんでした。もしこの記事を読んで興味を持たれたら、仕事で使う予定がなくても、堅牢なフロントエンドを作る仕組みを知るためにElmに触れてもらえればと思います。