2

source1 emits A,B,C,D etc and never completes

source2 emit 1,2 and completes

I want to merge to A1, B2, C1, D2 etc

update

my initial attemp was to Zip and Repeat as suggested from Theodor however this creates a lock cause source2 generation is expensive.

Last comment from Enigmativity addresses that problem

source1.Zip(source2.ToEnumerable().ToArray().Repeat())
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Apostolis Bekiaris
  • 2,145
  • 2
  • 18
  • 21

2 Answers2

1

Since you want to repeat source2 indefinitely and you say that it is cold (in the sense that it produces the same set of values each time, and generally in the same sort of cadence) and it is expensive, we want to turn the IObservable<T> into a T[] to ensure it's computed once and only once.

var array = source2.ToEnumerable().ToArray();
var output = source1.Zip(array.Repeat(), (x, y) => (x, y));
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • maybe u can clarify a bit about the cold definition I gave and also updated my post to clarify my initial research and problems further. Thsnks for the input! – Apostolis Bekiaris Jan 01 '22 at 05:47
  • 1
    I find that hot and cold get very confused in the Rx world. I tend to look at it that an observable is cold if it inexpensively gives the same values for every subscription, it's cold. If it gives different values to different subscribers, or at different times, or at great expense then these things make the observable hotter and hotter means you have to be more thoughtful about how you use the observable. – Enigmativity Jan 01 '22 at 06:09
  • Enigmativity I think that a sequence that emits the same values to all concurrent subscribers is probably hot, not cold! – Theodor Zoulias Jan 01 '22 at 08:15
  • @TheodorZoulias - And how does that differentiation help in writing queries? – Enigmativity Jan 01 '22 at 08:17
  • Enigmativity this is a broad philosophical question that I would have a hard time answering, even if I was not restricted by the 500-chars limit per comment. :-) – Theodor Zoulias Jan 01 '22 at 08:27
  • @TheodorZoulias - Yes, and that's why I think there's such confusion with hot versus cold. – Enigmativity Jan 01 '22 at 09:08
  • Enigmativity I think that you should fix your initial comment, because it contributes to this confusion. The descriptions of hot and cold sequences are reversed! – Theodor Zoulias Jan 01 '22 at 09:18
  • Btw instead of `var array = source2.ToEnumerable().ToArray();` I would suggest `var array = await source2.ToArray();` as an improvement, to avoid blocking the current thread. – Theodor Zoulias Jan 01 '22 at 09:26
1

Assuming that the desirable marble diagram is like this:

Source1: +--------A-------B-------C--------D-------|
Source2: +----1--------------2--------|
Merged:  +--------A1---------B2-------C1---D2------|

Here is a ZipWithRepeated operator having this behavior:

static IObservable<(TFirst First, TSecond Second)> ZipWithRepeated<TFirst, TSecond>(
    this IObservable<TFirst> first, IObservable<TSecond> second)
{
    return second.Replay(replayed => first.ToAsyncEnumerable()
        .Zip(replayed.ToAsyncEnumerable().Repeat())
        .ToObservable());
}

Usage example:

var merged = source1.ZipWithRepeated(source2);

This solution requires a dependency to the System.Linq.Async and System.Interactive.Async packages, because both sequences are converted to IAsyncEnumerable<T>s before the zipping.


Alternative: Instead of relying on the Rx Replay operator for the buffering of the source2 sequence, a more efficient solution would be to do the buffering after the conversion from observable to async-enumerable. AFAICS there is no built-in support for replaying/memoizing IAsyncEnumerable<T>s in the official Rx/Ix libraries, but creating a custom Repeat operator with embedded buffering is not very difficult. Below is an alternative implementation of the ZipWithRepeated operator, which is based on this idea:

static IObservable<(TFirst First, TSecond Second)> ZipWithRepeated<TFirst, TSecond>(
    this IObservable<TFirst> first, IObservable<TSecond> second)
{
    return first.ToAsyncEnumerable()
        .Zip(second.ToAsyncEnumerable().RepeatBuffered())
        .ToObservable();
}

private async static IAsyncEnumerable<TSource> RepeatBuffered<TSource>(
    this IAsyncEnumerable<TSource> source,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    var buffer = new List<TSource>();
    await foreach (var item in source
        .WithCancellation(cancellationToken).ConfigureAwait(false))
    {
        buffer.Add(item); yield return item;
    }
    while (true) foreach (var item in buffer) yield return item;
}

This implementation does not depend on the System.Interactive.Async package, but only on the System.Linq.Async package.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I get the feeling that this operator is preferable, canoot guess exactly your motivation though. Care to alab on it a bit? – Apostolis Bekiaris Jan 01 '22 at 08:18
  • @ApostolisBekiaris the assumption is that you want to start receiving merged pairs as soon as both sequences have emitted a value, not when the source2 sequence has completed. If you are OK with waiting for the completion of the source2, then Enignativity's solution is simpler and more efficient. My suggestion is slightly inefficient because the elements of the source2 sequence are buffered two times. One time by the `Replay` operator, and another one by the `replayed.ToAsyncEnumerable()`. – Theodor Zoulias Jan 01 '22 at 08:25
  • is there a version that compiles in netstanrd2.0? – Apostolis Bekiaris Jan 01 '22 at 08:26
  • 1
    @ApostolisBekiaris the System.Interactive.Async package is also required, because it contains the `Repeat` operator for async-enumerable sequences. I just tested that it compiles in a class library with `netstandard2.0`. – Theodor Zoulias Jan 01 '22 at 08:44