Edited at
ElmDay 13

複数ページのSPAをElmで書くときのボイラープレート部分をalchelmyで自動的に†錬成†する

Alchemik_Sedziwoj_Matejko.JPG

Elmのようなフレームワークに接すると、そのうちウェブサイト全体をElmで書き直したいという衝動に駆られる人も多いと思います。でも、The Elm Architectureで複数ページからなるシングルページアプリケーション1を作ろうとすると、ルーティングでもupdateでもviewでも、とにかく大量の分岐を書かされるの地味につらいですよね。ひとつページを追加するだけでも、複数のファイルに散らばったコードのあちこちをちょっとづつ修正しなくてはならなくて、とにかく面倒です2

ElmでSPAを実現する仕組みについては、今回のアドベントカレンダーでkawausoさんがちょうど記事にしてくれているので、そちらもご覧になるといいと思います。The Elm ArchitectureでSPAを実現するためには、この「ページを切り替える仕組み」のパターンを覚えなくてはならず、これを理解するまでは少々大変です。慣れてしまえば、めちゃくちゃ難しいというほどではないのですが、出来ることならこんなことに頭を悩ませたくはありません。

PHPなんかだと、適当なディレクトリに適当に空の*.phpなファイルを作れば、それだけでページを追加できるので、静的なHTMLと同じくらいわかりやすいですしお手軽です。ページを消すにもその*.phpを削除すればいいし、パスを移動したければその*.phpを別のディレクトリへ移動すればいいでしょう。もちろんサーバサイドの技術とクライアントサイドの技術という根本的な違いはありますが、Elmでもそれと同じくらい気軽にページを追加したいものです。

そこでalchelmy3という闇のツールを使うと、多少の柔軟性を対価にして、The Elm Architectureの分岐の部分のボイラープレートを自動的に錬成してくれます。アプリケーションに新たなページを追加するときには、そのページに対応するソースファイルをひとつ追加するだけで済み、既存のコードを手作業でいじる必要が事実上なくなります。もちろん、あくまでシングルページアプリケーションとしてです。kawausoさんが解説してくれたいろんなアレの大半は、知らなくても済むのです。すごい! まあ作ったのは私なんですけども。リポジトリはこちらです。


Alchelmyのしくみ

Alchemlyの大まかな仕組みを簡単に紹介しておきます。Elmのシングルページアプリケーションでは、通常はBrowser.application関数に以下のような型のオブジェクトを与えてアプリケーションを定義します(Elmの初期化については、 jinjorさんの2日目の記事も参考にどうぞ)。

    { init : flags -> Url -> Key -> ( model, Cmd msg )

, view : model -> Document msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, onUrlRequest : UrlRequest -> msg
, onUrlChange : Url -> msg
}

対してAlchelmyでは、それとは少し型が異なる次のようなオブジェクトを定義したモジュールをページごとに用意することで、ページを定義します。

    { init : Flags -> Url -> Key -> route -> Maybe Session -> ( model, Cmd msg )

, view : model -> Document msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, onUrlRequest : UrlRequest -> msg
, route : Parser (route -> a) a
, session : model -> Session
}

viewupdatesucscriptionsonUrlRequestはまったく同じですが、initには引数が追加されているほか、routesessionというプロパティが追加され、それからonUrlChangeが削除されています。

routeはこのページへのパスをパースするParserを指定するものです。Alchelmyでは、URLのパーサは一箇所にまとめて書くのではなく、各ページのモジュールにそれぞれ分けて定義します。パスをパースして成功すると、得られた結果のrouteはがinitの引数として渡されます。

また、ページを遷移したときに、任意に定義できるSession型のデータをページ間で持ち越すことができるようになっています。各ページで共通して参照できるグローバルな状態のようなものです。pushUrlでURLが変化したときに、直前のページのmodelから上のsession : model -> Session関数を使ってSession型を取り出し、それが次のページのinitの引数に渡される仕組みです。アプリケーションが読み込まれた最初のinitではNothingになります。

そして、各ページに対応するこのようなモジュール複数定義されるわけですが、Alchelmyはこのような複数のモジュールをそのパスに従って自動的に配置して切り替えるようなコードを生成し、Alchelmy.elmという名前のモジュールとして定義します。ちなみに、Alchelmy.elmにはどんなコードが生成されているのかは、example/src/Alchelmy.elmを見ていただくと一目瞭然だと思います4


