Archive - Silverlight
.NET Reactive Framework メソッド探訪第五回:Scan
- C# Rx Framework Silverlight - 09.09/28
var random = new Random(); Func<byte> nextByte = () => (byte)random.Next(0, byte.MaxValue + 1); // CenterEllipseは真ん中の円のこと。 // 1000秒以内に3回クリックするとランダムで色が変わる // GetMultiClickScan関数が今回の主題で、解説は後で。 CenterEllipse.GetMultiClickScan(3, 1000).Subscribe(() => CenterEllipse.Fill = new SolidColorBrush(Color.FromArgb(255, nextByte(), nextByte(), nextByte())));
円を1秒以内にトリプルクリックすると色が変わります。右側のログ表示はミリセカンド単位でトリプルクリック時の経過時間を表しています。つまり、これが1000以内ならば色が変わり、1000より上ならば色は変わりません。このサンプルのテーマは、トリプルクリックの検出です。トリプルだけではなく、クアドラプルでもクインタプルでも、1秒以内じゃなくて5秒でも10秒でも応用できる形で書くとしたら、どう書く? グローバル変数に、クリックイベントを格納する配列でも置くかしらん。それがきっと簡単で分かりやすくて、でも……。
今回はRx Frameworkの関数から、Scanを取り上げます。この例題は私が考えたわけではなく、Ramblings of a Lazy Coder : My solutions to the Reactive Framework Tripple-click puzzleという問題からです。URLリンク先は解答編、というわけで、この解答を解説します。
考え方としては、クリックイベントに「クリックされた時間」という情報を付加。更に、クリック回数分の「前のデータ」を参照して指定時間内で連続クリックかどうかを確認する。時間情報の付加は匿名型を作るだけなので簡単ですが、「前のデータ」を参照するのが厄介。Linqは前にしか進まないし、送られてくるデータは過去のものなど知らない。このことはReactive Frameworkだけの話ではなく、以前にもLINQで左外部自己結合やScan?という記事で書きましたが、そこで出てくる解決策がScan。そうそう、ScanはAchiralにあるので(ということでlinq.jsにもあります)、動作は分かってます。ようするにAggregateの計算過程吐きだし版です。とりあえずlinq.js ReferenceでE.Range(1,10).Scan("x,y=>x+y")と打ってみてください(宣伝宣伝)
Scanならば「一個前」の情報が手に入る。しかし「複数個前」の情報はどうすれば? 答えは配列使えばいいぢゃない。だそうです。過程のデータをとりあえず配列に入れて、次に送り出してやれば、そりゃ簡単に取り出せますね。何だか邪道な気がしますが、気にしない気にしない。URL先の解答例では生の配列を使って、インデックスをゴニャゴニャとしていて非常に正しいとは思いますが、あまり生の配列のインデックスは扱いたくないので、Queueを使ってみました。別にQueueのClear()のコストなんてたかが知れてるっしょ(中ではArray.Clearを呼んでいて、Array.Clearは……以下略)という割り切りで。
public static IObservable<MouseButtonEventArgs> GetMultiClickScan (this UIElement element, int count, int multiClickSpeedInMilliSeconds) { return Observable.FromEvent<MouseButtonEventArgs>(element, "MouseLeftButtonDown") .Select(e => e.EventArgs) .Scan(new { Times = new Queue<DateTime>(count), Hit = false, Event = (MouseButtonEventArgs)null // ダミーなのでnullをキャストするのが楽 }, (a, e) => { var isHit = false; var now = DateTime.Now; a.Times.Enqueue(now); if (a.Times.Count == count) { var first = a.Times.Dequeue(); Debug.WriteLine((now - first).TotalMilliseconds); // Debug if ((now - first).TotalMilliseconds <= multiClickSpeedInMilliSeconds) { isHit = true; a.Times.Clear(); } } return new { Times = a.Times, Hit = isHit, Event = e }; }) .Where(a => a.Hit) .Select(a => a.Event); }
ScanはAggregateと全く同じで、accumlatorのみの実行の他に、第一引数でseedを渡すこともできます。Scanのseedで、変数と判定を行うためのフラグを保持するクラスを作り、accumlatorで判定と変数の持ち越しを行い、Whereで判定をフィルタリング。このコンボは非常に強力で、幾らでも応用が効きそう。LinqでのAggregateはほとんど使われませんが、RxにおけるScanはよく見かけることになるのではないかと思います。
で、理屈は分かったけれど、何だかゴチャゴチャとしてない? という感想は否めない。ただ、グローバル領域に変数を置く必要なくフラグを閉じ込められていること、そして、応用の効きそうな柔らかさが見えたり見えなかったりしませんか? 応用的なものも、追々考えていきたいです。
次元の狭間へ
Scanが出たので、Aggregateもついでにおさらい。これはLinq to ObjectsのAggregateと変わりません。ちなみにRx Frameworkでの内部実装はScan().Last()だったりするので、Scanとほんとーに丸っきり変わりません。じゃあ、無限リピートのFromEventにたいしてAggregateって、実行するとどうなるの?というのは気になるところですが、答えは次元の狭間に入ってしまって、その行からコードが一切進まなくなります。AggregateだけじゃなくCountやLast、ToEnumerableなど全てを列挙してから答えを返す系のメソッドは全て同じ結果になります。コンソールアプリの簡単なコードで試してみると、こうなる。
class MyClass { public event EventHandler<EventArgs> Ev; public void Fire() { Ev(this, new EventArgs()); } } static void Main(string[] args) { var mc = new MyClass(); var count = Observable.FromEvent<EventArgs>(mc, "Ev") .Count(); // ここで次元の狭間にダイブする mc.Fire(); // ここに到達することは未来永劫無い }
恐ろしや。ただ、前段階でTakeやTakeWhileを挟めば、無限リストは有限リストとなるので、面白い感じに制限が出来ます。この辺も応用例として、そのうち紹介していければと思います。
カウンター
もう一度Scanを見ます。トリプルクリックの例題は捻りすぎな感が否めないので、もっとストレートに、Scanの「前の値を保持し続けることが出来る」という点を見せる例題を一つ。クロージャの例なんかでも定番のカウンターで。
Observable.FromEvent<RoutedEventArgs>(CounterButton, "Click") .Scan(0, (x, y) => ++x) .Subscribe(i => CounterButton.Content = i + "Clicked");
とても簡潔で、変数が全て閉じ込められていて、綺麗……。
ログ吐き骨組み
- C# Rx Framework Silverlight - 09.09/16
デモ大事。Subscribeの時にConsole.WriteLine並べて、実行結果想像つきますよね、というのがいまひとつすぎたので、出力が見える骨組みを作りました。今後のReactive Frameworkの紹介時にソースコード上のDebug.WriteLineは、こーいうことなんですねー、と思ってください。毎回これ乗っけてると長ったらしいので、暗黙の、ということで。
<UserControl x:Class="SilverlightApplication4.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> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="200" /> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0"> <Button Name="ExecuteButton" Content="Execute" /> <Button Name="ErrorButton" Content="Error" /> <Button Name="ObservableButton" Content="Observable" /> <Button Name="EnumerableButton" Content="Enumerable" /> </StackPanel> <ScrollViewer Grid.Column="1"> <TextBlock Name="LogBrowseTextBlock"></TextBlock> </ScrollViewer> </Grid> </UserControl>
using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; using System.Windows.Controls.Primitives; using System.Reflection; namespace SilverlightApplication4 { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); Debug.Set(LogBrowseTextBlock, Dispatcher); ExecuteButton.GetClick().Subscribe(() => Observable.Range(1, 10).Subscribe( i => Debug.WriteLine(i), e => Debug.WriteLine(e), () => Debug.WriteLine("completed") ) ); ErrorButton.GetClick().Subscribe(() => Observable.Range(1, 10) .Do(i => { if (i == 5) throw new Exception(); }) .Subscribe( i => Debug.WriteLine(i), e => Debug.WriteLine("onError"), () => Debug.WriteLine("onCompleted") ) ); ObservableButton.GetClick().Subscribe(() => GetMethodNames(typeof(Observable)).ToList().ForEach(s => Debug.WriteLine(s)) ); EnumerableButton.GetClick().Subscribe(() => GetMethodNames(typeof(Enumerable)).ToList().ForEach(s => Debug.WriteLine(s)) ); } IEnumerable<string> GetMethodNames(Type type) { return type.GetMethods(BindingFlags.Static | BindingFlags.Public) .Select(mi => mi.Name) .OrderBy(s => s) .Distinct(); } } public static class Debug { private static TextBlock textBlock; private static Dispatcher dispatcher; public static void Set(TextBlock textBlock, Dispatcher dispatcher) { Debug.textBlock = textBlock; Debug.dispatcher = dispatcher; } public static void WriteLine(object message) { if (textBlock != null) { dispatcher.BeginInvoke(() => textBlock.Text = message.ToString() + Environment.NewLine + textBlock.Text); } } } public static class ControlExtensions { public static IObservable<Event<RoutedEventArgs>> GetClick(this ButtonBase button) { return Observable.FromEvent<RoutedEventArgs>(button, "Click"); } } }
幾ら簡易的なものだから、という言い訳がましさを考えても、かなり微妙なコードな気がする。原型はWPFでTraceListenerにTextBlockに書きだすものを追加して、Trace.WriteLineで処理していたもの。SilverlightにはTraceがなかったので、そのまま静的クラス・メソッドに置き換えて、Silverlightにも本来あるDebugを塗り替えちゃうという……。ようするにそのままコピペしても動くよね!的な感じでやりたいな、というところなわけです。ダメ?
ついでにSubscribeの中でObservableって、汚い、ように見えるかも。でも実際これはなんてことなくて、ようはJavaScriptっぽいんですよね、ほんと。DOMContentLoadedにイベント登録のaddEventListener並べるのと一緒で。じゃあ実際こうしてグチャグチャ並べるかというとそうではないようで、実際は拡張メソッドへ記述する、という形で分散していくようですが、まだまだ分からず。Microsoft側の実例やドキュメントが整ってくれないと何とも言えない感じ。
作業環境
画像クリックで原寸サイズ。最近思うところあってVisual Studioの配置をごにょごにょと弄っています。今は、こんな感じに落ち着きました。左にエラー一覧・検索など。右にソリューションエクスプローラー・クラスビュー・スタートページなど。そして左右にそれぞれコードウィンドウを分割。原則的にメインウィンドウは左。コード定義ウィンドウを右ウィンドウに開いて常時表示。もしくはXAML編集と並列したり。といったところです。コード定義ウィンドウはデカい画面で常時表示で初めて効果を発揮しますね、素晴らしく便利。
30インチ 2560×1600の無駄遣いが火を吹く!というわけですが、やっぱ広いって便利、エディタウィンドウ2面同時表示って便利、です。30インチでなくても、横2560は19インチ1280×1024のデュアルで行けます。ただ、実際はこれに加えてデバッグ時のプログラム本体なりブラウザなりを置いておく場所が欲しいので、その場合はデュアルじゃ足りないですね……。グラフィックボードが一枚でトリプルをサポートしてくれれば、というか、するべき、ですよね。ATIのEyefinityにはとても期待してます。
.NET Reactive Framework メソッド探訪第一回:FromEvent
- C# Rx Framework Silverlight - 09.09/04
まず、リアクティブフレームワークとは何ぞや、ということなのですが今のところInfoQ: .NETリアクティブフレームワーク(Rx)がLINQ to Eventsを可能にするの記事ぐらいしか情報はありません。.NET 4.0に含まれる(かもしれない)ということ、現在のところSilverlight Toolkitの単体テストのところにこっそりと配置されていること。それだけです。紹介も、記事中にもリンクされていますがunfold: Introducing Rx (Linq to Events)の一連の記事ぐらいしかありません。これの前文が中々に素敵です。
Buried deep in the bin folder of the Silverlight Toolkit Unit Tests is a hidden gem: The Rx Framework (System.Reactive.dll). If you glanced quickly you’d miss it altogether but it’s one of the most exciting additions to the .NET framework since Linq.
今のところ微妙にパッとしない(Parallelは簡単に使えるがゆえにインパクトが足らない)4.0の隠し玉はコレですね、間違いない。軽く触ってみたのですが、中々に感動的。Linq to Objects好きならば間違いなく琴線に触れます。C#3.0がコレクションの操作をforeachからLinqに変えてしまったように、.NET4.0はイベントもLinqに変わる。まさにLinq to Everywhere! Functional Reactive Programming!
How to use
Silverlight ToolkitをダウンロードしてSource/Binaries/System.Reactive.dllを頂けば完了。ただし、これはそのままだとSilverlightのプロジェクトでしか動作しないので、その他ので利用したい場合はここの記事に示されているように、githubに公開されているコードを実行(Cecilのdllが必要、記事文中にリンクされています)して変換する必要があります。今回はとりあえず、Silverlightで試してみたいと思います。こちらはこちらで、Silverlight 3 Toolsのダウンロードが必要ですけれど。
実例
マウスの動きに円が追随する、という単純なものをSilverlightで作ってみました。移動は完全に追随するのではなく、座標が15で割り切れる位置の場合のみ移動としました。スナップすることをイメージしたつもりなのですが、動きがガクガクです。これは、マウス移動に完全に追随して全ての座標でイベントが発生するわけではない = 15で割り切れる座標を通過してもイベントが発生しない場合がある = 動きがガクガク。というわけで、スナップしたい場合はWhereで間引くのではなく、Selectで近傍座標に寄せるべきです、が、いやまあ、例なので……。
// XAMLではなく全部コード上に書いたのは両方を張るのが面倒だから…… // 内容はCanvasとEllipseを配置するというもので、本筋とは関係ありません InitializeComponent(); var canvas = new Canvas { Background = new SolidColorBrush(new Color {A=255, R = 100, G = 100, B = 100 }) }; var ellipse = new Ellipse { Height = 30, Width = 30, Fill = new SolidColorBrush(Colors.Orange) }; canvas.Children.Add(ellipse); this.Content = canvas; // FromEventはイベント発火がトリガとなってLinq発動 // 後段に送られるのはEvent<T>というもので、 // SenderとEventArgsという読み取り専用プロパティを持つクラス var canvasMove = Observable.FromEvent<MouseEventArgs>(canvas, "MouseMove") .Select(e => e.EventArgs.GetPosition(canvas)) .Where(p => (p.X % 15 == 0) || (p.Y % 15 == 0)) .Subscribe(p => { ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2); ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2); }); // Subscribeの戻り値の型はIDisposable // Disposeを呼ぶと登録したイベントをデタッチすることが出来る // デタッチしないなら取得する必要は特にはない // canvasMove.Dispose();
MouseMoveでイベントが発火する度にLinqを通る。なるほど、イベントがリストに、見える。イベントを無限リスト生成として捉えることで、イベントに対してLinq操作が可能になった。ObserverパターンとIteratorパターンは同じだったんだよ!なんだってー!みたいなノリがある。もう少し丁寧に見ると、Observable.FromEventでイベントをPush型の無限リストに変換。戻り値はIObservable<T>。イベント発火時に後段に流れてくるのはEvent<T>。これは通常のイベント登録時に使うsenderとeventArgsをラップしただけの単純なもの。あとはIObservableに用意されているメソッド(Select, Where, TakeWhileなどお馴染みのものから、Delay, WaitUntilなどイベント用の目新しいメソッドなど多数)を繋げて、最後にSubscribe。このSubscribeは、つまり通常のイベント登録時のメソッド本文の役割を果たす。Linqで言ったらForEachのようなもの。Subscribeのオーバーロードも幾つかあるのですが、それはまた後日。
// つまるところ、以下のコードと同じだったりはする // ただ、IObservable<T>は通常のイベント登録では無理な複雑な操作が簡単、 // そして何よりも、このような単純なコードでもそんなに複雑になっていない! canvas.MouseMove += (sender, e) => { var pos = e.GetPosition(canvas); // Select if (!(pos.X % 15 == 0 || pos.Y % 15 == 0)) return; // Where ellipse.SetValue(Canvas.LeftProperty, pos.X - ellipse.Width / 2); ellipse.SetValue(Canvas.TopProperty, pos.Y - ellipse.Height / 2); };
通常のイベント登録と対比してみると分かりやすいかしらん。FromEventではMouseEventArgsという型を明示する必要があるのがカッタルイ。推論は偉大。が、しかし、IObservableが複雑な操作が可能なのに対し、イベントに追加では直球なものしか書けない。また、複雑な操作が可能なわりには、FromEventは驚くほどシンプルに書ける。シンプルな操作でも(記述するのに)重たくない、というのは特筆すべきことじゃあないでしょうか。
// stringを避けたこういう登録方法もあるけれど、面倒なうえに警告出る Observable.FromEvent((EventHandler<MouseEventArgs> h) => new MouseEventHandler(h), h => canvas.MouseMove += h, // addHandler h => canvas.MouseMove -= h) // removeHandler .Subscribe(e => Debug.WriteLine(e.EventArgs.GetPosition(canvas)));
ところで、イベント名をstringで書くのはどうよ、ていうかJavaScriptのaddEventHandlerみたいで嫌だよね?ね?リファクタリング効かないわ、IntelliSenseも動かないわでロクなことがない。というわけで、FromEventのオーバーロードを見ると、ちゃんと普通に登録する方法も用意されてはいる。一応、用意、されては、いる。が、しかし、あんまりだー。あんまりすぎるー。流れてくるEventhandler<MouseEventArgs>をMouseMoveが受け取ってくれないので、第一引数でMouseEventHandlerに変換する(ところで警告が消せないのですが、警告無しで処理する方法ってあるのかしらん)。あとは、addとremoveの登録。長ったらすぎてこれはダメぽ。確かに、こんなんなら、stringでいいです……。
// 普段あまり書かないMouseEventArgsとかいう型定義は書きにくいし // メソッド名もstringで書くのはミスが出がち、ということで // 拡張メソッドでイベント取り出し用のメソッドを予め作っておくと良い public static IObservable<Event<MouseEventArgs>> GetMouseMove(this UIElement elem) { return Observable.FromEvent<MouseEventArgs>(elem, "MouseMove"); } // マウスの軌跡を1秒後に描画します canvas.GetMouseMove() .Select(e => e.EventArgs.GetPosition(canvas)) .Delay(1000) .Subscribe(p =>Dispatcher.BeginInvoke(()=> { ellipse.SetValue(Canvas.LeftProperty, p.X - ellipse.Width / 2); ellipse.SetValue(Canvas.TopProperty, p.Y - ellipse.Height / 2); }));
汚い部分は隔離!ということで、拡張メソッドに退避してやると、美しく書ける。いやまあ、この辺は全部unfold: The Joy of Rx: Extension Eventsに書いてあることなのですけど。んで、デモ的にもう少し面白げがあったほうがいいかな、と思ったのでDelayを足してみました。1秒後にマウス移動の軌跡を描画します。グルグルーっとマウス動かして止めてみてください。スムーズ、とは言い難いですね、しょんぼり。記述も、Delayを足すだけ。と言いたかったんですがBeginInvokeかあ、これどーにかなる方法ないかなあ。
次回
全10回ぐらいで、全部のメソッドを紹介するつもりです。私が理解できればの話ですが。ちょこちょこと実例的なものも交えていきたいと思います。私が使いこなせればの話ですが。というわけで、次回はSubscribeのオーバーロードの紹介にしたいと思います。Reactive Frameworkならではの魅力、に関してはもう少し先になってしまいそう。少し飛ばして、非同期連結の話なんかを先に持ってきた方が良いかなあ。
