前置き
再代入は怖いです。
タイトルに入れただけでマサカリ飛んできそうな気がします。
int x = 0;
x = 1; // さっき0だって言ったじゃん!ウソつき!
単なる変数ならまだいいですが、ユーザー入力とか絡むと最悪です。
そんな(時間)変化とかいうわけわからん物を包むためにRxなるものが現れました。素敵ですね。
カウンタなど入力値から出力値を作るのに副作用があっても、
「In(t) -> Out(t)
と、
時間の関数を受け取り、時間の関数を返しているのです。ほーら関数的」などと
屁理屈 素晴らしい発送の転換で回避できます。
しかし、私のRx力が足りなくて、せっかくRxを使っているのに再代入を使っちゃってます。
モヤッとします。
Scan()
でガンガン殺しましょう。
状態変数を宣言せずとも状態を扱ってやりましょう。
なおScan()
の内部実装とかは気にしません。いいですね?
カウンタ
こう書く人はいないと思いますが、
一応基本的なところから。
int count = 0; // <- 状態変数(こわい)
var counter = input.Do(x => count += x).Select(_ => count);
Scan()
でこう書けます
var counter = input.Scan(0, (a, x) => a + x);
状態変数を宣言、再代入せずに、「ひとつ前の状態」を扱うことが出来ます。
リセット付きカウンタ
学びたての頃はこんな感じで書いちゃってました。
bool reset; // <- 状態変数(こわい)
var counter = input.Scan((a,x) => reset ? 0 : a + x);
リセットしたければ
reset = true
にして
input
に値を流して
reset = false
にします
カッコ悪い。
どうせならリセット入力もストリームとして扱いたいところです。
扱いましょう。
reset
を特別扱いするからイカンのです。
input
とreset
もScan()
への入力と考えればいいのです。
var counter =
Observable.Merge(
input.Select(x => new {Value = x , Reset = false}),
reset.Select(_ => new {Value = 0 /* <- dummy*/ , Reset = true})
).Scan(0, (a, x) => x.Reset ? 0 : a + x.Value);
Either
型みたいなものです。
こんな感じで、Scan()
の入力と出力の型に複数の情報をもたせると
再代入を使いがちな処理を再代入無しで実装できます。
SampleLatest的なもの
source
とsampler
ストリームがあり、
sampler
の発行タイミングで、source
の最終発行値を流してみましょう。
source.Sample(sampler).CombineLatest(sampler,(x,_)=>x)
をよく使っていたんですが、なんか動作が怪しいのでScan()
で作ります。
(これの動作が怪しくないとは言い切れませんが。)
var source = new Subject<int>();
var sampler = new Subject<Unit>();
var sampleLatest
= Observable.Merge(
source.Select(x => new { Value = x, Sample = false }),
sampler.Select(_ => new { Value = 0, Sample = true }))
.Scan(
new { Now = 0, Next = 0, Sample = false },
(a, x) => x.Sample ? new { Now = a.Next, Next = a.Next, Sample = true }
: new { Now = a.Now, Next = x.Value, Sample = false })
.Where(x => x.Sample)
.Select(x => x.Now);
//てすつ
source.Subscribe(x => Console.WriteLine($"source : {x}"));
sampler.Subscribe(_ => Console.WriteLine("Sample"));
sampleLatest.Subscribe(x => Console.WriteLine($"-- sampled : {x}"));
source.OnNext(1);
source.OnNext(2);
source.OnNext(3);
sampler.OnNext(Unit.Default); // 3
source.OnNext(4);
source.OnNext(5);
sampler.OnNext(Unit.Default); // 5
sampler.OnNext(Unit.Default); // 5
sampler.OnNext(Unit.Default); // 5
source : 1
source : 2
source : 3
Sample
-- sampled : 3
source : 4
source : 5
Sample
-- sampled : 5
Sample
-- sampled : 5
Sample
-- sampled : 5
拡張メソッド化すれば使いまわせてニコニコです。
まとめ
Scan()
の入出力に複数の値をもたせたりして、「以前の状態」を上手く扱いましょう。
ただあまりやり過ぎると読みづらくてしょうがないので
関数型言語に持ち込むときやネタとしてどうぞ。