連載:Reactive Extensions(Rx)入門
第2回 イベント・プログラミングとRx
河合 宜文
2012/01/06 |
|
|
●合成のためのメソッド
Rxの代表的な合成のためのメソッドを紹介していこう。
○SelectManyメソッド
SelectManyメソッドはRxでは最もよく使われるメソッドの1つだ。例えばマウス・ダウンをきっかけにしてマウス・ムーブに差し替えてしまう、など、後へ流すシーケンス自体の差し替えを可能にする(次の図はそのイメージ)。また、1つ目の非同期の結果から2つ目の非同期を起動させる、といったように、特にRxで非同期プログラミングをする際にも重宝される。
|
SelectManyメソッドのイメージ図 |
Aのシーケンスでの値によって、後続のBのシーケンスの内容が差し替えられる。このイメージ図では、Aのシーケンスの1つ目の値によってBのシーケンスで1つの値に差し替えられ、Aの2つ目の値によってBで2つの値に差し替えられ、Aの3つ目の値によってBで3つの値に差し替えられている。 |
メソッド構文でのSelectManyメソッドはクエリ構文の多重「from」に書き換えることができる。もし幾つものSelectManyメソッドを使うことがあったら、クエリ構文を使用することを検討すると、きれいなコードが書けるだろう。次のコードは、同じ内容をメソッド構文とクエリ構文で記述した例である。
// 別のObservableに差し替えるような挙動をする
// 結果:10, 10, 11, 10, 11, 12
Observable.Range(1, 3)
.SelectMany(x => Observable.Range(10, x))
.Subscribe(Console.WriteLine);
// クエリ構文を使うと、見栄えよく仕上げられることも
// { x = 1, y = 10 }
// { x = 2, y = 10 }
// { x = 2, y = 11 }
// { x = 3, y = 10 }
// { x = 3, y = 11 }
// { x = 3, y = 12 }
var query = from x in Observable.Range(1, 3)
from y in Observable.Range(10, x)
select new { x, y };
query.Subscribe(Console.WriteLine); |
' 別のObservableに差し替えるような挙動をする
' 結果:10, 10, 11, 10, 11, 12
Observable.Range(1, 3).
SelectMany(Function(x) Observable.Range(10, x)).
Subscribe(AddressOf Console.WriteLine)
' クエリ構文を使うと、見栄えよく仕上げられることも
' { x = 1, y = 10 }
' { x = 2, y = 10 }
' { x = 2, y = 11 }
' { x = 3, y = 10 }
' { x = 3, y = 11 }
' { x = 3, y = 12 }
Dim query = From x In Observable.Range(1, 3)
From y In Observable.Range(10, x)
Select New With {.x = x, .y = y}
query.Subscribe(AddressOf Console.WriteLine) |
|
SelectManyメソッドの例(上:C#、下:VB) |
「Range(1, 3)」(以降、A)シーケンスの1つ目の値(x=1)によって後続の「Range(10, x)」(以降、B)シーケンスで「10」という1つの値に差し替えられ、Aの2つ目の値(x=2)によってBで「10」「11」という2つの値に差し替えられ、Aの3つ目の値(x=3)によってBで「10」「11」「12」という3つの値に差し替えられている。 |
非同期プログラミングでの活用事例については、次回に詳しく解説する。
○Concatメソッド
Concatメソッドは2つのシーケンスを連結する。その際、1つ目のシーケンスが終了するまでは、仮に2つ目のシーケンスで値が発行されていたとしても値を無視する。次の図はそのイメージだ。
|
Concatメソッドのイメージ図(Aの末尾の四角はOnCompletedメソッドを表す) |
Aのシーケンスが終了したら、Bのシーケンスに連結する。 |
以下に単純なConcatメソッドの例を示す。
// 実行結果:1, 2, 3, -1, -1, -1
Observable.Range(1, 3)
.Concat(Observable.Repeat(-1, 3))
.Subscribe(Console.WriteLine); |
' 実行結果:1, 2, 3, -1, -1, -1
Observable.Range(1, 3).
Concat(Observable.Repeat(-1, 3)).
Subscribe(AddressOf Console.WriteLine) |
|
Concatメソッドの例(上:C#、下:VB) |
「Range(1, 3)」シーケンスが終了したら、「Repeat(-1, 3)」シーケンスに連結する。 |
2つのシーケンスだけではなく、複数(IEnumerable<IObservable<T>>オブジェクト)の連結にも対応しており、強く実行順序を規定したい場合に活用可能だ。
○Mergeメソッド
すべての値を、発行されると即座にそのまま流す(次の図はそのイメージ)。2つのみの連結ではなく、複数の連結にも対応している。幾つものコントロールに対して共通の処理をしたい、などのときに便利に使える。
|
Mergeメソッドのイメージ図 |
それぞれのシーケンスで発行されたすべての値を、即座にそのまま流す。 |
次のコードはMergeメソッドの利用例。
// Windowsフォームで4つのTextBoxコントロール全てに
// 「DragDropEffects.All」を設定する
new[] { textBox1, textBox2, textBox3, textBox4 }
.Select(x => Observable.FromEventPattern<DragEventArgs>(
x, "DragEnter"))
.Merge()
.Subscribe(x => x.EventArgs.Effect = DragDropEffects.All);
// 上のMergeメソッドは以下のコードの変形パターン。
// IEnumerable<IObservable<T>>オブジェクトでまとめてMergeすると
// コードがきれいに仕上がる
Observable.Merge(
Observable.FromEventPattern<DragEventArgs>(textBox1, "DragEnter"),
Observable.FromEventPattern<DragEventArgs>(textBox2, "DragEnter"),
Observable.FromEventPattern<DragEventArgs>(textBox3, "DragEnter"),
Observable.FromEventPattern<DragEventArgs>(textBox4, "DragEnter")
); |
' Windowsフォームで4つのTextBoxコントロール全てに
' 「DragDropEffects.All」を設定する
Dim merge = {TextBox1, TextBox2, TextBox3, TextBox4}.
Select(Function(x) Observable.FromEventPattern(Of DragEventArgs)(
x, "DragEnter")).
Merge().
Subscribe(Sub(x) x.EventArgs.Effect = DragDropEffects.All)
' 上のMergeメソッドは以下のコードの変形パターン。
' IEnumerable(Of IObservable(Of T))オブジェクトでまとめてMergeすると
' コードがきれいに仕上がる
Observable.Merge(
Observable.FromEventPattern(Of DragEventArgs)(
TextBox1, "DragEnter"),
Observable.FromEventPattern(Of DragEventArgs)(
TextBox2, "DragEnter"),
Observable.FromEventPattern(Of DragEventArgs)(
TextBox3, "DragEnter"),
Observable.FromEventPattern(Of DragEventArgs)(
TextBox4, "DragEnter")
) |
|
Mergeメソッドの例(上:C#、下:VB) |
IEnumerable<IObservable<T>>オブジェクトからのMergeというのは一見奇妙に見えるかもしれないが、LINQ to Objectsと組み合わせてIEnumerable<IObservable<T>>オブジェクトをうまく作り上げることで、コードの重複をうまく消すことができる。
○Zipメソッド
ZipメソッドはAとB、2つの値がそろったときに値を流す。片方に値が偏ったときは、2つがそろうまでキューに古い値がためられていく。次の図はそのイメージである。
|
Zipメソッドのイメージ図 |
AとBのシーケンスで、2つの値がそろったときに値を流す。 |
以下の例では、Zipメソッドの挙動をIntervalメソッド(=指定時間間隔で値を発行する)とTimestampメソッド(=通過した際の時間と値をくっつける)で確認している。
// 実行結果:
// { x = 0@2011/12/20 7:37:15 +09:00, y = 0@2011/12/20 7:37:17 +09:00, now = 2011/12/20 7:37:17 +09:00 }
// { x = 1@2011/12/20 7:37:16 +09:00, y = 1@2011/12/20 7:37:20 +09:00, now = 2011/12/20 7:37:20 +09:00 }
// { x = 2@2011/12/20 7:37:17 +09:00, y = 2@2011/12/20 7:37:23 +09:00, now = 2011/12/20 7:37:23 +09:00 }
// { x = 3@2011/12/20 7:37:18 +09:00, y = 3@2011/12/20 7:37:26 +09:00, now = 2011/12/20 7:37:26 +09:00 }
// { x = 4@2011/12/20 7:37:19 +09:00, y = 4@2011/12/20 7:37:29 +09:00, now = 2011/12/20 7:37:29 +09:00 }
// { x = 5@2011/12/20 7:37:20 +09:00, y = 5@2011/12/20 7:37:32 +09:00, now = 2011/12/20 7:37:32 +09:00 }
// { x = 6@2011/12/20 7:37:21 +09:00, y = 6@2011/12/20 7:37:35 +09:00, now = 2011/12/20 7:37:35 +09:00 }
Observable.Interval(TimeSpan.FromSeconds(1))
.Timestamp()
.Zip(Observable.Interval(TimeSpan.FromSeconds(3)).Timestamp(), (x, y) => new { x, y, now = DateTimeOffset.Now })
.Subscribe(Console.WriteLine); |
' 実行結果:
' { x = 0@2011/12/20 7:37:15 +09:00, y = 0@2011/12/20 7:37:17 +09:00, now = 2011/12/20 7:37:17 +09:00 }
' { x = 1@2011/12/20 7:37:16 +09:00, y = 1@2011/12/20 7:37:20 +09:00, now = 2011/12/20 7:37:20 +09:00 }
' { x = 2@2011/12/20 7:37:17 +09:00, y = 2@2011/12/20 7:37:23 +09:00, now = 2011/12/20 7:37:23 +09:00 }
' { x = 3@2011/12/20 7:37:18 +09:00, y = 3@2011/12/20 7:37:26 +09:00, now = 2011/12/20 7:37:26 +09:00 }
' { x = 4@2011/12/20 7:37:19 +09:00, y = 4@2011/12/20 7:37:29 +09:00, now = 2011/12/20 7:37:29 +09:00 }
' { x = 5@2011/12/20 7:37:20 +09:00, y = 5@2011/12/20 7:37:32 +09:00, now = 2011/12/20 7:37:32 +09:00 }
' { x = 6@2011/12/20 7:37:21 +09:00, y = 6@2011/12/20 7:37:35 +09:00, now = 2011/12/20 7:37:35 +09:00 }
Observable.Interval(TimeSpan.FromSeconds(1)).
Timestamp().
Zip(Observable.Interval(TimeSpan.FromSeconds(3)).Timestamp(), Function(x, y) New With {.x = x, .y = y, .now = DateTimeOffset.Now}).
Subscribe(AddressOf Console.WriteLine) |
|
Zipメソッドの例(上:C#、下:VB) |
1秒間隔で作成されるタイムスタンプ値(Aシーケンス)と、3秒間隔で作成されるタイムスタンプ値(Bシーケンス)の両方がそろったときに値をSubscribeメソッドに流す。 |
この場合、xが1秒間隔、yが3秒間隔なので、xの方が多く値が発行されるが、yの間隔に合わされて値が流されている。また、出力結果を見るとxのタイムスタンプ値は1秒間隔で古いものから順に出力されており、多く発行されたxの値はキューに入っているのが確認できる。
○CombineLatestメソッド
Zipメソッドと似ているが、これは、どちらかの値が発行された際に、最も新しい値とそろえて値を流す(次の図はそのイメージ)。例えばどちらかのイベントが発行されるたびに、結果を再計算してほしい、といった場合に有効活用できる。
|
CombineLatestメソッドのイメージ図 |
AとBのシーケンスで、どちらかの値が発行された際に2つの値がそろっていれば値を流す。ただし、各シーケンスにおける最新の値がそろえられて流される。 |
以下は、2つのチェックボックスを、チェックが切り替わるたびにチェック状態を確認し、両方がチェック状態ならば実行する、というものだ。
public static class ToggleButtonExtensions
{
// WPF/Silverlight/WP7のToggleButtonコントロール(チェックボックスなど)で
// チェック状態が変化したらIsCheckedプロパティの値を送るように合成
public static IObservable<bool> IsCheckedAsObservable(this ToggleButton button)
{
var checkedAsObservable = Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
h => (sender, e) => h(e),
h => button.Checked += h, h => button.Checked -= h);
var uncheckedAsObservable = Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(
h => (sender, e) => h(e),
h => button.Unchecked += h, h => button.Unchecked -= h);
return Observable.Merge(checkedAsObservable, uncheckedAsObservable).Select(_ => button.IsChecked.Value);
}
}
// checkBox1とcheckBox2という2つのチェックボックスが
// 両方ともチェックされていたらメッセージボックスを表示
checkBox1.IsCheckedAsObservable()
.CombineLatest(checkBox2.IsCheckedAsObservable(),
(isChecked1, isChecked2) => new { isChecked1, isChecked2 })
.Where(x => x.isChecked1 && x.isChecked2)
.Subscribe(_ => MessageBox.Show("両方チェックされている")); |
Module ToggleButtonExtensions
' WPF/Silverlight/WP7のToggleButtonコントロール(チェックボックスなど)で
' チェック状態が変化したらIsCheckedプロパティの値を送るように合成
<System.Runtime.CompilerServices.Extension()>
Function IsCheckedAsObservable(button As ToggleButton) As IObservable(Of Boolean)
Dim checkedAsObservable = Observable.FromEvent(Of RoutedEventHandler, RoutedEventArgs)(
Function(h) Sub(sender, e) h.Invoke(e),
Sub(h) AddHandler button.Checked, h, Sub(h) RemoveHandler button.Checked, h)
Dim uncheckedAsObservable = Observable.FromEvent(Of RoutedEventHandler, RoutedEventArgs)(
Function(h) Sub(sender, e) h.Invoke(e),
Sub(h) AddHandler button.Unchecked, h, Sub(h) RemoveHandler button.Unchecked, h)
Return Observable.Merge(checkedAsObservable, uncheckedAsObservable).Select(Function(x) button.IsChecked.Value)
End Function
End Module
' checkBox1とcheckBox2という2つのチェックボックスが
' 両方ともチェックされていたらメッセージボックスを表示
CheckBox1.IsCheckedAsObservable().
CombineLatest(CheckBox2.IsCheckedAsObservable(),
Function(x, y) New With {.isChecked1 = x, .isChecked2 = y}).
Where(Function(x) x.isChecked1 AndAlso x.isChecked2).
Subscribe(Sub(x) MessageBox.Show("両方チェックされている")) |
|
CombineLatestメソッドの例(上:C#、下:VB) |
「checkBox1.IsCheckedAsObservable()」と「checkBox2.IsCheckedAsObservable()」のシーケンスで、どちらかのチェック状態が変更された際に、2つの値(最新の値)がチェック状態でそろっていれば、Subscribeメソッドに値を流す。 |
しかし、この例は、こんな回りくどいことはせずに、XAMLコード上で、CheckBoxコントロールのIsCheckedプロパティにデータバインドすべきだろう。
XAMLファミリでは、データ・バインディングの積極活用によりコントロールへのID付与やイベントの利用があまりされないため、GUIプログラミングでRxを活用可能な範囲はあまり広いとはいえない。しかし、RxをXAMLファミリで有効活用するための、サードパーティ製のライブラリが幾つか出ている。1つは「ReactiveUI」で、MVVMパターンにおけるViewModelの構築をRxで支援するというものだ。もう1つは筆者が開発している「ReactiveProperty」で、プロパティ自身がバインド可能かつIObservable<T>であるReactivePropertyや、宣言的にCanExecuteの条件を記述可能なReactiveCommandによって、Viewと切り離したGUIプログラミングを支援する。
○Scanメソッド
最後に、これは連結ではなく集計ではあるが、Scanメソッドを紹介しよう。Scanメソッドは1つ前の「結果」と現在の「値」を合成して値を流す。自分自身の1つ前の結果値を見ることが可能なので、差分を計算したりなどに活用できる。次の図はそのイメージ。
|
Scanメソッドのイメージ図 |
Aのシーケンスで、1つ前の「結果」(=中央にある茶色の横線)と現在の「値」(=上にある青色の横線)を合成して値を流す。 |
Scanメソッドの挙動は、LINQ to ObjectsでのAggregateメソッドの計算結果の途中を全て列挙していることと等しい。
// 1, 3(=1+2), 6(=3+3), 10(=6+4), 15(=10+5)
Observable.Range(1, 5)
.Scan((x, y) => x + y)
.Subscribe(Console.WriteLine); |
' 1, 3(=1+2), 6(=3+3), 10(=6+4), 15(=10+5)
Observable.Range(1, 5).
Scan(Function(x, y) x + y).
Subscribe(AddressOf Console.WriteLine) |
|
Scanメソッドの例(上:C#、下:VB) |
「Range(1, 5)」のシーケンスで、1つ目の現在値が「1」で1つ前の(x+yの)結果値がないので結果値は「1」、2つ目の現在値が「2」で1つ前の結果値が「1」なので結果値は「3」、3つ目の現在値が「3」で1つ前の結果値が「3」なので結果値は「6」、という結果値がSubscribeメソッドに流される。 |
なお、LINQ to ObjectsでのAggregateメソッドと同じく、シード値を与えることも可能だ。
■
今回はReactive Extensions(Rx)の基本的な流れと、イベントでの利用の仕方、合成のためのメソッド群を説明した。次回は非同期での使い方や、時間に関するメソッド群を解説していく予定だ。