ジンジャー研究室

長めのつぶやき。難しいことは書きません。

Elm を既存の JavaScript と併せて使う N の方法

今月、社外向けに1回、社内向けに1回 Elm の紹介をしたところ、両方とも好評で「ぜひ使ってみたい」という声が多く、良かったと思う一方で、ここは補足すべきだったなーという点があったので書きます。

両者に共通して頻出だったのが、

  • 既存プロジェクトの一部に導入するにはどうすればいいか
  • JavaScript のライブラリは使えるのか

という質問。 Elm の紹介としてはメインの話題じゃないんだけど、まぁ実際に導入しようと思ったら確かにそこ一番気になると思う。

Elm の初期化方法

まず、Elm の仕組みから見ていくと、例えば Main モジュールをエントリポイントとした場合、JavaScript 側で最も簡単に初期化する方法は次の通り。

<script src="/assets/elm.js">
<script>
Elm.Main.fullscreen();
</script>

fullscreen() を使うと、Elm で書いた画面がウインドウいっぱいに表示される。さらに、Elm の main 関数はフラグ(引数)を受け取ることができるので、設定の類を最初に突っ込むと良い。

<script>
Elm.Main.fullscreen({
  apiHost: 'https://hogehogeapi.com'
});
</script>

次に、画面の一部に Elm の画面を埋め込むには、次の例のように embed() を使う。

<div id="target"></div>
<script>
var elemenet = document.getElementById('target');
Elm.Main.embed(elemenet);
</script>

ちなみに、ここまでの例では HTML に直接ロジックを書いているが、最近のフロントエンド開発では Node.js で開発することが多い。Elm が吐き出すコードはそのまま Node.js のモジュールとして読み込める

var Elm = require('./elm.js');

Elm.Main.fullscreen();

さらに、 React コンポーネントとして Elm を利用することもできる。公式サポートではないが数十行のコードでそういう仕組みが作れる。具体的な話は公式記事の How to Use Elm at Work に書かれている。

JavaScript のライブラリを使いたい

Elm から JavaScript を呼ぶ(あるいはその逆)をするために Port という仕組みがある。これを使うと、JavaScript の世界と Elm の世界はお互いにデータを通信し合うことができる。

ここで注意すべき点は、 JavaScript は Elm の下位レイヤーではなく外部サービスになる ということ。Elm の世界から見た JavaScript の世界はサーバーだと思えばいい。これを公式ドキュメントではJavaScript as a Service」と呼んでいる。

例えば、 JavaScript 側にスペルチェックのロジックが実装されていて、 Elm 側からそれを使いたいとする。その場合、Elm 側から「スペルチェックをしてくれ」というリクエストを投げる。すると JavaScript は処理結果を Elm 側に返す。コードにするとこう。

var app = Elm.Spelling.fullscreen();

app.ports.check.subscribe(function(word) {// Elm 側からデータを受け取る
   var suggestions = spellCheck(word);
   app.ports.suggestions.send(suggestions);// Elm 側に結果を返す
});

制約としては、Elm 側のコードは非同期処理の書き方になる。この辺りは無理してハックするよりも、そういうものだと受け入れてそれ前提で設計を組むのが良いと思う。クラウドの画像処理エンジンを利用するのと同じ。テキストエディタとか(まだ Elm のライブラリがない)も無理せずに JavaScript のライブラリを使って Port でやりとりする。

Elm の世界と JavaScript の世界は隔離されていて、 Port 以外の方法でやりとりすることはできない。言い換えると、Elm が JavaScript のライブラリを「ラップ」することは出来ない。もし、ラップが可能であるとすると Elm の特徴である「実行時例外が発生しない」という保証を守ることができない。ユーザーからしたら見た目 Elm の関数だから、実は中で JavaScript がモリモリ使われていてクラッシュするという話だと疑心暗鬼になってしまう。だからそれは出来ないようになっている。(Elm の外側で起きたらそれは知らない、500エラーのようなもの)

同様に、Elm のエコシステム(外部ライブラリ)も JavaScript ライブラリをラップしているものは一切なく、全て Elm で書かれている。JavaScript をラップしたライブラリを公開することはシステムに禁止されている。だからインストールして使ってみたらクラッシュするという心配はない。

CSS はどうするの?

Elm 固有の特別な仕組みはない。普通に class をつけて 外部の CSS ファイルでスタイリングすれば良いと思う。

他の方法として、このブログでは一度 CSS in Elm (CSS in JS の Elm 版、要はインラインスタイル)という方法を紹介していてモジュール化の観点からは良いのだが、開発ツールとか色々考えていくと総合的なバランスは悪いので、一般的用途にはあまりおすすめできない。elm-css という有名なライブラリもあるのだが個人的にはちょっとやり過ぎ感を感じていて保留中。あとは CSS Modules の Elm 版を作る動きもあったが、今どうなってるか知らない。

とまあ、高度なことをしようとすると色々事情はあるものの、普通の CSS を普通に書くぶんには何も困らない。

失敗しない Elm の導入方法

良いツールがあったとしても、上手くいくかどうかは最終的にチームワークとか組織論とかに行き着いてしまう。社内向けには「ハイリスク・ハイリターン」と表現した。JavaScript に比べたら全然情報がないので、スタート地点でリスク高いのは否定できない事実で、そこからどう工夫してリスクを下げていくかが戦略になる。

その辺の話も公式記事の How to Use Elm at Work に書かれている。この記事にかかれているのは、 Elm を導入して成功した事例の共通点をまとめたものだ。短い記事だから興味のある人は全部読んでほしいのだが、まとめるとこう。

  • 小さい部分から実験しながら徐々に導入する
  • チームに必ず一人めっちゃ詳しい人がいる状態にする
  • 最初のプロジェクトは一番メリットの出る場所を戦略的に選ぶ
  • 議論も良いがとにかく Elm のコードを実際に書いてみる

本文にも書いてある通り 「リスクを最小化する」 のがポイント(というか、これ全然 Elm に限った話じゃないと思う)。

まとめ

なんやかんや言って、コードを見るのが一番イメージ湧きませんか?なので連携部分をコードで紹介しました。参考になれば幸いです。

追伸: Elm 本は 0.19 が出たタイミング(時期未定)で校正入れるので、もう少しお待ち下さい。

Idris で簡単なゲームを作ってみた

3日ほど前に、↓の記事を読んで「最近 Idris 熱いのかー」と思ったので入門してみた。

takezoe.hatenablog.com

実は以前から Elm コミュニティのエッジな人が Idris すごいと言ってて気になっていて、ちょうどバージョンも最近 1.0 になったばかりというのもあってタイミングとして良いと思った。

出来たのがこれ。

github.com

■ ■ □ ■ ■
■ ■ □ □ ■
□ □ ■ □ ■
■ □ □ □ □
■ ■ ■ □ ■

33
■ ■ □ ■ ■
■ ■ □ □ ■
□ □ ■ ■ ■
■ □ ■ ■ ■
■ ■ ■ ■ ■

12
■ ■ ■ ■ ■
■ □ ■ ■ ■
□ □ □ ■ ■
■ □ ■ ■ ■
■ ■ ■ ■ ■

21
Cleared! Press any key to continue.

ライツアウトを知らない人はググってください。

依存型の旨味を感じたかった

依存型を使うと、値から型を作ったりということができる。例えば「長さ n のベクトル」だったら Vect n のようになる。そうすると、 IndexOutOfBounds 例外を未然に防げる。これが Elm とかだと範囲を超えたら Maybe が返るから例外は出ないんだけど、確かに Maybe を取得した後に即座に分岐して Nothing の方に Debug.crash "ここに来ることはあり得ない" とか書いたりするので、それを防げるのは嬉しいのかもしれない。そういう期待があったのと、それ以外の用途が知りたいという動機があった。

お題にライツアウトを選ぶのは、以前からオセロとか 15 パズルみたいな盤面を使うゲームを使う時に、上で触れた「いちいち Maybe で面倒」問題が発生しやすいのを感じていたから。

実装例(一部)

今回は Lights n という型で長さ n の正方形を表したいので、2次元のベクトルの別名とする。型の別名は次のように書く。

Lights : Nat -> Type
Lights n =
  Vect n (Vect n Bool)

Lights n自然数(Nat 型)の引数 n を取って型を作る関数とみなせるので、 Nat -> Type

続いて2次元のインデックスも型にしておく。長さ n までのインデックスが Fin n なので、それをタプルにする。(Fin は Finite Set の意味でインデックスというよりは有限な集合的な意味らしいのだが、詳しいことはよくわからない。)

Position : Nat -> Type
Position n =
  (Fin n, Fin n)

ライツアウトの場合、押したボタンとその上下左右のボタンが反転するので、全てのインデックスが盤面をはみ出ないようにしたい。次の関数は、指定した分だけ移動した時に成功したら Just (Position n)、失敗したら Nothing を返す関数。

move : Integer -> Integer -> Position n -> Maybe (Position n)
move {n} dx dy (x, y) =
  case (integerToFin (finToInteger x + dx) n, integerToFin (finToInteger y + dy) n) of
    (Just x, Just y) => Just (x, y)
    _ => Nothing

一応出来た…がこれで良いのだろうか。 finToInteger で整数にして integerToFin でインデックスに戻しているところが、すごく負けた感がする。結局そこで整数にするのかよ。ちなみに最初の {n} は、型引数にある n の値をそのまま持ってこれる。なにやらすごく不思議な感じがする。

メインループは Elm アーキテクチャで実装

CUI だけど GUI とみなせば、見慣れた Elm アーキテクチャが使える。

モデルは、タイトル画面とプレイ中の状態のいずれか。

data Model =
  Start | Playing (Lights Main.size)

メッセージは、タイトルからゲーム画面への遷移と盤面を押すアクション。

data Msg
  = NoOp
  | StartGame
  | Toggle (Position Main.size)

アップデート。本当は最初の盤面をランダムにしたかったが、その辺は未学習なので決め打ち。 NoOp とかあり得ないモデルとメッセージの組み合わせがやはり出てきてしまったのが無念。その辺も型でなんとかできることを期待しているのだが。

update : Msg -> Model -> Model
update msg model =
  case (msg, model) of
    (NoOp, _) =>
      model

    (StartGame, _) =>
      Playing $ toggle (position 2 2) (empty size)

    (Toggle position, Playing lights) =>
      Playing $ toggle position lights

    _ =>
      model

ビューは、モデルが与えられた時に画面に表示する情報と入力をどうメッセージに変換するかをタプルで返す。Html msg のように Output msg のような別名を与えてもいいかもしれない。

view : Model -> (String, String -> Msg)
view model =
  case model of
    Start =>
      ("Press any key to start.", \_ => StartGame)

    Playing lights =>
      if isEmpty lights then
        ("Cleared! Press any key to continue.", \_ => StartGame)
      else
        (format lights, decodePosition)

感想

正直、ここまでで依存型の恩恵を肌で感じ取るには至らなかった。「盤面の長さが5なんだけど8とかが来るのを未然に防げてよかったー」みたいな気持ちにならない。null が来ないのを未然に防ぐのに比べて、ある範囲の整数値が来ないことはあまり嬉しくないということなのかなんなのか。あるいは使い方が間違っている?その辺り、 Idris 脳にならないと普通の Haskell を書いてしまうので難しいと思った。

当初の予定としては JavaScript に吐き出して HTML のデモを作ろうと思ったのだが、 FFI に関するドキュメントが古いのか、うまくインストールが出来なかったので諦めた。

コンパイル時のエラーメッセージに関しては、親切にしようという努力は見られるものの結構おかしな事を言ってくるので注意が必要。本当は「変数が見つからない」なのに「型が曖昧」と怒ってきたり、パターンマッチでコンストラクタ名を間違えるとワイルドカードになってコンパイルが通ってしまったり、色々と問題がある。

あと、型システムに関しても微妙に Haskell と違う点があって、Wiki にまとめられている。さらっと書かれてるけど「deriving does not work yet」めっちゃ辛い。

現時点での感想は以上です。

Elm のコンポーネント論争とは何か

Elm 界隈で「コンポーネントをどう作るべきか」みたいな話がよく出る。日本に限った話ではなくメーリングリストとか Slack でも頻出の話題で、その度に熟練者が説明するのだが、すんなり理解されることもあれば喧嘩になることもある。

ちょうど昨日 Twitter で盛り上がってたので、可能な限りわかりやすく現状を説明してみる。

TL;DR

出来る限りコンポーネントを作らずにビューの関数で済まそう。

コンポーネントとは何か

最初に言ってしまうと、 Elm にはいわゆる「コンポーネント」という画期的なシステムはない。ただ関数があるだけだ。

ここでいうコンポーネントというのは、例えば date picker のような HTML 内に埋め込める便利な UI のようなもので、厳密な定義はない。JavaScript 的な感覚としては、画面内にポンと設置すればあとはよろしく動いて欲しいのだが、 Elm だとそのようには行かない。なぜかというと Elm のビューは純粋な関数で状態を持てない、言い換えると UI の状態をビューが管理できないからだ。代わりに UI の状態はモデルで管理する。例えば、date picker なら現在選択されている日付や表示されている月なども全部モデルが管理することになる。

まずここで、そんな UI の状態なんかいちいち管理してられるかーという話になる。だがこれは仕方がない。その代わりにビューが純粋な関数であることで得られるメリットがある。Virtual DOM 描画の最適化だ。次のようなビュー関数があったとする。

view model =
  viewSomething model.something

ここで、somethingの値が変わらなければ、viewSomething関数で生成される値は常に同じだ。なぜなら全ての関数は純粋であって状態に依存しないことが保証されているからだ。この性質を使うと、 something の値が変わった時だけ viewSomething を走らせて実際の描画を行うということが簡単にできる。具体的には次のようにする。

view mode =
  lazy viewSomething model.something

この lazy はキーワードではなく、 Elm の HTML ライブラリが提供する関数(lazy : (model -> Html msg) -> model -> Html msg)だ。こうしておくと、 Elm ランタイムは something の値が変わったと判定するまで関数の評価を遅延する。もし仮に viewSomething の中のどこかに状態を持つコンポーネントがあったとすれば、このようなことはできない。例えば、現在時刻に依存する時計コンポーネントを置いて lazy で評価をスキップすれば、動かない時計の出来上がりだ。ところが Elm ではそもそも全て純粋だと分かっているから lazy をつけるに当たってそのような心配をする必要はないし、内部に状態が含まれていないかどうかを調べて回る必要もない。

話を戻すと、 UI の状態をモデルで管理しなければいけないという問題は依然として残っている。そのこと自体は避けられないのだが、様々な工夫によってその負担を軽減することができる。

かつて推奨された方法

コンポーネントの話に入る前に、Elm アーキテクチャについて触れておく必要がある。

Elm アプリケーションは model, update, view という3つの部分に分けて記述する。以下はシンプルなカウンターの例。(※シンタックスハイライトが Haskell なのでちょっと色がおかしい)

f:id:jinjor:20170512144814p:plain

-- モデルの定義と初期値

type alias Model = Int

model : Model
model = 0

-- メッセージの定義と更新

type Msg = Increment | Decrement

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model - 1

-- 描画

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

まず、モデルとしてカウンターの型を定義(ここではInt)して初期値を 0 とする。更新処理は、コンポーネントから発火したイベント(メッセージと呼ぶ)がインクリメントなら +1 デクリメントなら -1 とする。最後にビューとイベントハンドリングを書く。

