一般的な考え方は単純です。PとQをマージし、BufferWithCount(2)を使用して値のペアを取得し、ロジックに従ってペアを処理します。
P.Merge(Q).BufferWithCount(2).Select(values =>
{
var first = values[0];
var second = values[1];
if (first is P && second is P ||
first is Q && second is Q)
{
return 0;
}
if (first is P)
{
return selector(first, second);
}
else // suppose Q, P is a valid sequence as well.
{
return selector(second, first);
}
});
ここで難しいのは、PとQが異なるタイプの場合はそれらをマージしてから、Selectでそれらを区別することです。それらが同じタイプである場合、Enigmativityによって提案されたアプローチのような単純なものを使用できます。
var pqs =
(from p in ps select new { k = "p", v = p })
.Merge(from q in qs select new { k = "q", v = q });
ここで難しいのは、それらが異なるタイプである場合、それらをマージするには、たとえばData.EitherfromHaskellのような一般的なラッパータイプが必要になることです。
public abstract class Either<TLeft, TRight>
{
private Either()
{
}
public static Either<TLeft, TRight> Create(TLeft value)
{
return new Left(value);
}
public static Either<TLeft, TRight> Create(TRight value)
{
return new Right(value);
}
public abstract TResult Match<TResult>(
Func<TLeft, TResult> onLeft,
Func<TRight, TResult> onRight);
public sealed class Left : Either<TLeft, TRight>
{
public Left(TLeft value)
{
this.Value = value;
}
public TLeft Value
{
get;
private set;
}
public override TResult Match<TResult>(
Func<TLeft, TResult> onLeft,
Func<TRight, TResult> onRight)
{
return onLeft(this.Value);
}
}
public sealed class Right : Either<TLeft, TRight>
{
public Right(TRight value)
{
this.Value = value;
}
public TRight Value
{
get;
private set;
}
public override TResult Match<TResult>(
Func<TLeft, TResult> onLeft,
Func<TRight, TResult> onRight)
{
return onRight(this.Value);
}
}
}
面白いことに、System.Reactive.dllにはすでに同様のEitherクラスがありますが、残念ながらそれは内部であるため、独自の実装が必要です。これで、PとQの両方をEitherに入れて、ソリューションなしで続行できます(少し一般化したので、intだけでなく任意の結果を返すことができます)。
public static IObservable<TResult> SmartZip<TLeft, TRight, TResult>(
IObservable<TLeft> leftSource,
IObservable<TRight> rightSource,
Func<TLeft, TRight, TResult> selector)
{
return Observable
.Merge(
leftSource.Select(Either<TLeft, TRight>.Create),
rightSource.Select(Either<TLeft, TRight>.Create))
.BufferWithCount(2)
.Select(values =>
{
// this case was not covered in your question,
// but I've added it for the sake of completeness.
if (values.Count < 2)
{
return default(TResult);
}
var first = values[0];
var second = values[1];
// pattern-matching in C# is really ugly.
return first.Match(
left => second.Match(
_ => default(TResult),
right => selector(left, right)),
right => second.Match(
left => selector(left, right),
_ => default(TResult)));
});
}
そして、これはこの恐ろしい醜いものすべての小さなデモです。
private static void Main(string[] args)
{
var psource = Observable
.Generate(1, i => i < 100, i => i, i => i + 1)
.Zip(Observable.Interval(TimeSpan.FromMilliseconds(10.0)), (i, _) => i);
var qsource = Observable
.Generate(1, i => i < 100, i => (double)i * i, i => i + 1)
.Zip(Observable.Interval(TimeSpan.FromMilliseconds(30.0)), (i, _) => i);
var result = SmartZip(
psource,
qsource,
(p, q) => q / p).ToEnumerable();
foreach (var item in result)
{
Console.WriteLine(item);
}
}