TransducersでRxJSを高速化

TransducersでRxJSを高速化
こんにちは、インターン生でpairs開発メンバーの竹内です!

前回の記事のおかげか、ありがたい事に社内でのClojureの知名度が急速に広がりつつあります。

そこで今回は一旦Clojureからは離れまして、Clojureの生みの親であるRich Hickey氏が生み出したTransducersと、RxJSとTransducersの連携について書こうと思います。

RxJS(及びReactiveX)については既に様々な解説や豊富なドキュメントが存在するため割合させていただきますが、この”Transducers”はなかなか聞き慣れない言葉かと思われますので、まずはTransducersについて少しだけ説明致します。

Transducers

Transducersについて、Clojureの公式ドキュメントでは以下の通りの説明がされています。

Transducers are composable algorithmic transformations. They are independent from the context of their input and output sources and specify only the essence of the transformation in terms of an individual element. Because transducers are decoupled from input or output sources, they can be used in many different processes – collections, streams, channels, observables, etc. Transducers compose directly, without awareness of input or creation of intermediate aggregates.

噛み砕くと、Transducersは配列・ストリーム等の入力データ、または出力を受け取る他の関数等の入出力に依存しない合成関数を提供する為の機構です。1

例えば、Transducersの実装の一つであるtransducers-jsにてTransducerを宣言するコードは、以下の様になります。

// transducers-js ライブラリ
const t = require("transducers-js"); 

// 0~1を百分率へ
function toPercent(x) {
    return Math.floor(x * 100);
}

function isEven(x) {
    return x % 2 === 0;
}

// Transducerを宣言
let transducer = t.comp(t.map(toPercent), t.filter(isEven));

これにより、0~1の数字を0~100へ変換し、その内の偶数のみを抽出するTransducerが宣言出来ました。

このように、Input/Outputを一切意識せずにロジックを記述出来るのがTransducersの大きな強みですが、更にTransducersは高階関数の中間データを生成しないため、通常の高階関数を使った処理よりも高速に動作します。

試しに、同じ処理をJavaScriptの高階関数とTransducersの両方のパターンで実装してみます。

// 要素の多い配列を準備
let source = [];
for (var i = 0; i < 1000000; ++i) {
    source.push(Math.random());
}

function sum(x, y) {
    return x + y
}

// 通常の高階関数を適用
console.time("native higher-order function");

source
    .map(toPercent)
    .filter(isEven)
    .reduce(sum)

console.timeEnd("native higher-order function");

// Transducerを適用
console.time("transducers");

t.transduce(transducer, sum, 0, source);

console.timeEnd("transducers");

Transducerの適用に使用しているt.transduce関数は、謂わばtransducerの適用とreduceを同時に行うもので、

t.transduce(<Transducer>, <引数を2つ取り、結果を返す関数>, <最初の関数適用時に渡される初期値>, <配列・ストリーム等の処理対象>)

の様に適用します。

先ほどのコードを実行すると以下の出力が得られます。

native higher-order function: 946.622ms
transducers: 72.226ms

限定的な例ではありますが、Transducersを用いた方が通常の高階関数より約13倍程度早く処理出来ている事を確認出来たかと思います。

さて、Transducersの威力を確認できた所で、この素晴らしい機構をどのようにしてRxJSと連携させるかについて書いていきます。

RxJS + Transducers

RxJSには多数のOperatorがあり、その殆どはこちらのドキュメントに纏められていますが、TransducersをRxJSで用いるには、ここに記されていないOperatorであるtransduceを使います。

試しに、先程用意したTransducerを早速RxJS Observableへ適用してみましょう。

// ランダムな0~1の値を返すObservable
function randObs() {
    return  Rx.Observable.just(Math.random());
}

// 大量のランダムな0~1の値を返すObservable
let sourceObs = Rx.Observable
    .range(1, 1000000)
    .selectMany(function(x) { 
        return randObs()
    })

// Transducersを用いてsubscribeする
sourceObs
    .transduce(transducer)
    .reduce(sum)
    .subscribe(function(x) {
        console.log(x)
    });

// => 24492260 など

このコードにより、先程の配列に対してTransducersを用いた時と同様の結果が得られます。

Transduce Operatorの動作を確認できた所で、通常のOperatorを使った処理場合との性能を比較してみましょう。

// 通常のOperatorを用いた処理
console.time("rxjs");

sourceObs
    .map(toPercent)
    .filter(isEven)
    .reduce(sum)
    .subscribe(function(x) {
        // Some side effects
    });

console.timeEnd("rxjs");

// Transducersを用いた処理
console.time("transducers + rxjs");

sourceObs
    .transduce(transducer)
    .reduce(sum)
    .subscribe(function(x) {
        // Some side effects
    });

console.timeEnd("transducers + rxjs");

結果として、以下の出力が得られます。

rxjs: 2608.152ms
transducers + rxjs: 2251.535ms

先ほどの結果を見ると、通常のRxJSでの処理と比較して1.2倍程度高速した形と一見ぱっとしませんが、RxJSを使用する以上多用を避けられない高階関数の処理を高速化出来るため、それだけでも導入のメリットがあります。

また、Operatorの高階関数をTransducerとして定義すれば、Observableに関わらずその他のデータ構造にも汎用的に出来るため、Input/Outputに囚われないコードを書く事が可能になります。

まとめ

Transducersは以下の2点、大きな魅力を持っています。

  • Input/Outputに囚われずにコードを記述出来る
  • 通常の高階関数より高速に動作する

やや難解な部分もあり、Clojure以外では利用される事の少ないTransducersですが、既にJavaScriptJavaPythonRuby等多くのプラットホームにも対応しているので、是非導入して高階関数を高速化して頂ければと思います:)

それでは、また!

参考文献

※今回掲載したコードはこちらにて掲載しております。

Credit

トップ画像を作成するにあたり、以下の画像を改変・使用しております。


  1. この説明は甚だ乱暴なので、正確にはClojureの公式ドキュメントもしくは考案者による記事をご覧ください。 
  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

デザインの品質を保つpairsデザインレビュー

Angular2は「使える」フレームワークか?