「なんだ MVC か」と思ったら大体その理解で良いと思う。今の状態だと単に画面にひとつカウンターをおいたアプリケーションを作っただけで、コンポーネントにはなっていない。そこで、このカウンターをコンポーネントにするために「独立した3つのカウンターが必要」という想定でアプリケーションを作ってみる。

f:id:jinjor:20170512145043p:plain

実は上のコードをそのまま再利用することができる。上のコードをCounterという名前のモジュールにして、Main モジュールから呼び出すと、次のようになる。

-- モデルの定義と初期値

type alias Model = List Counter.Model

model : Model
model = [ Counter.model, Counter.model, Counter.model ]

-- メッセージの定義と更新

type Msg = CouterMsg Couter.Msg

update : Msg -> Model -> Model
update msg model =
  case msg of
    CouterMsg index counterMsg ->
      List.indexedMap (\i counter ->
        if i == index then Counter.update counter else counter
      ) model


-- 描画

view : Model -> Html Msg
view model =
  div []
    ( List.indexedMap (\i conter ->
        Html.map (CouterMsg i) (Counter.view counter)
      )
    )

まず、3つ分のカウンターのモデルを作る(すべて初期値 0)。更新処理は、いくつ目のカウンターから来たメッセージかを判定して、対応するカウンターのモデルを更新する。最後に3つのカウンターを描画する。

ここで面白いのは、実装詳細が完全に Counter モジュール内に隠蔽されていることだ。Counter.ModelInt であること、インクリメント・デクリメントというメッセージ、ビューの具体的な中身は外からは全く意識しなくていいようになっている。これによって、機能追加があっても変更はモジュール内に閉じることができる。たとえば「リセットボタンを追加してくれ」なら、Counter モジュールのメッセージに Reset を生やして更新処理を追加、さらにリセットボタンをビューに追加すれば良い。

もうひとつ面白い点は、Main モジュール、 Counter モジュールともに model, update, view の構成になっており、一種の階層構造と見ることができる点だ。この構造の美しさが、多くの人を魅了すると同時にアンチパターンに陥れる原因になった。それは後で触れる。

さて、ここでまともなプログラマからは「いやちょっと待て」というツッコミが入る。なぜなら、コンポーネントから受け取ったメッセージを元に対応するコンポーネントの状態を更新するという処理はあまりに退屈だからだ。それこそがこの記事で扱う「論争」の火種である。

ボイラープレートを消す努力

このボイラープレートをなんとか手で書かなくて良いようにしようと積極的に取り組んできたのがelm-mdlというライブラリだ。マテリアルデザインは Ripple のような視覚エフェクトのために CSS ではなく JavaScript のロジックを使ったりする。その是非はここでは置いておくとして、こういうことをしようとするとコンポーネントは状態の宝庫になる。それで UI を置くたびにボイラープレートが増えるのが嫌なので、elm-mdl では一意なキーを割り当てることによって、パイプラインをライブラリ側に任せるという方法をとっている。

elm-mdl は使ったことがないので細かいことは分からないが、まあそうなるだろうなという感想。ちなみにこういう用途に対しては、WebComponents の Custom Element が有効に使えるという話もある。

というわけで、こういうコミュニティの努力が一応ないことはない。ただこういう仕組みを導入することによって生じる副次的な複雑さがあるのは事実で、まあ我慢して書いてもいいんじゃないのみたいな気分になったりする。

過度のコンポーネント

もうひとつ議論になるのが「そもそも状態を持つコンポーネントってそんなに必要?」という話で、言い換えると「ほとんどの場合は純粋なビューの関数で済むんじゃないか」ということだ。

たとえば、先ほどの3つのカウンターの合計値を知りたいとする。3つの値はそれぞれのカウンターが持っているので、合計を求めるには次のようにする必要がある。上の例だと List.sum model で済むのでそんなに問題にはならないのだが、カウンターのモデルがレコードになったり、もっと複雑になってくるとなんらかの API を解して値を取得する形になってきて、結構面倒になる。

もうひとつは、コンポーネントに良くある「○○が起きたときに△△イベントを発生させる」というもの。考慮すべき点は、クリック時ではなくモデルの更新時にそれが分かる場合がある(たとえば「カウントが10の倍数になった時にイベントが発生」)ことで、これを実装すると Coutner.update : Msg -> Model -> (Model, Event) のようになる。これも少々煩雑だし、だんだんボイラープレートも機械的に書けなくなってくる。

ただ、ここでの問題は面倒なことではなく「必要以上に面倒」なことだ。 そもそもこのコンポーネントは必要だろうか? 今ここにあるのは「増減ボタンとリセットボタンがあり値が10の倍数になった時にイベントを発生させるカウンター」だ。汎用的に使えるとは思えないし、明らかにアプリケーションの特定機能を意識している。だとすれば、普通に Main のモデルに数値を持たせて、ビューはそれを描画することに徹したらどうだろう。そうすれば、 Counter モジュールに必要なのは view だけになってすっきりする。それ以外はアプリケーションロジックだ。

これは馬鹿馬鹿しい例だが、実際には結構やってしまう。なぜなら「すべてはコンポーネントだ」という前提で設計を始めてしまうと必然的にボトムアップになり、親子のコミュニケーションが発生してしまうからだ。「親子のコンポーネントでどうやってコミュニケーションを取ればいいのか」という質問はコミュニティでは頻出で、Slack でこれを質問すると必ず「やあ、君が作ってるのはどういうタイプのアプリケーションで、どこでそんなコミュニケーションが必要になるんだい?」という質問返しから始まって、最終的には「それコンポーネントにする必要ないからビューの関数でいいよ」になる。