Alchelmyのサンプルアプリケーション

elmexamples.png

ドキュメントはまだあまり書かれていませんので、Alchelmyの仕組みについては、以下のサンプルコードを見ていただくのが一番手っ取り早いかと思います。このサンプル自体は何の変哲もない簡単なポートフォリオみたいなアプリケーションで、昔のElmの公式サンプルから、カウンタや、AJAXで特定のテーマのGIFを取ってくるサンプル、SVGで描かれた時計などが移植されています5。このサンプルアプリケーション自体は特に面白いところはありませんが、ビルドは netlify に置いてありますので、以下で実際に試すことができます。ちなみに、各ページは背景部分の色が異なっているのですが、リンクをクリックしてページを遷移すると、CSSのトランジションを使って滑らかに色が変化することがわかると思います。これもシングルページアプリケーションを採用するメリットのひとつですね。

サンプルのコードはこちらで閲覧できます。


さいごに

実際に大規模なElmのシングルページアプリケーションを運用しているサービスではどうしているのかと思い、いろいろ調べてみたのですが、たとえばElmのオリジナル作者Evanさんや、前述のFeldmanさんも所属しているNoRedInkでは、もともとRubyで作られたサービスをElmに順次置き換えつつあるという段階のようで、Elmでこの手の大きなサービス全体をSPAとして作る方法というのは、私が知る限りでは確立されているとまではまだ言えないようです。

あまたのメタプログラミングの中でも、コード生成は特に邪悪な黒魔術のひとつだと筆者は思うのですが、今回ばかりは他に現実的な方法が思いつきませんでした。それでできたツールがコレです。あらゆるユースケースに対応できるわけではないと思いますし、この方法でどこまでうまくいくかは今後もよく検証していく必要があります。かなり前に一晩でヤケクソで作ったやつが元になっていて、最初JavaScriptで書きなぐったやつをPureScriptに移植したりしたりして、今もいろいろ試行錯誤が続いています。

それにまだ課題もいろいろ残っています。例えば複数のページで共通の振る舞いをどう書くかは難しいところで、初期のバージョンでは共通部分と個別の部分が親子のコンポーネントのようになっていて、親子間でメッセージを送り合うみたいな仕組みをいったん実装したときもあったのですが、あまりに複雑なうえ、例外的なケースがあると逆に面倒くさくなるので、その種の機能はあえて削りました。上のサンプルアプリケーションでは404のページなどを除いて各ページが3ペイン式の共通のレイアウトになっていますが、各ページで共通する部分をどのように書くかの検討にもなっています。

昨日の記事はGoryudyumaさんの『ElmとJSONの話』でした。静的な言語で永続的なデータを扱う方法というのは、永遠のテーマなのかもしれません。明日の記事はA_kirisakiさんです。


参照と参考文献





  1. ところでこういう、シングルページアプリケーションなのに、History APIでURLを変えながら仮想的な複数のページを切り替えられるアプリケーションて、なんて呼べばいいんでしょうか。 



  2. このあたりのつらさがピンとこない人のために、つらみをクドクドと説明しようとしたのですが、あまりにクドくなってしまったので消しました。代わりにrtfeldman/elm-spa-exampleにある大量の規則的なコンストラクタや条件分岐をご覧いただければと思います。ひとつページを増やすたびに、これらの分岐に枝をひとつづつ足していくわけです。基本的には難しくはないですがとにかく面倒くさいですし、updateはとくに( _, _ ) ->でパターンマッチングをしており、枝を足さなくてもコンパイルが通ってしまうので、問題は深刻です。 



  3. 名前はalchemy + elmでつけました。読みかたはたぶん『アルケルミー』だと思います。elchemy という名前も考えたのですが、その名前の Elm のツールがすでに存在していたので止めました。 



  4. Alchelmy.elmは自動的に生成されるファイルなので.gitignoreに書いてしまってもいいのですが、サンプルとして見せるためにあえてリポジトリに残してあります。 



  5. そういえばこのElmのサンプル群 ( https://elm-lang.org/examples )は、私がこの Alchelmy を作り始めたときにはまだあったのですが、今はなくなっちゃいましたね。言語仕様がだいぶ変わってきたので、学習のためのリソースを全面的に見なおしているようです。