先日行われた We Are JavaScripters! @19th で Elm と JavaScript ライブラリの連携について発表してきました。
Elm の初心者向けの解説としてよく Msg, Model, update からなるアーキテクチャが挙げられていますが、今回の発表ではもう一歩だけ進んで、Cmd と Sub を使って Elm から JavaScript のライブラリを呼ぶ方法について解説しました。
サーバとしての JS ライブラリ
他の AltJS では JavaScript を呼び出す際、ソースコードの内部に埋め込む形になるのが普通です。
例えば Haskell を JavaScript にコンパイルする GHCJS の場合、JSaddle という DSL を利用して次のように呼び出すことになります。
store :: Int -> IO () store n = runJSaddle () $ do ref <- jsg "firebase" ^. js0 "database" ^. js1 "ref" (val "/counter") ref ^. js1 "set" (val n) return ()
Firebase SDK を呼び出して Realtime DB に値をセットする部分です。JavaScript 側の関数を文字列で指定することで、直接 Haskell のソースコード内に IO
アクションとして JS の呼び出しが定義されていることがわかります。この場合、Haskell とは別に JavaScript 側を自分で実装する必要はありません。
一方、Elm で同様の呼び出しを実装する場合、「Port」「Command」「JavaScript 側での subscribe」という 3 つの部分に分割されます。
まず、JavaScript 側へのインタフェースとなる Port を次のように定義します。
port module RealtimeCounter.Port exposing (store) import Json.Encode as E port store : E.Value -> Cmd msg
JSON を受け取って、Cmd を発生させる関数 store
が定義されています。port
に指定された関数は型シグネチャだけが存在し、中身は記述しません。
次に、実際に update
関数の中でこの Cmd を発生させるために、Elm の Int
値を JSON エンコードしてこの port に流し込む関数を定義します。
import Json.Encode as E storeCount : Int -> Cmd msg storeCount = store << E.int
最後に、Elm 側から発生した Cmd を受け取るための JavaScript を実装します。
const { Elm } = require('./Main.elm'); const app = Elm.Main.init({ node: document.getElementById('app') }); app.ports.store.subscribe((count) => { firebase.database().ref('count').set(count); });
上に挙げたコールバックによる実装を見ても分かる通り、JavaScript 側はあたかも Web サーバのコントローラのように subscribe
で待ち受けており、Elm から Cmd によって JSON が渡されると実際に Realtime DB に値をセットします。
逆に、JavaScript 側から Elm 側に値を戻す必要がある場合には、JavaScript 側で send
関数を使用すると Elm 側からは Sub となって観測されます。具体的には下記のサンプルレポジトリを参照してください。
なお、実際に JS 連携する部分をコーディングする際には、boiyaa さんが書かれている詳しい記事も参考になると思います。
boiyaa さんの記事は Elm v1.18 時点で書かれているので起動時に Html.programWithFlags
を使用していますが、Elm v1.19 ではここに相当する関数は Browser.element
に変更になっています。
動作サンプル
実際に触って動かせるデモもデプロイしておきました。先に挙げたソースコードと合わせて、よかったら参考にしてみてください。
プレゼンの際にお見せしたデモは単にテキストを置いただけの画面でしたが、公開版はちょっと凝った CSS を付けてみました。カウンタ自体の Elm 側のロジックは非常にシンプルで行数も少ないので、GitHub からは Elm ではなく CSS のレポジトリ扱いされてしまっていますが、それはまた別の話。