そもそも上の3つのカウンターの例は、かつて Elm アーキテクチャのドキュメントにあったサンプルそのもので、あまりに多くの人がこのアンチパターンにはまるのである日きれいさっぱり削除された

Remove all the nesting/ examples

This was leading people astray. Need to have examples that let folks work up to these concepts so they do not overuse them in inappropriate situations.

「不適切な状況で使いすぎる」とあるように、この方法が全面的に駄目なわけではなく、使いどころによっては良いが間違った使い方をされやすいということだ。

コンポーネントの設計方針

というわけで、Elm アーキテクチャをスケールさせることに関して最新のドキュメントはこちら。

Scaling The Elm Architecture · An Introduction to Elm

雑な要約:コンポーネントじゃなくて再利用可能なビューの関数を作るんだ

Too Much Reuse

雑な要約:その状況に応じた一番シンプルな方法で解決するんだ、必要以上に汎用化するんじゃない

More · An Introduction to Elm

雑な要約:参考までに一番複雑なパターンも紹介するけど、こんなの実際ほとんどないからね

この通り、相当懲りているらしくかなり口すっぱく書いてある。これが書かれた頃からコミュニティでは「コンポーネント」という言葉自体が禁句のようになっていて、話を持ち出すと何かと炎上する(個人的には過剰反応気味な気はするが)。ちなみにドキュメントはまだ書き途中で、複雑なパターンの紹介は今後また増えるとのこと。それまでは、 elm-sortable-table が一番参考になる。

github.com

あくまでひとつのサンプルという位置付けなので銀の弾丸ではない。実装も面白いが README に設計の観点が書いてあるので、まずそっちを読んでほしい。おそらく一番重要なポイントは データ本体と UI 自体の状態を明確に区別し、データは UI に持たせないということで、結構複雑に見える UI も隠蔽すべき状態というのは意外と少ない。

ここまでが Elm 作者の Evan Czaplicki 氏の見解。同じ NoRedInk 社の Richard Feldman 氏による回答例は以下。

www.reddit.com

YAGNI

たぶん気づかれた方も多いと思うが、上で言ってることはほとんどYAGNIの原則そのままだ。要するに「本当に必要になるまで作るな」を徹底して幸せになれるということで、個人的な実感としてもこれは正しいと感じる。最近作った個人のホームページでは、MIDI プレイヤーが必要だったのだが、これも 必要が生じて後から分割した

この分割は機械的にできる。以下は Main モジュールから MidiPlayer モジュールに関係ありそうな部分をモデルから引き剥がした例。

Main.elm

type alias Model =
     { midiContents : Dict String MidiContent
     , selected : Maybe Content
-    , playing : Bool
-    , startTime : Time
-    , currentTime : Time
-    , futureNotes : List (Detailed Note)
+    , midiPlayer : MidiPlayer
     , gitHub : GitHub
     , fullscreen : Bool
     , error : Error
    }

MidiPlayer.elm

+type alias MidiPlayer =
+    { playing : Bool
+    , startTime : Time
+    , currentTime : Time
+    , futureNotes : List (Detailed Note)
+    }

これと同じことを update と view についてもそれぞれやれば完了。Elm では強力な型の力でリファクタリングが安全に行えるので、この変更で実際に起きたバグは0件だった。

実際にアプリを作っている人ほどこれで幸せになっているのでこの方法が良いと感じていて、それでも良く燃える原因は「プログラミング言語なんだから無限にスケールする汎用部品を作れて当然だろ」みたいな思想と YAGNI 的な世界観が衝突しているせい。自分も Eclipse とか Excel みたいな複雑・大規模・高機能なやつを作りたいのでそれは分かる。ただ、実際に1ページに10000行詰め込んだ感想としては、汎用コンポーネントは実際ほとんどないし、フラットに書き直した方が簡単だったなーだった。もちろん世の中には汎用コンポーネントを多く必要とするアプリも沢山あると思うので、そういうのをまずは作ろうとしてみて無理だとなってから文句を言うのでいいんじゃないかなと思っている。 Elm の開発はプラクティス重視で、実例を持ってきて十分よくあるパターンだと判断されると優先度が上がる傾向にあるので。

課題とまとめ

現状の問題は、まだ古い Elm アーキテクチャに引きづられている人が多いことと、消えた分のドキュメントを補完する情報が足りなくて新規ユーザーにとっては道が途切れたようになっていることだと思う。この記事も、古い情報を上書きするために書いている。

で、ここまでの内容を1行でまとめるとこうなる。

出来る限りコンポーネントを作らずにビューの関数で済まそう。

以上です。

Elm で個人ホームページを作ってみた

まず個人ホームページって響きが懐かしいな!という話はさておき。

普段は複雑な GUI 作りたいモチベーションで Elm をやっているんだけど、もちろん普通の Web サイトも普通に作れますよということで、参考になれば。

作ったサイト

world-maker.com

高校の頃からやっている自作音楽を公開するサイト。タイトルが中二っぽいけど気にしてはいけない。

ソース

github.com

使った技術

数年前のリニューアルでは Polymer を使ったが、ブラウザのバージョンアップで動かなくなって放置していた(トラウマ)。今回はもっと安心感のある Elm を使う。

MIDI + MP3 プレイヤー

f:id:jinjor:20170511074806p:plain

ピアノロール大好きなので、MP3 と並行して MIDI から読み取ったノートをピアノロールで流せるようにした。描画は Elm 公式サポートの SVG ライブラリ(elm-lang/svg)を使う。普通の HTML 要素と同じく Virtual DOM なので宣言的・富豪的にプログラミングできる。

バイナリデコーダ

MIDI はバイナリ形式だが、現在 Elm はバイナリをサポートしていない。そういう時は JavaScript 側で読んで Elm 側に送り込むのが推奨のなのだが、どうしても Elm でやりたかったので Native(Kernel) モジュールという裏技を使って実現した。もちろんバイナリ用のデコーダーなど用意されていないので、自分で作った。

github.com

Native(Kernel) モジュールを使うのはエコシステムとして健全な方法ではないので広めないでほしい。星は欲しいのでください。

関数型界隈ではお馴染み(?)のパーサーコンビネーターと同じ仕組みで動く。バイナリデータは言語などと違って記述にあいまいさがないので「パーサー」ではなく「デコーダー」としている。これを使うと次のように簡単にかける。

wave : Decoder Wave
wave =
  succeed Wave
    |. symbol "RIFF"
    |= uint32BE
    |. symbol "WAVE"
    |= formatChunk
    |= dataChunk

Web Audio API

現在の Elm は音声周りもサポートしていないので、Portを使って JavaScript 側で処理する。 Port は JavaScript as a Service と考えるのが良くて、クラウド上の画像処理エンジンを使うのと同じ要領で設計すればいい。

今回は MP3 を再生するだけなので大したことはしていない。はまったのは iOS で遭遇する以下の問題。

  1. ユーザーインタラクションを起点にしないと音が出ない
  2. マナーモードにしていると、直に Web Audio API を使うと音が出ない

1 に関しては、有名なハックがあるのでそれを利用。

var unlock = function() {
  var buffer = context.createBuffer(1, 1, 22050);
  var source = context.createBufferSource();
  source.buffer = buffer;
  source.connect(context.destination);
  source.start(0);
  window.removeEventListener('touchend', unlock, true);
};
window.addEventListener('touchend', unlock, true);

2 に関しては、以下の記事を参考にした。

WebAudioAPIを使っているはずなのに、マナーモードで音が出る!? - Qiita

HTML Audio、もしくはHTML Videoをページ内で1つでも使用していた場合、 そのページでは WebAudioAPI の音がマナーモード時にも鳴ってしまう

これを逆に利用して、ダミー要素を設置して音が出るようにした。

<audio src="./assets/dummy.mp3"></audio>

Twitter Card

Twitter Card の Player card を利用すると、かなり自由度の高いプレイヤーが Twitter に埋め込めるというのを Picotune@cagpie さんに教えてもらったので、作ってみた。Card Validator を使うのだが、player card の場合は承認されるまでプレビュー出来ない。知らずに何度も試したが徒労だったらしい。

GAE/GO

Twitter Card を利用するためには、 Twitter bot が読むための タグが必要。曲ごとに タグを作らないといけないので静的 HTML 配信だと無理だということに気づく。それまで GitHub Pages での配信を想定したいたのが、急遽 GAE に移動。

サーバーサイドは Go。Node だと Flexible Environment が必要で割高らしい。Java はだるいし、 Python もつまらない。Go はマルチプラットフォーム用にバイナリを吐けるので、習得しておくと何かと便利そう。

Elm アーキテクチャ

The Elm Architecture · An Introduction to Elm

Elm アーキテクチャは Redux の原型みたいなので有名になったのだが、経験知を元に常に進化を続けている。具体的に言うと、昔は「すべては階層型のコンポーネントである」的なノリで作るのは今では無駄に複雑化させるアンチパターンと見なされていて、ビューは積極的に再利用していいけど可能な限りコンポーネントに状態を持たせるのは避けるべしということになっている。

とはいえ、状態の共有が必要なコンポーネントというのは実際には存在して、今回は Twitter Card でホームページと同じ MIDI+MP3 プレイヤーを再利用した。最初はメインにべったり書いていたプレイヤーをモジュールに分離。このリファクタリング機械的に出来る。実際、大掛かりだったわりにバグが一切出ていない。

Platform.program

誤算だったのが、サーバサイドで タグを仕込もうと思ったらコンテンツ情報が Elm で書かれてたこと。何十個か JSON に書き直せばいいのだが、Elm の方が記述量が少なく書けている気もしたので、逆にElmの定義からJSONエンコードするプログラムを書いて(Platform.program) Node から呼び出して JSON ファイルに吐き出した。

普通の CSS

以前に CSS in JS(Elm) を試していて、それはそれで CSS の弱点を消せてよかったのだが、今回は別にそこまで大規模になる予定がなかったので、普通に CSS ファイルを使った。 CSS in Elm だと DevTools からコピペしたのを Elm で書き直す必要があったり、親子関係を見てスタイルを設定できないなどの弱点があった。

あとは、最近になって IE11 以外で CSS Variable が使えるようになった ので使ってみた。便利。

