カテゴリー
【Rxjs基礎講座】GeneratorをObservableへ変換する方法
※ 当ページには【広告/PR】を含む場合があります。
2020/07/17
2022/10/05
以前の
async/await
もっとニッチなところで、
今回は
generator
Generatorの簡単なおさらい
「
javascript generator
なのでここでは深く説明しませんが、ECMAScript2015(ES6)から盛り込まれた機能で、ES7から正式に追加された
async/await
ジェネレーターは一見すると従来のイタレーターの拡張版に見えます。
しかし単なるイタレーターとは違って、遅延評価・同期処理・関数外部からの制御・無限シークエンス表現...などのテクニックを提供してくれる便利なものです。
typescriptではジェネレーターは
Generator<T>
例えば簡単な実装で遊んでみますと、
function* generator(): Generator<any> {
yield 0;
let i = 4;
yield 1;
yield 'HOGE';
yield 2 + i;
i++;
yield 'PIYO';
yield 1.0e-3 * i;
yield false;
yield 'FUGA';
yield i - 3;
yield true;
}
const iterator = generator();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
これをビルドして実行すると、
$ node dist/index.js
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 'HOGE', done: false }
{ value: 6, done: false }
{ value: 'PIYO', done: false }
{ value: 0.005, done: false }
{ value: false, done: false }
{ value: 'FUGA', done: false }
{ value: 2, done: false }
{ value: true, done: false }
{ value: undefined, done: true }
{ value: undefined, done: true } # 処理が終わってもGeneratorは消滅せずに残る
というような出力を得ます。
見てお分かりのように、ジェネレーター内部で
yield
next()
そのときの返り値は
{value: any, done: boolean}
value
done
GeneratorからObservableへの変換
それでは早速、先程のジェネレーターを元にして、Observableに変換した処理を行ってみます。
検証① ~ fromオペレーターから
別のブログ記事で以前からfromオペレーターで
実のところ、
fromオペレーター
import { Observable, from } from 'rxjs';
import { take } from 'rxjs/operators';
function* generator(): Generator<any> {
yield 0;
let i = 4;
yield 1;
yield 'HOGE';
yield 2 + i;
i++;
yield 'PIYO';
yield 1.0e-3 * i;
yield false;
yield 'FUGA';
yield i - 3;
yield true;
}
// 👇fromオペレーターからジェネレーターもObservableへ一発変換
const iterator$ = from(generator()).pipe(
take(11)
);
iterator$.subscribe(
res => console.log(res),
err => console.log(err),
() => console.log('DONE!')
);
これをビルドして実行すると、
$ node dist/index.js
0
1
HOGE
6
PIYO
0.005
false
FUGA
2
true
DONE! # Generatorの処理が終わったらtakeでUnsubscribe
このようにジェネレーターの返り値の内
value
next()
また、ジェネレーターが
done
true
complete()
検証② ~ deferオペレーターから
Fromオペレーターが使えるのなら、
defer
以下試してみましょう。
import { Observable, defer } from 'rxjs';
import { take } from 'rxjs/operators';
function* generator(): Generator<any> {
yield 0;
let i = 4;
yield 1;
yield 'HOGE';
yield 2 + i;
i++;
yield 'PIYO';
yield 1.0e-3 * i;
yield false;
yield 'FUGA';
yield i - 3;
yield true;
}
// 👇deferオペレーターからジェネレーターもObservableへ一発変換
const iterator$ = defer(generator).pipe(
take(11)
);
iterator$.subscribe(
res => console.log(res),
err => console.log(err),
() => console.log('DONE!')
);
とすることで、先程の結果と同じ結果を得ます。
fromオペレーターとの違いは、fromのほうが引数に
Generator<any>
ジェネレーター関数
応用編 ~ mergeMap(flatMap)のresultSelector(第二引数)を理解する
上記まででfromオペレーターやdeferオペレーターがGenerator型の引数をカバーしてることを示したました。
map系のオペレーターも第一引数にGenerator関数を指定することで利用可能です。
ここではmap系も種類が多いのですべてのオペレーターを個別に使用例をご紹介することができません。
丁度いい機会ですので、とりわけmap系オペレーターの持つ謎多き第二引数
resultSelector
resultSelector
関数の定義の部分だけ抜粋すると、
Projects each source value to an Observable which is merged in the output Observable.
mergeMap<T, R, O extends ObservableInput<any>>(
project: (value: T, index: number) => O,
resultSelector?: number |
((outerValue: T, innerValue: ObservedValueOf<O>, outerIndex: number, innerIndex: number) => R),
concurrent: number = Number.POSITIVE_INFINITY
): OperatorFunction<T, ObservedValueOf<O> | R>
Parameters:
project:
A function that, when applied to an item emitted by the source Observable,
returns an Observable.
resultSelector:
Optional. Default is undefined.
Type:
number |
((outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number) => R).
concurrent:
Optional.
Default is Number.POSITIVE_INFINITY.
Maximum number of input Observables being subscribed to concurrently.
Returns:
OperatorFunction<T, ObservedValueOf<O> | R>:
An Observable that emits the result of applying the projection function
(and the optional deprecated resultSelector) to each item emitted
by the source Observable and merging the results of the Observables
obtained from this transformation.
そもそも単に
mergeMap
resultSelector
もし必要に迫られ、mergeMapの複数の内部処理を可能な限りカスタマイズする場合には、
resultSelector
resultSelectorの挙動を探るため、mergeMapにジェネレーターを噛ませた以下のようなテストコードでその挙動を探ってみましょう。
import { Observable, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
function genWithMergemap$(): Observable<any> {
return of(1,5,8).pipe(
mergeMap(
(x: any, i: number) => (function* () {
yield x;
yield i;
})(),
// (outerValue: T, innerValue: ObservedValueOf, outerIndex: number, innerIndex: number)
// (outerValue) oV: 1 --> 5 --> 8 (member in array of "of")
// (innerValue) iV: return a member in array as the the generator has observed.
// (outerIndex) oI: 0 --> 1 --> 2 (index of member in the array in "of").
// (innerIndex) iI: 0 --> 1 (index that the generator rolls inside).
(oV: any, iV: any, oI: number, iI: number) => {
return `oV: ${oV}, iV: ${iV}, oI: ${oI}, iI: ${iI}`;
}
)
);
}
const source = genWithMergemap$();
source.subscribe(
x => console.log('Next: | %s |', x),
e => console.log('Error: %s', e),
() => console.log('Completed')
);
これをビルドして実行してみますと、
$ node dist/index.js
Next: | oV: 1, iV: 1, oI: 0, iI: 0 |
Next: | oV: 1, iV: 0, oI: 0, iI: 1 |
Next: | oV: 5, iV: 5, oI: 1, iI: 0 |
Next: | oV: 5, iV: 1, oI: 1, iI: 1 |
Next: | oV: 8, iV: 8, oI: 2, iI: 0 |
Next: | oV: 8, iV: 2, oI: 2, iI: 1 |
Completed
...と
resultSelector
mergeMap(flatMap)オペレーターは1つの主ストリームから、また別のサブストリーム一つと合成して、新しいストリームを生成して流す仕組みです。

※出典: Rxjs/mergeMap APIリファレンスページ
https://rxjs-dev.firebaseapp.com/api/operators/mergeMap
公式ページの説明図でいうと、
「1 --> 3 --> 5 --> ...」
「10 --> 10 --> 10 --> ...」
初学者向けの説明とはいえこの図だと、1つのストリームがもう一つ別のストリームと掛合わさって、新しい1つのストリームに合流するような印象を与えそうな気がします。
ですが実際は、前回の記事・
この図でいうと、
「1 --> 3 --> 5 --> ...」
「10 --> 10 --> 10 --> ...」
つまりは下流に流されるストリームの数は$$3 \times 3 = 9$$です。
再び上記のソースコードに目を向けると、
of
「1 --> 5 --> 8 --> ...」
今回はmergeMapの第一引数をジェネレーター関数で指定して利用します。
この場合、
yeild
これは、Innerストリームは2回相当となります。
ストリーム総数だけでいうと、$$3 \times 2 = 6$$回処理が行われています。
Innerストリーム
コードの中身を詳しく見ていきます。
まず、mergeMapの第一引数(
project
ジェネレーター指定する場合には、
(value: T, index: number) => O
ここでの
O
//....
(x: any, i: number) => (function* () {
yield x;
yield i;
})()
//...
という形になっています。
こうすると、
yield
また、
project
Innerストリーム
Outerストリーム
元となったベースのObservableから生成された流れです。
今回のコードでは
of(1,5,8)
resultSelectorのマトリックス表記
resultSelectorの4つの引数である
(outerValue: T, innerValue: ObservedValueOf<O>, outerIndex: number, innerIndex: number)
outerValue
1,5,8
outerIndex
対して
innerValue
ここでの
innerIndex
yield
yield x;
0
yield i;
1
innerValue
yield
今回でいうと
yield
x
i
では、
outerValue
Eq. (1)
outerValueからすると、innerIndexがどうあれ値としてはOuterストリームの方の値が常に参照されます。
innerValue
Eq. (2)
こちらはouterValueの時と違い、
innerIndex
0
yield x;
1
yield i;
まとめ
以上までで、mergeMapでの
resultSelector
これは
project
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー