Search code examples
c#system.reactive

Get previous element in IObservable without re-evaluating the sequence


In an IObservable sequence (in Reactive Extensions for .NET), I'd like to get the value of the previous and current elements so that I can compare them. I found an example online similar to below which accomplishes the task:

sequence.Zip(sequence.Skip(1), (prev, cur) => new { Previous = prev, Current = cur })

It works fine except that it evaluates the sequence twice, which I would like to avoid. You can see that it is being evaluated twice with this code:

var debugSequence = sequence.Do(item => Debug.WriteLine("Retrieved an element from sequence"));
debugSequence.Zip(debugSequence.Skip(1), (prev, cur) => new { Previous = prev, Current = cur }).Subscribe();

The output shows twice as many of the debug lines as there are elements in the sequence.

I understand why this happens, but so far I haven't found an alternative that doesn't evaluate the sequence twice. How can I combine the previous and current with only one sequence evaluation?


Solution

  • There's a better solution to this I think, that uses Observable.Scan and avoids the double subscription:

    public static IObservable<Tuple<TSource, TSource>>
        PairWithPrevious<TSource>(this IObservable<TSource> source)
    {
        return source.Scan(
            Tuple.Create(default(TSource), default(TSource)),
            (acc, current) => Tuple.Create(acc.Item2, current));
    }
    

    I've written this up on my blog here: http://www.zerobugbuild.com/?p=213

    Addendum

    A further modification allows you to work with arbitrary types more cleanly by using a result selector:

    public static IObservable<TResult> CombineWithPrevious<TSource,TResult>(
        this IObservable<TSource> source,
        Func<TSource, TSource, TResult> resultSelector)
    {
        return source.Scan(
            Tuple.Create(default(TSource), default(TSource)),
            (previous, current) => Tuple.Create(previous.Item2, current))
            .Select(t => resultSelector(t.Item1, t.Item2));
    }