JavaScriptのイベントドリブンプログラミング周りの登場人物を雑に整理する
自分向けのダンプであって、後半にいくほど間違いが含まれる可能性が高まります。
イベントドリブンプログラミングとは
処理の流れに関するプログラミングモデル。
書いた順番の通りに処理が流れるわけではなく、起きたイベントによって様々なところから動作が始まる。歴史としては別に新しいものではなく、メインループとシグナル、と呼んでもよい。様々な分野、時代で様々な呼び方、様々なテクニックがあるはず。(ちょっと追いきれません。悪しからず)
基本的な登場人物は
- Event Listener
- Event Handler
- ( 結果の Value )
くらいでよい。どのような Event の種類があるかは扱うプラットフォームによって変わる。
基本的には後発のものほど抽象度を上げるテクニックが増えていると考えればよい。
なぜイベントドリブンなのか
- 人類には早すぎる問題 ( スレッド )
- プラットフォームの制約
マルチスレッドで UI を制御するのはロックの問題が非常に難しいらしい。それで最近の UI 周りはスレッドではなくイベントで取り回しを行う流れになっている。
もう一つはこれは明確な根拠はないけどたぶんこうだろうという推測。JavaScript は 1995年に登場したわけだが、当時すでに恐らくこの UI をスレッドで制御するのは難しいという議論はあった。結果、Netscape ブラウザは UI を管理するスレッドを一つにしてあり、その中で JavaScript は動いていたので JavaScript はマルチスレッドを扱えない。ただし非同期の処理はやはり生まれるので、timer を用意してイベントドリブンの形にした。(言語自体がシンプルになるという意味もありそうな気がする。)
ということで特に UI 周りではイベントドリブンという考え方がポピュラーになっている。バックエンドもイベントドリブンで、という考え方はもちろんあって、それが Node.js や RxJava に繋がる。
cf.
- 方法: Windows フォーム コントロールのスレッド セーフな呼び出しを行う | Microsoft Docs
- The Backyard - FailedDreamOrMultiThreadingGuiTool
- multithreading - Should all event-driven frameworks be single-threaded? - Stack Overflow
- 【翻訳記事】なぜFlutterにおいてDartを使用するのか? - Qiita
Callback関数
JavaScript は Function が first class object になっており、当初から自由に closure が使えた。これで event に対して function を割り当てる 手法が一般的になる。擬似コードを示すとこんな感じ。
object.on(event, function(event) {
..
})
しかしこの方法には課題がある。
関心が混ざり、テスト、デバッグしにくい
もう一度擬似コードを示す。
object.on(event, function(event) {
..
})
この書き方には listen したい event と handle する処理の両方が密に書かれている。そして function は closure であり、名前がない。
この組み合わせはテストおよびデバッグを難しくする。
- function に独立した名前がないので独立して呼ぶことができない。したがってユニットテストを書けない1
- function に独立した名前がないので trace を追いにくい
- JavaScript の基本的な scope は function であり、外から中の状態にタッチするのは難しい
- 高性能なデバッガがないとデバッグが難しいが、デバッガ依存の開発は再現性が低い(誰でも自動化されたテストで同じ恩恵を受けられるわけではない)2
要するに callback での開発は難しい。
インターフェイスが定まっていない
callback は単に function を引数に取るということだが、この function に渡す引数の順番にはルールがないためインターフェイスがブレてしまいやすい。例えば jQuery では
$.each(array, function(index, element) {
..
})
jQuery.each() | jQuery API Documentation
$.map(array, function(element, index) {
..
})
jQuery.map() | jQuery API Documentation
のように引数のブレがある
※ さらに
$.map(function(index, element) {})
とも違う。.map() | jQuery API Documentation
jQuery のこの例は this として何を渡すと都合がよいか、また従来の構文との親和性などを考えた結果こうなったのだと思うが、このようなブレがあらゆる callback に対して存在し得る。常にリファレンスを参照し、常に挙動を確認しないと安心して使えない。
ネストが深くなる
例えば callback の中でさらに成否を分ける callback を呼ぶ必要が出てくるとネストが深くなる。jQuery で書くとこんな感じ。
$.on('click', function(ev) {
$.ajax(url, {
data: {},
error: function(error) {
..
},
success: function(data) {
..
}
}}
})
success はさらに callback function を与えることができる。
さきほど挙げた名前のない function の階層がいとも簡単に増えるのは非常にあぶない。
Promise
Promise は Future パターンの一種。パラダイムではなくデザインパターン。1977年考案。
callback の持っている問題のうち、
- 引数および戻り値のルールを決める
- ネストせずに書けるようにする
ことが Promise の最大の意義。ざっくりイメージを書くと以下のような感じ。
promise
.then(function(resolve) {
..
})
.catch(function(reject) {
..
})
これで旧来の if-then-else のように書ける。ここが最大のポイント。
ただし、Promise を定義する側は気をつけるべきポイントが残っている。
return new Promise((resolve, reject) => {
if ( !true ) {
return reject()
} else {
resolve()
}
})
大きくは上のような書き方にするのがよい。
ポイントは return reject() を先に並べていく形にしておかないと、reject すべきものが resolve として処理されてしまいやすいところ。
となると、Promise を 使っている部分 については if-then-else のように分かりやすく読めるが、Promise を 書く部分 については Promise の特徴を意識しながら読み書きする必要があり、必ずしも扱いやすいばかりではない。
しかし、ネストが深くなって扱いにくいという問題と引数の順番が固定していないという問題は解消されている。callback にまつわるすべてが解消されているわけではないが、前進している感じはする。
JavaScript では Promise が標準に入ったし Google などが積極的にライブラリで Promise を活用しているので今後は Promise を扱うのは標準と言ってよい。また仕様が言語の外で決まっているので、どの言語のエンジニアと会話しても同じ意味で会話できることの意味は大きいと思う。
参考
- Promises/A+
- Promises/A+ (日本語訳)
- javascript - Understanding the Promises/A+ specification - Stack Overflow
- JavaScript Promiseの本
async/awaitはPromiseの処理の流れではなく結果の値の方に注目するもの
Promise を受け取る部分で await を書いてあげるとこの Promise の解決を待ってくれる。これで
val = await promise
と書ける。
イベントドリブンは
- イベントが主
- それに対応して処理を書く
- 処理の結果を受け取れない(callbackなので中に処理を書かざるを得ない)
という形だったが、async/await の登場でようやく callback を使わないコードと同じように並べることができるようになった。
ただし async/await を意図通りに動作させるためにはいくつかルールがあって、かつそれを 守れていなくてもプログラムは正しく動作してしまう ので、繊細であることは否めない。
Promise.allはゼロイチだがイチはジュウやヒャクかもしれない
一度に10のPromiseを処理していたらエラーの数も10になってしまうことがよくある。これは嬉しくない。しかしPromise.allの単位で「まとめる」などはPromiseの範囲外。
Reactive Programming
- How is reactive programming different than event-driven programming? - Stack Overflow</a>
event そのものではなく event によって得られるデータに注目する。そういう意味では async/await と同じ発想だけど、
- async/await はデータを得るまでの話
- Reactive はデータの変化に注目して、そこから処理を始める
という考え方になっている。大雑把に言うと Observer パターン。
イベントそのものではなくデータの方に注目することで本当にやりたかったことに注力することができるという考え方。
もう一つは async/await ( Promise ) では一つしか解決済みの値を扱えないので、複数回データが更新されたという事実は扱えない。これをもっと汎用的にした考え方が Reactive Programming.
最近の View フレームワーク ( React とか Vue とか ) はこの考え方を DOM にいかに速く適用するかということに基本的に特化していて、それが data ( state ) と data binding になる。データには注目しているがイベントとしてはストリームを扱えるものはメジャーにはなっていないと思う。
ReactiveX
Functional Reactive Programming を一般的な Web やアプリと親和性の高いツール上で実現する恐らく一番有名なライブラリ。
event を stream として捉え、stream を加工する function を提供してくれるもの。
これによって event handler 内で条件分岐するとか、複数の event が絡むことを考慮して event handler 内で状態を扱う必要性を減らすことができる効果がある。
もとは .NET の世界から始まっているが、Netflix の RxJava を契機に他の言語にも広まっている。JavaScript の世界でも React や Vue ではなく「RxJS + 何か」という選択をする流派もある。
この辺は扱う領域によって選択は変わるだろうなぁという印象。典型的な View フレームワークは HTML がそもそもツリー構造であることを前提にしているのかコンポーネントもツリー構造が前提になっているが、View を扱うこととイベント、データを扱うことの両方の関心が混ざっていてそのどちらに比重を置くかの難しい決断を迫られている感じがする。
言い換えるとどちらの比重が大きいかがはっきりしている場合、例えば View の比重が大きい場合は View から考えればよく、イベントとデータの比重が大きい場合はイベントとデータを中心に考えればよいと思うのと、同じ発想を広く適用したいということを考えるなら、React や Vue, Angular という「フレームワーク」を Web からネイティブアプリに適用していくアプローチと「Rx なんとか + 言語」という「考え方」を両方に適用していくというアプローチがありそう。息が長いのは考え方の方だろうけど、目の前の課題をより素早く解決するのはフレームワークの方だろうなぁ。