[rxjs] deferで作るステイトフルでPipeableなカスタムオペレーターの作り方


2020/08/26

前回の話題でdeferオペレーターに関して触れましたが、今回はdeferを利用して自作のRxjsオペレーターを作成してみます。

内容としては、
Custom Rxjs Operators by Exampleの記事をベースにして解説させていただいております。


モチベーション

Rxjs標準のPipeableなオペレーターはmapやfilterなど頻度が高く利用できるものから、どこで使うんだろうみたいなニッチなものまでラインナップされております。

大体はこれらを組み合わせてパイプライン処理を合成する程度で事足りるのですが、複数のプロジェクトで利用するような自家製ライブラリで共通の処理を持たせたい場合があります。

再利用性を考慮して、カスタムPipeableオペレーターを作っておくと便利です。

例えばイメージとしては、

            
            import { Observable } from 'rxjs';

const something$: Observable<any> = ... // Implement an observable

something$.pipe(
    myAwesomePipe() // Custom Pipeable
).subscribe(res => console.log(res))
        
上のケースのように、出力側に流れてくる何かが分かっているようなsomething$ストリームが存在する場合に、自作してライブラリ化しておいたmyAwesomePipe関数を用意しておきましょう。

毎回面倒な標準パイプ関数の合成を作らなくても使い回しができるのが大きな利点です。


Pipeableと純粋関数

カスタムオペレーターの作成手順の話の前に、関数型プログラミングで言うところの純粋関数(Pure function)について、ちょっとだけ触れておきます。

純粋関数を理解すると、rxjsをベースにした関数型プログラミングにももっと親近感が沸いてくる(?)気がします。

純粋関数を本当に理解する上で、集合路のような抽象数学の分野からの説明が必要なのですが、当ブログ記事程度ではとても収まりきれない内容になるので、今回は概要だけ浅くすくって要点だけまとめます。

純粋関数をjavascriptで取り扱うお話をより深堀したい方は、
関数型JavaScriptへの入門#1(純粋関数とは)の記事を参照ください。

このブログのお言葉を借りると、rxjsのパイプオペレーターを自作する上で覚えておきたい用語として、

            
            + 純粋関数:
    数学的な関数

+ 不純な関数:
    数学的でない関数

+ 参照透過性:
    関数の唯一性

+ 副作用:
    出力の外部依存性、
    もしくは関数外部への出力、
    もしくはグローバル変数の操作
        
などが出てきます。

このブログの中では何が
数学的な内容は詳しくとり上げません。

ざっくりいうと純粋関数は次のような
副作用を持たないと言うのが条件です。

            
            + 外部からの入力:
    グローバル変数やdocument.getElementByIdなどの関数の
    引数以外の入力に出力が依存している

+ 外部への出力:
    console.logなどで関数外部への出力している

+ 外部の操作:
    グローバル変数に対する代入などを操作している
        
これらを内部でやっている関数は全て不純な関数であり、純粋関数では無くなってしまいます。

純粋関数が保証される場合のプログラミングに嬉しいことがあるかと言うと、以下に挙げられます。

            
            + コード可読性の向上
+ ログやデバック機能の実装
+ エラーハンドリング
        
今回取り上げる話題であるPipeableなrxjsオペレーターには、Observableを入力し、Observableを出力する、純粋関数を保証する必要があります。

これを簡単なコード式で表すと、

            
            const customOperator = () => (source: Observable<any>) => new Observable<any>()
        
といったパターンになります。

実際はrxjs標準のオペレーターは
純粋関数であるとされています。

ですので、例えば以下のカスタムオペレーターも純粋関数です。

            
            import { map } from 'rxjs/operators';

const myPow = (n: number) => map(x => Math.pow(x, n));
        
ただこれだと、用法としてmap単体使うのと変わらないので、カスタム性が乏しく、実用性に欠きます。

これを解決するのに使えるのが、
deferオペレーターです。


deferによる複数のオペレーターの合成

前回の内容で検証したようにdeferオペレーターを利用することで、ストリーム内部の処理の同期的し、流れの前後の挙動を予期できることを保証してくれます。

この特性により、パイプ関数をチェーン合成したカスタムオペレーターが例えば以下のように作成出来ます。

            
            const statefulPipe = () => (source: Observable<any>) => defer(() => source.pipe(
    map( /* ...do something with state */ ),
    tap( /* ...do something with state */ ),
    switchMap( /* ...do something with state */ ),
    filter( /* ...do something with state */ ),
    // ...
));
        
これで、通常のrxjsオペレーターのように取り扱いできるようになります。

            
            of(1).pipe(
    stateful()
).subscribe(
    res => console.log(res)
);
        


まとめ

カスタムオペレーターを作って、自作のライブラリ化を進めておくと、入力の対象として同一の集合に属するものに対して再利用可能となり、自分のアセットとしていざと言うときに使うことが出来ます。

余裕があれば、より複雑な処理を一括処理できるカスタムオペレーターを引き出しに貯め込んでおくと、ふとした時に役立つこと請け合いです。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。