Reactive Framework (Rx) で遊んでみた

Rx とは

このごろじわじわと情報が出てきている Rx こと Reactive Framework で遊んでみました。x はどっから出てきたの?ってつっこむのがお約束らしいw
pull 型の IEnumerable/IEnumerator をひっくり返して、push 型な IObservable/IObserver を作ったそうです。だから Iterator と Subject/Observer パターンは双対の関係(ひっくり返した関係)にある!大発見!というのが現時点の話題の中心っぽいです。モナドとコモナドらしい。圏論わかりません。ひっくり返す説明は NyaRuRu さんがあげてるビデオで見れますが、文章で読みたければこちらをどうぞ。
細かいところはわからないけど、ひっくり返すとか、おもしろいこと考える世界ですねぇ。自分でも何かひっくり返してみたくなりますw

  • IObservable … 発行者。パフリッシャ。サブジェクト。観測できる。
  • IObserver … 購読者。サブスクライバ。オブザーバ。観測者。

イベントを関数型っぽく扱えるようですが、いまいちメリットがわかりません。Java に一歩歩み寄りたいんでしょうかw
.NET だと Observer パターンを簡単にした event があるので特に困ってないので、Rx はややこしい場合にでも使うのかな?例えば、ネット越しに通知するカプセル化された方法とか、サブジェクトをこねくりまわしたりとかかなぁとビデオを見ながら思ったものの、英語が聞き取れないのでよくわかりませんw

ドキュメントやサンプルを待ちながらエリックメイヤーさんのビデオを見てるのがよさそうと思ってたら、おもしろいビデオを見つけて試したくなってしまいました。

http://langnetsymposium.com/2009/talks/23-ErikMeijer-LiveLabsReactiveFramework.html

姿は見えませんが、ノリノリの声はまちがいなくエリックさんw きっとまたハデな服着てるんでしょうw
おもしろそうなのが 28:36 のマウスのコード。変なコードで二つのイベントの情報を使って一つのイベントに合成してるように見えます。副作用を扱うには IObservable.Let を使えばいいようで、メモ化するのかな?副作用があるのは Zip だと思います。

遊んでみた

Toolkit は Ms-PL ライセンスなので、配っていいよね!?というわけでデモはこちら。マウスの動いた方向を矢印で表示します。これを変なコードで書いてみました。

Visual Studio で Silverlight3 のプロジェクトを作って、http://silverlight.codeplex.com/ から Sliverlight3 用の Toolkit を落としてきて、Microsoft SDKs\Silverlight\v3.0\Toolkit\Jul09\Source\Source code\Binaries (テストに使ってる)にある System.Reactive を参照に追加したら後は遊ぶだけ。関係ないけど Toolkit の DataForm がすごい。

まずは MouseMove イベントを IObservable に。基本形 + Select の合わせ技。

var mouseMove = Observable.FromEvent<MouseEventArgs>( this, "MouseMove" )
  .Select( x => x.EventArgs.GetPosition( this ) );

Observable.FromEvent は IObservable> を返してきます。Eventイベントハンドラで受け取るいつものあれ、sender と EventArgs を一つにしただけのクラス。つまりここでの IObservable> はイベント通知をオブジェクトにしたシーケンスというイメージ。
シーケンスなの?と疑問に思うけど、シーケンスとして扱えるよと示しているのが Select。IObservable には Select などの Linq とゆかいな仲間たちメソッドが用意されてます。ここでは座標だけを取り出します。
発行者をいじる行為は event ではできないので新鮮です。

次。

var mouseDiffs = mouseMove.Let( move =>
  from diff in move.Skip( 1 ).Zip( move, ( l, r ) => new { l, r } )
  select new { dx = diff.l.X - diff.r.X, dy = diff.l.Y - diff.r.Y } );

Zip で一つ前の座標と現在の座標をまとめて、差分を取りました。イベントの合成…なのかな?よくわかりません。Let もよくわかりませんが副作用を扱うために必要らしい。Rx の話じゃないけど、このへんとか参考になるかも。きっと同じようにメモ化してると予想。mouseDiffs は IObservable< { dx, dy } >。まだ発行者のまま。

次ははしょるけど mouseDiffs を IObservable にして、最後に購読の申し込み。

direction.Subscribe( e => { ...; this.tb1.Text = str; } );

つまりイベントハンドラの登録。
イベントの登録解除をするにはこうやるようです。賢い。

var unSubscribe = direction.Subscribe( e => ... );
unSubscribe.Dispose();

イベントの世界も関数型っぽく扱えるようです。まだ何がなんだかよくわかりませんがw

以下、醤油。

MainPage.xaml.cs

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Input;

namespace RxTest
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();

            // MouseMove イベントを IObservable にする
            // Select など Linq なメソッドたちが実装してある
            var mouseMove = Observable.FromEvent<MouseEventArgs>( this, "MouseMove" )
                .Select( x => x.EventArgs.GetPosition( this ) );
            //mouseMove.Subscribe( e => Debug.WriteLine( e ) );

            // イベントの合成…なのか?
            var mouseDiffs = mouseMove.Let( move =>
                from diff in move.Skip( 1 ).Zip( move, ( l, r ) => new { l, r } )
                select new { dx = diff.l.X - diff.r.X, dy = diff.l.Y - diff.r.Y } );
            //mouseDiffs.Subscribe( e => Debug.WriteLine( e ) );

            // マウスが動いた方向を矢印で表現する
            var direction = mouseDiffs.Select( d =>
            {
                string s = ( d.dx < 0 ) ? "←"
                         : ( 0 < d.dx ) ? "→" : "";
                s += ( d.dy < 0 ) ? "↑"
                   : ( 0 < d.dy ) ? "↓" : "";
                return s;
            } );
            //direction.Subscribe( e => Debug.WriteLine( e ) );

            //Debug.WriteLine( "ID init : " + Thread.CurrentThread.ManagedThreadId );

            string str = "";
            // 購読する
            direction.Subscribe( e =>
                {
                    // この場合イベントはメインスレッドで動いているようです
                    //Debug.WriteLine( "ID event : " + Thread.CurrentThread.ManagedThreadId );

                    // 最新30行だけ表示する
                    var ss = str.Split( '\n' ).Concat( new[] { e } );
                    str = string.Join( "\n",
                        ss.SkipWhile( ( _, i ) => 30 < ss.Count() - i )
                          .ToArray() );
                    this.tb1.Text = str;
                } );
        }
    }
}

MainPage.xaml

<UserControl x:Class="RxTest.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
  <Grid x:Name="LayoutRoot">
    <TextBlock Name="tb1" TextWrapping="Wrap" TextAlignment="Center" />
  </Grid>
</UserControl>

リンク集

コードだけ載せようと思ったのに長くなった…

(追記)書いてから気づいた。
イベントをシリアル化してデータのシーケンスとして扱えるようにしたよ!ってところが本質かも。