Hello, Reactive Extensions
- 2009-11-24
Reactive FrameworkがReactive Extensions for .NET (Rx)として、DevLabsで公開されました。紫のうなぎアイコンが可愛い。これは(消滅してしまった)Microsoft Voltaと同様のものなのですが、開発チームが同じだからだそうです。DevLabsには他にAxumやSTM.NETなど、興味深いプロジェクトがいっぱいありますが、日本語による情報がほとんど手に入らないので手を出しづらいところがあります。RxもDevLabsに登場したことでグッと情報が増えましたが、英語ソースによるものばかりなので、私の脳みそ的には相当シンドイことになっています。英語辛いよぅ。
とはいえ、小細工しなくても.NET3.5 SP1上で動かせるのは素敵なので是非試しましょう! Silverlight Toolkitにこっそり収録版からも、かなりパワーアップしています。メソッド大増量、そしてToolkit版でバグい挙動していたのが本当にバグなのか私のやり方が悪いのか悩んでいた部分がサクッと修正されいてホッとしたり。
インストールディレクトリに置いてあるchmのヘルプを見ることも出来るし、IntelliSenseも動きますので、前に比べると触りやすくなりました。ただまあ、例ぐらいは入れてよって感じで、簡素極まりない一行説明から理解するのは、やっぱり難しい。
System.Interactive.dll
Rxには興味ないよ、という人にも大変役立ちなのがこのdll。デフォで参照設定に加えることは確定的に明らか。中のクラスはSystem.Linq.EnumerableExのみで、ようするに拡張メソッド集です。EnumerableExという名の通り、Linq.Enumerableに対する追加版となっています。基本的にはIObservableに用意されていたメソッドをIEnumerable用に持ってきたという感じです。Repeat(value)による無限リピートやReturn(value)による単体シーケンス作成、Generate(所謂Unfold)など、欲しかった生成メソッドが沢山用意されています。
更に当然のようにIEnumerableに対する拡張メソッドもたっぷり。Zip(.NET 4に搭載)やMemoize、それにDo(副作用専用のActionメソッド)、Run(ようはForEachです、そう、ForEachですよ!)が非常にうれしい。他、挙げればキリがない用途不明の拡張メソッドがテンコ盛りなので要研究ですね。
例えばフィボナッチ数列。
EnumerableEx.Generate(
new { v1 = 0, v2 = 1 }, // initialState
_ => true, // condition
a => a.v1, // resultSelector
a => new { v1 = a.v2, v2 = a.v1 + a.v2 } // iterate
)
.Take(30)
.Run(i => Console.WriteLine(i));
// conditionが無限ならこうも書ける(第二引数の戻り値がIEnumerable、だけど平たくされる?)
EnumerableEx.Generate(
new { v1 = 0, v2 = 1 }, // initial
a => EnumerableEx.Return(a.v1), // resultSelector
a => new { v1 = a.v2, v2 = a.v1 + a.v2 }); // iterate
んね、素敵。
System.CoreEx.dll/System.Threading.dll
CoreEx.dllにはAction, FuncのT16まで版(.NET 4に搭載)やUnit(voidを表す型)が主なところ。他にIEventやNotificationがありますが、これらは主にRxで使うためのものですね。Threading.dllはLazy, Task, Parallelといった、.NET 4に搭載されるメソッドの先取りといった感じのものががが。真剣に追っかけると大変なのでスルー。
System.Reactive.dll
以前のに比べるとメソッドが増えているのは当然なのですが、目立つところではSystem.Joins.Patternクラスの追加が目新しいです。何に使うのかはまだ知りません。ふむ。自分でお題を探すのも大変なので、Forumで出てる内容を幾つか紹介します。
// 100,1,2,3,....,10
Observable.StartWith(Observable.Range(1, 10), 100);
// 順番が奇妙なのは拡張メソッドとして使えるようにしたため
Observable.Range(1, 10).StartWith(100);
ConsはStartWithに改名されました。ついでに順番がちょっと変わりました。先頭に付け足すのに、第二引数というのが違和感全開なのは否めない。これは、拡張メソッドとして利用できるようにしたためでしょうね。メソッドチェインを崩さずに、先頭に値を足すことが出来るようになりました。ObservableだけではなくEnumerableにもあります。
public static IObservable<IEvent<MouseEventArgs>> GetMouseDown(this Control control)
{
return Observable.FromEvent<MouseButtonEventHandler, MouseEventArgs>(
h => (sender, e) => h(sender, e),
h => control.MouseDown += h,
h => control.MouseDown -= h);
}
h => (sender, e) => h(sender, e)っていうのが混乱しますな。第一引数のhはEventHandler<MouseEventArgs>です。ここでhをMouseButtonEventHandlerに変換します。この辺も、ActionやFuncと同じく、EventHandler<TEventArgs>だけあればいいのに、その他のゴチャゴチャしたデリゲートは消滅してしまえばいいのに、とか思わなくもないのですがしょうがない。「sender,e => h(sender,e)」は引数がobject,MouseEventArgsで戻り値がvoidのよくあるイベント用のデリゲートです。素の状態でこのラムダ式を書くと型が決まらないので動作しませんが、FromEventの型宣言時にMouseButtonEventHandlerだと明示しているので、変換出来ます。ここで変換されるので、第二、第三引数のhはMouseButtonEventHandlerになります。
メソッド探訪の第一回で警告が出る、とか書いてしまったのですが、こういう風に記述すれば警告も出ず、文字列メソッド名を使わずに利用できたようです。言われてみればなるほど、って感じなのですが、気付けなかったなあ……。
ambはLISPのambを由来として、ambiguous(不明瞭)の略。だそうです。Rxでは、例えば……
var first = Observable.Range(1, 3).Delay(300);
var second = Observable.Range(4, 3).Delay(100);
var third = Observable.Range(7, 3).Delay(200);
Observable.Amb(first, second, third).Subscribe(s => Console.WriteLine(s));
Console.ReadLine();
Delayは発火を指定ミリ秒だけ遅らせるメソッドです。では、何が表示されるでしょうか。答えは、"4,5,6"です。んじゃあthirdをDelay(0)にしたら? "7,8,9"が表示されます。なるほど、分かってきた。つまり最初に到達したものを採用する、というわけです。この例ではわざとらしくDelayを足したシーケンスを投げてみましたが、例えば幾つかのイベントを並べて、最初にイベントが発火したものを。みたいな用途が考えられなくもない。
EnumerableEx.Amb(
new[] { 1, 2, 3 }
.Select(i => i + i)
.Select(i => i + i),
new[] { 7, 8, 9 }
.Select(i => i + i))
.Run(i => Console.WriteLine(i));
IObservableは分かるとして、何故かIEnumerableにもAmbがあります。上の例は何が表示されるでしょうか?答えは、8割は14,16,18です。残り2割は4,8,12です。チェインを沢山繋いだ方が原則的には「時間がかかる」ため、チェイン数の少ない方が採用される場合が多い。ただし、内部ではThreadを立てているので、必ずしもそうなるわけじゃない。というわけで、結果は非常に不確定で不明瞭で、使い道は完全に謎。