モバイルファースト

スマホは使うけど PC は使わないみたいな人が増えてるので真面目にやってみた。グリッドを駆使したらレスポンシブでモダンなサイトになった。

elm-format

github.com

今や準公式くらいの位置づけのフォーマッター。昔からあったのだが、npm でインストールできるようになった(npm install -g elm-format)のを機に試してみた。

事前に予想していた意図しないフォーマット崩れはほとんど起きなかった(パイプを多用するとネストが深くなる以外)ので、みんな使えばいいと思う。4 スペースインデントもまあ慣れた。

elm-live

github.com

こちらはいわゆるライブリロードのためのツール。Elm 製 Mastodon クライアントの Tooty で使われていたので、試してみた。

操作中の状態を保持したまま CSS だけを入れ替えてくれるのが嬉しい。

感想

Web 技術が色々試せて面白いし、みんなホームページ作ろう。

Elm の update 関数を綺麗に書くための Tips

生活の知恵です。

No more Task.perform identity (Task.succeed Bar)

Bad

update msg model =
  case msg of
    Foo ->
      ( model
      , Task.perform identity (Task.succeed Bar)
      )

非同期にしたせいで2回レンダリングが走る。 Lazy を使えばある程度緩和できるが、できることなら無駄な処理を避けたい。

Good

update msg model =
  case msg of
    Foo ->
      update Bar model

update 関数をそのまま呼び出せばいい。

No more model, model_, model__, model___ ...

Bad

update msg model =
  case msg of
    Foo ->
      let
        (model_, cmd) =
          updateSomething "something" model

        (model__, cmd_) =
          updateAnother "another" model_
      in
        model__ ! [ cmd, cmd_ ]

どの model がなんだったか混乱する(実際これを書きながら間違えた)。

Good

update msg model =
  case msg of
    Foo -> 
      updateSomething "something" model
        |> andThen (updateAnother "another")


andThen f (model, msg) =
  let
    (newModel, newMsg) =
      f model
  in
    newModel ! [ msg, newMsg ]

ヘルパー関数 andThen でスッキリする。

DIV ⇒ SVG 移行して気付いたこと

マップ閲覧・編集システムで画面上に DIV で描いていた図形を SVG に移行した。内部に数百のオブジェクトを含む要素の動作が重く、ドラッグしたときにカクカクになる問題を解決したかった。ちなみに Virtual DOM は使っているがすでに最適化は済んでおり、毎回描画されるような事態はあらかじめ避けている。代替案として Canvas と迷ったが CreateJS のようなライブラリを使わないとイベントハンドリングがきついので、 SVG で行けるならそれが一番。以下、移行した結果と移行中に気付いたことのまとめ。

  • 描画パフォーマンスが上がった(Firefox: 20fps ⇒ 60fps、Chrome: 50fps ⇒ 60fps)。
  • Chrome の DevTools で見ると、DIV 版は UpdateLayout と Paint に時間がかかっているが、 SVG は Layout に時間がかかっている。
  • requestAnimationFrame で測るとデータが飛び飛びだったので平滑化したところ読みやすくなった。
  • Virtual DOM も特に問題なく使える(今回は Elm の SVG ライブラリを使用)。
  • createElementNSsetAttributeNS を使わないとSVG要素として動かないので注意が必要。
  • z-indexがない。あらかじめ並び替えておく必要がある。
  • 隣の要素とボーダーが重なる。普通の DIV だと 1px のボーダーが隣と合わせて 2px になることがあった。
  • <img> の代わりに <image>を使う。 title はあるが alt はない。 SEO 的には微妙かもしれない。
  • 相対位置は <g transform="translate(x,y)"> を使う。xyではない。
  • align: center の代わりに text-anchor: middle と 親要素の中央の X 座標を使う。
  • alignmentBaseline<g> などに設置すると Chrome は効くが Firefox では効かないので <text> に直接つける。
  • Twemoji のような HTML を書き換えて何かするライブラリが動かなくなる。仕方がないのでいったん仮の要素を作って適用してから、SVG に移し替えた。 SVG の中に HTML を埋め込むことはできる(foreignObject要素)ようだが積極活用は避けたい。
  • viewBoxでマップの平行移動、拡大・縮小などができるが、transitionができない(頑張ってanimateで対応しても動作が重い)ので普通に CSS で位置と大きさを指定した方が良い。
  • word-break のような便利なものはない。foreignObject要素でも出来るようだが、今回は<canvas>要素のmeasureTextを使って自力で計算した。

SVG の特性上、やはり文書よりも図形っぽいものに使うのが適していると思う。パフォーマンスは条件によると思うが、少なくとも試す価値はある。Canvas vs SVG は良く見るが、 DIV vs SVG はあまり見なかったので速くなるケースがあることを確認できてよかった。ちなみにデータは以下。計測方法が微妙で発表できる形にまとまっていないので、雑なメモと思って欲しい。元のデータが CSS in JS(Elm) だったので、本当は普通に CSS を使った時と比較した方が良いのだが、その余裕はなかった。

Rendering Performance · Issue #31 · WorksApplications/office-maker · GitHub

複雑な UI を持つ Web アプリの実装課題を洗い出す

