JavaScript関数型プログラミング Ch.8 非同期イベント/データのための関数型手法
- まえがき
- 非同期コードの課題
- Promiseによる第一級非同期処理
- thenメソッドチェーン
- 同期処理と非同期処理の合成
- 遅延データ生成
- RxJSによる関数型プログラミングとリアクティブプログラミング
- まとめ
まえがき
- クライアントサイドJSの責務は太古の昔よりも増えてきた
- ユーザー入力の効果的な処理
- Ajax等を介したリモートサーバーとの通信
- データの表示
- FPで高い保全性を
- 非同期処理とFPとの併用
- コールバック地獄からの脱却
非同期コードの課題
- 処理の完了を待たずに他の処理を開始する
- 例: 大量のデータfetch
- 何千件も全部読み込んで何十秒も待たせるのはNG
- 最初に見える数件だけ読み込んだら、あとは裏で読む
- 例: 大量のデータfetch
関数間の時間的な依存関係
- 時間的結合/時間的結束
- 複数の関数を論理的に1つのグループにする
- 他の処理待ち
コールバックピラミッドに陥る
- 制御の反転
- 私を呼ぶな、私が呼ぶ
- 普通、再利用可能な関数を呼び出す
- 制御が反転すると、再利用可能な関数が個別目的特化の関数を呼び出してしまう
- FPの前提を揺るがす
- 各関数が独立した存在であること
- コールバックが入れ子になると状況は悪化
- callback hell(コールバック地獄)
- pyramid of doom(破滅のピラミッド)
- インデントの形が横向きのピラミッド
継続渡しスタイルを使う
【補】継続渡しスタイル
- CPS: Continuation-Passing Style
- Continuation(継続)を引数に渡す
// 組み込みの演算子 3 * (1 + 2) // 継続渡しスタイルだとこう var add = a => b => f => f(a + b) var mul = a => b => f => f(a * b) add(1)(2)(mul(3))(result => result)
- 無名関数がネストしないのが強い
コールバック地獄のリファクタ
- 【疑問】継続渡しスタイル関係なくない??
- コールバック関数を別の関数として分離する
- 【補】p.279のコードは多分動かないぞ
handleMouseMovement
の未定義変数info
- 【補】p.279のコードは多分動かないぞ
- JSはレキシカルスコープなので、分離した関数はクロージャスコープがなくなる
- スタックフレームが小さくなり、メモリ的に有利
- 意図せぬクロージャによる不具合の回避
- 同期ループの中で非同期処理を呼ぶと起きるやつ
- 【補】p.280の例は
let
変数を使っているから問題なく動くと思うぞ
for(var i = 0; i < 3; ++i) { setTimeout(function () { console.log(i); }, 1000); } // 期待する出力: 0, 1, 2 // 実際の出力: 3, 3, 3
console.log
はクロージャスコープ(グローバルオブジェクト)のi
を見てしまっている
function asyncOpr(i) { // asyncOprのスコープにiが保存される setTimeout(function () { console.log(i); // こいつは依然としてクロージャスコープのiを見る }, 1000); } for(var i = 0; i < 3; ++i) { asyncOpr(i); } // 期待通りの出力: 0, 1, 2
- 小分けの関数のクロージャの中に状態を保存する
Promiseによる第一級非同期処理
- 継続渡しスタイルはまだ弱い
- FPとして必要なもの
- 関数合成、ポイントフリープログラミング
- 入れ子状の構造を平坦化し、逐次的な流れに
- 時間的依存関係の考え方を抽象化し、依存関係を考慮しなくてもよいようにする
- エラー処理を各コールバックに実装するのではなく、コードの邪魔にならないように単一の関数に統合する
- Promiseモナド
- 「マッピングされた関数を実行する前に、時間がかかる処理の完了を待つ」ことを、文脈が知っている
// JSONを取得する var getJSON = function (url) { return new Promise( (resolve, reject) => { // 時間のかかる非同期処理 setTimeout( function () { // fetchできた値でPromiseを解決する // thenにはこの値が渡される resolve(JSON.stringify({url: url, name: 'hoge'})); }, 1000 // // 取得に1秒かかる ); } ) } // 解決された値を処理するコールバックをthenで渡す // 入れ子にならない getJSON('http://google.co.jp').then(json => console.log(json))
Promise.resolve
// 渡した値で解決されるPromiseを生成 var p1 = Promise.resolve(123); // Promiseを渡すと、そのまま返る var p2 = Promise.resolve(p1); p1 === p2; // true // thenメソッドを実装する自前オブジェクトを渡すこともできる // (thenable) // jQueryのDefferedとかがそう var thenable = { then: function (resolve) { setTimeout( function () { resolve(456) }, 1000 ); } }; var p3 = Promise.resolve(thenable); thenable === p3; // false p3.then(console.log.bind(console)); // 456
【補】ECMAScriptのPromiseは厳密にはモナドじゃない
- モナド則
- Left identity
return a >>= f = f a
- Right identity
m >>= return = m
- Associativity
(m >>= f) >>= g = (\x -> f x >>= g)
- Left identity
Promise.resolve
がreturn
に対応Promise.prototype.then
が>>=
に対応- Left identityを満たさない例
a
がPromise
の場合Promise.resolve
はPromise
のPromise
を作らない仕様のため、Promise
は1枚だけになるthen
でPromise
が引き剥がされ、生の値がf
に渡るf
はPromise
を期待しているのでTypeError
で死ぬ
【補】thenメソッドについて
Promise.prototype.then(value)
には、promiseがresolveされたときにその値が渡ってくる- 値を
return
すると、それが次のthen
で指定したコールバックに渡る - 戻り値が暗黙裡に
Promise.resolve(value)
されているような振る舞いPromise
でちょうど1枚包まれた形になる
Promise
が解決(または拒否)されて、Promise
が引き剥がされた値が
次のthen
(もしくはcatch
)に渡される
- 値を
Promise.resolve(thenable)
がthenable
に要求する
thenable.then(resolve, reject)
は別物っぽいnew Promise(func)
のfunc
と同じ仕様のもよう
thenメソッドチェーン
getJSON('/students') .then(hide('spinner') .then(R.filter(s => s.address.country == 'US')) .then(R.sortBy(R.prop('ssn'))) .then(R.map(student => { return getJSON('/grades?ssn=' + student.ssn) // Promise .then( R.compose( Math.ceil, fork(R.divide, R.sum, R.length) )) .then(grade => /* DOM書き出し */) }))
- 非同期呼び出しの詳細を隠してくれるので、コード上「待ち」を意識させない
-
then
の戻り値がPromise
で包まれる
-
- thenにより時間の概念は強調される
getJSON(url)
がgetJSON(localStorage)
になっても全く同じように動作する- リソースの位置透過性
並列化
- 各学生についてJSONをfetchして加工して表示する部分が直列で非効率
- JSONのfetchは並列化できる
Promise.all(iterable_of_promises)
Promise
の配列もしくはイテレータを受け取る- 全部解決されたら
then
のコールバックに配列が渡る - 一つでも拒否されたら、最初に拒否されたpromiseの拒否理由をもって拒否される
同期処理と非同期処理の合成
const showStudentAsync = R.compose( catchP(errorLog), then(append('#student-info')), then(csv), then(R.props(['ssn', 'firstname', 'lastname'])), chain(findStudentAsync), // MaybeがJustならこれが実行される。Justを引き剥がしてPromiseモナドに map(checkLengthSsn), lift(cleanInput) // Maybeモナド )
- 関数合成に使うために、適宜
Promise
のメソッドをラップする関数を用意するthen
,catchP
遅延データ生成
- 遅延シーケンスの話
- 無限を扱える
ジェネレータと再帰
function*
の話- 呼び出すと、iterableなiteratorオブジェクトを生成して返す
iterator.next()
を呼ぶと、値をyield
し、処理を一時停止するiterator.next()
を呼ぶたびに処理を再開・また次のyield
で一時停止するyield*
で他のジェネレータに処理を委譲できる
- 例えば、再帰を隠蔽してイテレータパターンとして抽象化できるね、という話
- 【所感】デザインパターンの話であり、関数型プログラミング関係ねえ
イテレータプロトコル
- iterable: 配列などと同じように
for ... of
で走査できるインタフェース - iterator: いわゆる「外部イテレータ」
next()
で次の値を取得できることnext()
の戻り値にも決まりがある{ done: 走査完了フラグ, value: 値 }
function*
(generator function)- iterableなiteratorオブジェクトを生成して返却する特殊な関数
【補】自前iterable/iteratorの実装例
テキストではiterableなiteratorの実装例しかなかったので、別々バージョンを自分で用意した
// iterable function MyString(string) { this.string = string; } // iterableは[Symbol.iterator]メソッドを実装すること MyString.prototype[Symbol.iterator] = function () { return new MyStringIterator(this.string); } // (external) iterator function MyStringIterator(string) { console.log('MyStringIterator instantiated'); this.string = string; this.cursor = 0; } // iteratorはnextメソッドを実装すること MyStringIterator.prototype.next = function () { if (this.cursor >= this.string.length) { return { done: true, value: '', }; } return { done: false, value: this.string.charAt(this.cursor++), }; } var mystr = new MyString('hoge'); for (let ch of mystr) { console.log(ch) } // MyStringIterator instantiated // h // o // g // e
RxJSによる関数型プログラミングとリアクティブプログラミング
- 関数型Promiseベースに似た動作
- より高次元の抽象化と、さまざまな強力な操作
オブザーバブルなシークエンスとしてのデータ
- データのプロバイダーをObservable Streamに抽象化
- ファイル読み込み
- Webサービスの呼び出し
- DBへの問い合わせ
- システムのプッシュ通知
- ユーザー入力
- コレクション
- 文字列
- iterable
Rx.Observable
モナド
関数型プログラミングとリアクティブプログラミング
Observable
はモナド- イベントは非同期処理と同様、元々FPと組み合わせるのが難しい
- 【補】制御の反転が起きてしまうため
- Observableが関数型とイベントの間の不整合を解決する
- Promiseが関数型と非同期関数の間の不整合を解決したのと同様に
RxJSとPromise
Rx.Observable.fromPromise( // データfetch等、時間のかかる処理 Promise.resolve([1,2,3]) ) // Observable [1,2,3] になるので、 // [Observable 1, Observable 2, Observable 3] にしてマージ .flatMap(Rx.Observable.from) .subscribe(console.log.bind(console))
まとめ
- Promiseは、JavaScriptプログラムの長年の悩みであったコールバック駆動設計に対する関数型ソリューションを提供する
- Promiseは、「将来的に」関数をチェーン化する機能や合成する機能を提供するものであり、時間に依存するコードによる低レベルの複雑な問題を抽象化する
- ジェネレータは、非同期コードに対して別のアプローチを取る。このアプローチは遅延イテレータによって実現されており、データが利用可能になるまで一時停止するプログラムが提供される。
- 関数型リアクティブプログラミングは、プログラムの抽象度を上げるので、イベントを論理的に独立した単位として取り扱うことに集中することができる。その結果、複雑な実装の詳細を取り扱う必要がなくなり、手元のタスクに集中することができる。