[Reactive Extensions]お題:5人揃ってゴレンジャイ!
最近 Reactive Extensions に入門した同僚に出したお題です。
ちょっとしたお遊びのつもりで出題したのですが、意外と楽しんでもらえたようです。
目次
お題
Reactive Extensionsを使って、次の処理を実装してください。
- ゴレンジャイのメンバー、「赤レンジャイ」「青レンジャイ」「黄レンジャイ」「緑レンジャイ」「桃レンジャイ」からランダムに1人選び、表示します。
- それを5回繰り返して、1人も重複していなければ成功です。「5人揃ってゴレンジャイ!」と表示して終了します。
- メンバーが重複した時点で失敗とします。1人目からやり直してください。
- 成功するまで繰り返し行います。
(元ネタはこちら)
私の回答
やり方はいろいろあると思いますが、今回は「成功するまで繰り返す」部分でRetry()を使おう、と決めて全体の実装を考えました。
using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; class Program { static void Main(string[] args) { var goren = new[] { "赤レンジャイ! ", "青レンジャイ! ", "黄レンジャイ! ", "緑レンジャイ! ", "桃レンジャイ! " }; Observable.Repeat(new Random(), 5) .Select(r => goren[r.Next(5)]) .Scan(new { Value = "", IsOK = true, Buffer = new HashSet<string>() }, (a, x) => { var IsOK = a.Buffer.Add(x); if (!IsOK) { a.Buffer.Clear(); } return new { Value = x, IsOK, Buffer = a.Buffer }; }) .SelectMany(a => a.IsOK ? Observable.Return(a.Value + " ") : Observable.Return(a.Value + " 被っとるやないかい!\n") .Concat(Observable.Throw<string>(new Exception()))) .Retry() .Subscribe( onNext: x => Console.Write(x), onCompleted: () => Console.WriteLine("5人揃ってゴレンジャイ!!")); } }
実行結果
青レンジャイ! 青レンジャイ! 被っとるやないかい! 桃レンジャイ! 黄レンジャイ! 青レンジャイ! 桃レンジャイ! 被っとるやないかい! 赤レンジャイ! 桃レンジャイ! 黄レンジャイ! 黄レンジャイ! 被っとるやないかい! 桃レンジャイ! 桃レンジャイ! 被っとるやないかい! 黄レンジャイ! 赤レンジャイ! 赤レンジャイ! 被っとるやないかい! … (中略)… 青レンジャイ! 赤レンジャイ! 緑レンジャイ! 青レンジャイ! 被っとるやないかい! 青レンジャイ! 赤レンジャイ! 黄レンジャイ! 緑レンジャイ! 青レンジャイ! 被っ とるやないかい! 桃レンジャイ! 赤レンジャイ! 緑レンジャイ! 黄レンジャイ! 緑レンジャイ! 被っ とるやないかい! 赤レンジャイ! 桃レンジャイ! 黄レンジャイ! 桃レンジャイ! 被っとるやないかい! 青レンジャイ! 緑レンジャイ! 赤レンジャイ! 青レンジャイ! 被っとるやないかい! 赤レンジャイ! 緑レンジャイ! 青レンジャイ! 黄レンジャイ! 桃レンジャイ! 5人揃ってゴレンジャイ!!
解説
Repeat() + Select()
=> ランダムに5人選ぶScan()
=> 重複チェックのために値をまとめるSelectMany()
=> 重複していたら 例外を流すRetry()
=> 例外が流れて来たら、やり直しSubscribe()-onNext
=> 流れてきた値を表示する。Subscribe()-onCompleted
=> 無事に5人分流れて来たら完了!「5人揃ってゴレンジャイ!」
重複判定
Scan()のところが少々分かりずらいでしょうか。
ここでは、毎回流れてくる値を、以下をメンバに持つ匿名型にまとめています。
string Value
: 現在の値bool IsOK
: 重複した(false)/していない(true)HashSet<String> Buffer
: 値をためておくバッファ(重複判定に使用)
HashSet<T>
は重複した値を追加できないため、既に存在する要素を追加しようとすると、追加用メソッドのAdd()
は失敗しfalse
を返してきます。
これを利用して「かぶった」/「かぶっていない」の判定を行っています。
var IsOK = Buffer.Add(x);
失敗したら例外を流す
Retry()は、Rxのストリームに例外が流れてきた場合に動作します。
失敗したら例外を発生させれば良いのですが、ストリーム上に流すためにはObservable.Throw()
メソッドを使用してIObservable<T>
にラップしてやる必要があります。
ここで、流れる値が IObservable<T>
、(つまり IObservable<IObservable<T>>
) となるので、通常の値も Observable.Return()
を使用してIObservable<T>
で流してやる必要があります。
そして、IObservable<IObservable<T>>
を IObservable<T>
に戻すために SelectMany()
が使われています。
.SelectMany(a => a.IsOK ? Observable.Return(a.Value + " ") : Observable.Return(a.Value + " 被っとるやないかい!\n") .Concat(Observable.Throw<string>(new Exception())))
(失敗時のメンバーも表示したいので、失敗時の値 と 例外 をConcat()でつなげて流すようにしています。 )
蛇足
失敗したら例外を流す処理 (SelectMany()の部分) は、拡張メソッドに切り出してThrowIfFalse()
みたいにしたらもっと分かりやすくなりそうですね。他のケースでも使えそうな気もします。
ThrowIfFalse() 拡張メソッド
という事で、作ってみました。
ThrowIfFalse 条件式がFalseを返したら例外を発行するIObservable<T> 拡張メソッド · GitHub
回答のコードを置き換えてみます。
書いている内容はほとんど変わっていませんが、やりたい事の意図がより伝わりやすくなったと思います。
using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; class Program { static void Main(string[] args) { var goren = new[] { "赤レンジャイ! ", "青レンジャイ! ", "黄レンジャイ! ", "緑レンジャイ! ", "桃レンジャイ! " }; Observable.Repeat(new Random(), 5) .Select(r => goren[r.Next(5)]) .Scan(new { Value = "", IsOK = true, Buffer = new HashSet<string>() }, (a, x) => { var IsOK = a.Buffer.Add(x); if (!IsOK) { a.Buffer.Clear(); } return new { Value = x, IsOK, Buffer = a.Buffer }; }) .ThrowIfFalse(a => a.IsOK, new Exception(), resultSelectorWhenTrue : a => a.Value + " ", resultSelectorWhenFalse : a => a.Value + " 被っとるやないかい!\n") .Retry() .Subscribe( onNext: x => Console.Write(x), onCompleted: () => Console.WriteLine("5人揃ってゴレンジャイ!!")); } }