勉強日記

チラ裏

JavaScript関数型プログラミング Ch.8 非同期イベント/データのための関数型手法

まえがき

  • クライアントサイドJSの責務は太古の昔よりも増えてきた
    • ユーザー入力の効果的な処理
    • Ajax等を介したリモートサーバーとの通信
    • データの表示
  • FPで高い保全性を
  • 非同期処理とFPとの併用
    • コールバック地獄からの脱却

非同期コードの課題

  • 処理の完了を待たずに他の処理を開始する
    • 例: 大量のデータfetch
      • 何千件も全部読み込んで何十秒も待たせるのはNG
      • 最初に見える数件だけ読み込んだら、あとは裏で読む

関数間の時間的な依存関係

  • 時間的結合/時間的結束
    • 複数の関数を論理的に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
  • 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)
  • Promise.resolvereturnに対応
  • Promise.prototype.then>>=に対応
  • Left identityを満たさない例
    • aPromiseの場合
      • Promise.resolvePromisePromiseを作らない仕様のため、Promiseは1枚だけになる
      • thenPromiseが引き剥がされ、生の値がfに渡る
      • fPromiseを期待しているので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 で走査できるインタフェース
    • [Symbol.iterator]()メソッドでiteratorを取得できること
    • こいつ自身をiteratorにするなら、return this;でいい
  • 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モナド
    • データのプロバイダーからのデータの取り出し方を文脈が知っている
    • 生成時に文脈を選択
      • Rx.Observable.fromEvent
        • システムのプッシュ通知
        • ユーザー入力
      • Rx.Observable.fromPromise
      • Rx.Observable.from
        • 文字列
        • iterable
      • 他多数

関数型プログラミングとリアクティブプログラミング

  • 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は、「将来的に」関数をチェーン化する機能や合成する機能を提供するものであり、時間に依存するコードによる低レベルの複雑な問題を抽象化する
  • ジェネレータは、非同期コードに対して別のアプローチを取る。このアプローチは遅延イテレータによって実現されており、データが利用可能になるまで一時停止するプログラムが提供される。
  • 関数型リアクティブプログラミングは、プログラムの抽象度を上げるので、イベントを論理的に独立した単位として取り扱うことに集中することができる。その結果、複雑な実装の詳細を取り扱う必要がなくなり、手元のタスクに集中することができる。