Virtual DOM でとりあえずレンダリング周りの問題が解決していて、 Elm を使うと型まわりも解決するのだが、その先に課題が山積してきたので、いま考えていることをメモしておく。前提としては、一般的な GUI っぽいアプリをブラウザ上で動かすことを考えている。お役立ち情報ではないので悪しからず。これで 90 日以上ブログを更新していないとかいう広告も消えるはず。

ルーティング

GUI 延長のブラウザアプリという視点では、アドレスバーは付加機能に過ぎなくて、個人的には「そこは UI じゃないからいじらないで」と言いたいのだが、やっぱり「?q=hogehoge」とかを弄る人がいて面倒だなと思っている。このパラメータ部分は組み合わせ問題で「?a=foo&b=bar」というのがあった時にどうしても「aを指定したときはbは指定するな」等という暗黙の制約があり、その時に単に無視する戦略と URL を修正するのかという問題がある。

それから、 URL をモデルあるいは UI の状態と1対1に紐づけるような設計にしていると思わぬところでハマりまくる。シリアライズ時に、「?a=foo&b=bar」の「bar」の部分だけを更新したいが構わず全部更新してしまったりする。あるいは逆に全部を更新したい場合もある。例えば、何らかのキーをしていしてページを訪れた後に何かを検索したとして、検索は一時的なものだから最初のキーが保存されててほしい場合と、その検索が重要なので新しい URL になっててほしい場合と両方ある。アドレスバーを無視すると、「このページへのリンクをコピー」ボタンというのが良くある UI で、無難な選択肢だと思う。

あとは履歴に残すか残さないか問題。「ページが変わった」と認識されるタイミングで残すのが良いのだと思うが、個人的にはほとんど使わない。というのは、1ページ中に複雑な UI を持つアプリにフォーカスしているので、ページをまたぐなら普通にブラウザ機能で遷移すればいいんじゃないかと思っている。同じ理由で「/foo/bar」という普通の URL をクライアント側でルーティングする気があまりない。サーバーサイドを巻き込むのと、クライアントサイドで複数ページの状態を一括管理するのが面倒なので。

コピペなどの一般的な GUI 機能

コピペや Undo 機能などを持つアプリはブラウザと喧嘩する。テキストフィールドをアンドゥしたつもりが、別の個所も一緒に Undo されたりする。まだ試してないが、編集対象にフォーカスを持たせると干渉しないで済むと思う。

コピー状態は、アプリ内に保持せず何らかの方法でシリアライズしてクリップボードに貼るのが良いと思う。最初はページ内で完結するコピペでも、機能追加で Excel から貼りたいとか、逆に Excel に貼りたいというのが出てくるので、その時に2か所で状態を持っているとどっちか分からなくなり、やっかいなことになる。あとは、クリップボードからペーストする時にセキュリティの都合でただ Ctrl + V をハンドルするだけではダメなので、こちらもフォーカスを解決する必要がある。

Undo については、 Flux の延長みたいなシンプルな方法が高度なアプリでは全然通用しない。モデルを全保存する方法と、アクションを保存して差分適用する方法を両方試したが、どちらにせよ他に考えることがたくさんある。例えば、連続した同じ動作はまとめて戻るとか、ある時刻まで戻るとか。あとは、過去のデータにタイムスタンプを持っていると「戻した結果をセーブする」場合に非常に混乱したので、次回やるときは賢くやりたい。

通信の最適化

画面上でデータをあれこれ編集するようなアプリだと、編集状態をサーバ側に同期したり、他の人が編集した内容をリアルタイムに反映したりという要求が出てきて、通信が大量に発生するので最適化が必要になる。通信量的には、画面全体のデータではなく差分のみを送ると削減できる。通信頻度的には、いわゆるデバウンス(またはスロットル)と呼ばれる仕組みを使う。ただ、やっぱり複雑なケースになるとシンプルなデバウンスでは通用しなくなって、「複数のリクエストを上手くマージしてから送信する」ような仕組みが必要になり、非常に面倒だった。

リアルタイム同時編集については、今のところ楽観的ロックでやっていて、自分の見ているのがいつ時点のデータなのかというのをリクエストに含めて、その間に誰かが編集してたら「駄目だったよん」という風にしている。ダメだった場合にどうするのが適切なのか、悲観的ロックの方が良いのでは?については回答を持っていない。それから、他の人が編集した結果をどの頻度で画面に反映するかも悩みどころ。あまり頻繁でも煩そうだし、通信頻度も増える。

マウスイベント制御

画面上のオブジェクトをクリックしたりドラッグしたりという事を大量にやっていると、処理が干渉してくる。例えば、「クリックした場合は処理A、ドラッグした場合は処理B、ただしドラッグ終了時にはAの処理をしたくない」というのを、 onclick と onmousedown でやっていてぐちゃぐちゃになった。最終的に ClickEmulator のような実装を作る羽目になり、過去2回のイベントからクリックを判定するとか、過去4回のイベントからダブルクリックかどうかを判定するとかをやっていてすごく面倒だった。その辺の解決策が欲しい。

あとは、ブラウザのデフォルトの挙動なのか、なぜか背景にある画像をドラッグするような挙動になることがあり、再現性なく起こるので原因が分かっていない。preventDefault / stopPropagation の組み合わせと Virtual DOM の描画タイミング問題が複雑に絡まっていそうな予感がある。