ジンジャー研究室

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

Promise でリトライや同時実行数の制御をするやつ作った

f:id:jinjor:20170917145732p:plain

JavaScript でバッチ実行を少しでも楽にしたいという思いで作ってみた。

www.npmjs.com github.com

まだバージョン 0.7.0 だけど、大体のことは出来るはず。

機能

デモ を触ると大体何ができるかわかると思います。

  • 実行間隔の指定
  • 同時実行数の制限
  • リトライ数の指定
  • リトライ間隔の調整
  • 失敗したリクエストの返却

動機

DynamoDB への書き込み時に流量を制御しようと色々していて、 Promise が不便だと思った。SDK でもなんか色々パラメータがあると後から知ったのだけど、まぁ Promise が便利になるのに越したことはないということで。もちろん既存のライブラリも沢山探したけど、しっくりくるのが無かった => 作ろう。

API に関しては、シリアライズ可能なデータとしてのリクエストの配列を受け取るようにして、失敗した時に情報を保存したり渡したりできるようにしている。リクエストの数はそんなに多くない想定なので、ストリームで読みつつ書き込むみたいな処理は今の所ない。

今後

やる気が持続すれば付くかもしれない機能。

  • バックオフのもう少し緻密な設定
  • タイムアウトの設定
  • ログ埋め込みの仕組み
  • 入力バリデーションエラー

それでは良い Promise ライフを!

Elm のパイプ |> の良さ

小ネタ。

JavaScript

[1,2,3].map(a => a + 1)

が、 Elm だと

[1,2,3]
  |> List.map (\a -> a + 1)

で、両方とも左から右に読めるからそんなに変わらないなーと思ってたんだけど、一つ違う点に気づいた。JavaScript で Promise を気持ちよく連鎖してて書いてて、いざ並列実行しようとなった時に

const promises =
  [1,2,3].map(a => a + 1).map(toPromise)
Promise.all(promises)

のように少し回りくどくなり、なぜ promises.all() と書けないのかと考えたら「配列にメソッドを追加するのが微妙だから」と気づいた(prototype 拡張で不可能ではない)。一般的に言うと既存の型に何か関数を追加できない。

Elm のパイプを使う場合、その制約はなくて

[1,2,3]
  |> List.map (\a -> a + 1)
  |> Debug.log "converted" -- List に対して Debug モジュールの関数を使う
  |> List.map toString
  |> MyListUtil.getByIndex 1 -- List に対して MyListUtil モジュールの関数を使う
  |> Debug.log "result" -- Maybe に対して Debug モジュールの関数を使う

こうしてどんどん連鎖できる。便利。

と言うのを、 JavaScript と Elm を行ったり来たりしてて気づいた。おわり。

Elm 用の CSV デコーダーを作った

Elm 界隈では構文解析するライブラリを「パーサー(Parser)」、それを Elm の型に落とし込むものを「デコーダー(Decoder)」と呼び分けることが多い。パーサーは既にあって(lovasoa/elm-csv)、デコーダーで良いものがなかったので作った。

CsvDecode - elm-csv-decode 1.0.0

使い方

elm-tools/parser インスパイアのパイプライン式。

-- CSV の各行をこの User 型にしたい
type alias User =
    { id : String
    , name : String
    , age : Int
    , mail : Maybe String
    }


-- デコーダー Decoder User を作る
userDecoder : Decoder User
userDecoder =
    succeed User
        |= field "id"
        |= field "name"
        |= int (field "age")
        |= optional (field "mail")


-- デコードしたい CSV を用意
source : String
source =
    """
id,name,age,mail
1,John Smith,20,john@example.com
2,Jane Smith,19,
"""


-- 実行
> CsvDecode.run userDecoder source
Ok
    [ { id = "1", name = "John Smith", age = 20, mail = Just "john@example.com" }
    , { id = "2", name = "Jane Smith", age = 19, mail = Nothing }
    ]

API 一覧

型を見るだけで使い方が本当に分かるのか実験。

-- Types
type Decoder a
type alias Options = { separator : String, noHeader : Bool }

-- Primitives
succeed : a -> Decoder a
fail : String -> Decoder a
field : String -> Decoder String
index : Int -> Decoder String
fieldsAfter : String -> Decoder (Dict String String)
fieldsBetween : String -> String -> Decoder (Dict String String)

-- Convertion
int : Decoder String -> Decoder Int
float : Decoder String -> Decoder Float
string : Decoder String -> Decoder String
optional : Decoder a -> Decoder (Maybe a)

-- Transform
(|=) : Decoder (a -> b) -> Decoder a -> Decoder b
map : (a -> b) -> Decoder a -> Decoder b
andThen : (a -> Decoder b) -> Decoder a -> Decoder b

-- Run
run : Decoder a -> String -> Result String (List a)
runWithOptions : Options -> Decoder a -> String -> Result String (List a)
runAll : Decoder a -> String -> (List a, List String)
runAllWithOptions : Options -> Decoder a -> String -> (List a, List String)
defaultOptions : Options

分からないと思うのでドキュメント読んでください。

苦労した点

空文字の扱い。要するに foo,,bar の2列目。特に optional を使った時に、数値だと Nothing なのに文字列だと Just "" になるのは微妙なので、デフォルトで空文字は null 扱いにして string と指定した場合のみ値が存在することにした。

ソース

割ときれいに書けたかもしれない。

elm-test は最近のアップデートでトップレベルに Test 型の関数を置くだけで勝手に実行してくれるようになった。便利。

Chrome 拡張機能「JSON-YAML Toggle」を作った

f:id:jinjor:20170722154842p:plain

chrome.google.com

これは何?

JSONYAML をページ上で切り替えるツール。最近 AWS を触っていて、設定ファイルを JSON でも YAML でも書けるんだけどサンプルがどっちか片方しかなく、コピペに不便だった。

使い方は、コンテテクストメニューで「Toggle JSON/YAML」を選ぶだけ。

f:id:jinjor:20170722154953g:plain

ソースはこちら。

github.com

似たものを作りたい方へ

「右クリックしたら画面を操作する」Chrome 拡張を作る場合、上のリポジトリを参考にすると楽。

  1. パッケージとして公開にするには ZIP 化が必要なので、package/ をそのフォルダにする。
  2. manifest.json に必要事項を書く。必要なファイルとか実行条件とか。
  3. context menu 登録用の background.js と、各ページで動作する content-script.js を用意。右クリックされたら前者から後者にメッセージが送られる。DOM は後者からのみ操作できる。
  4. アイコン用画像を3種類ほど用意する。
  5. package を zip 化して Chrome ストアにアップロードして、概要など必要事項を入力する。宣伝用の画像とスクリーンショットが追加で必要。確定すると数分後に公開される。

今回は webpack で js-yaml モジュールと一緒にバンドルしたが、リンクで読み込むのでもいけるような気がしないでもない。

過去に作ったもの

実は似たものを過去にも作っていて、こちらは「日英分割」という日本語と英語の間にスペースを挿入する Chrome 拡張。使い方は上のと同じ。

f:id:jinjor:20170722161901p:plain

chrome.google.com

github.com

JSON-YAML Toggle はここからコピーして作ったので、ほとんど調べずに2日で作れた。

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行でまとめるとこうなる。

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

以上です。