1

I'm trying to create an Rx.NET operator that takes an Observable<string> and:

  • Forwards each element unchanged if the first element is "a"
  • Emits just a completion signal otherwise

For example:

-a-b-c-d-|- --> -a-b-c-d-|-

-b-c-d-|- --> -|-

How can I do this?

jack
  • 147
  • 1
  • 7

2 Answers2

2

Here is one way to do it:

/// <summary>
/// If the first element has the expected value, return the whole sequence.
/// Otherwise, return an empty sequence.
/// </summary>
public static IObservable<T> IfFirstElement<T>(this IObservable<T> source,
    T expectedFirstElement, IEqualityComparer<T> comparer = default)
{
    comparer ??= EqualityComparer<T>.Default;
    return source.Publish(published =>
        published
            .Where(x => !comparer.Equals(x, expectedFirstElement))
            .Take(1)
            .IgnoreElements()
            .Amb(published)
    );
}

This implementation uses the Amb operator (short for “ambiguous”), which takes two sequences and propagates the sequence that reacts first.

  1. If the first element has the desirable value, the first sequence (the published.Where+Take+IgnoreElements) does not react, so the second sequence is propagated (the published, which is the whole sequence). At this point the first sequence is unsubscribed, so the comparer.Equals method will not be invoked for subsequent elements.
  2. If the first element has not the desirable value, the first sequence emits a completion notification, which is propagated by the Amb operator, and the second sequence (the whole sequence) is ignored.

Usage example:

IObservable<string> original = new string[] { "a", "b", "c", "d" }.ToObservable();
IObservable<string> transformed = original.IfFirstElement("a");

Note: This implementation is based on the assumption that when both sequences react at the same time, the Amb operator selects consistently the first sequence. This is not mentioned in the documentation, which states only that "The Amb operator uses parallel processing to detect which sequence yields the first item". The source code is quite complex, so I can't derive this guarantee by reading it. If you want something more reliable, you could try this implementation instead:

return Observable.Create<T>(observer =>
{
    bool first = true;
    return source.Subscribe(item =>
    {
        if (first)
        {
            first = false;
            if (!comparer.Equals(item, expectedFirstElement))
            {
                observer.OnCompleted(); return;
            }
        }
        observer.OnNext(item);
    }, observer.OnError, observer.OnCompleted);
});
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I wouldn't use `Amb` like that, but it works because of the publish. I'd also avoid `Create` as it can introduce race conditions. – Enigmativity Nov 04 '21 at 08:11
  • @Enigmativity could you describe a scenario that would cause a race condition in the `Observable.Create` implementation above? – Theodor Zoulias Nov 04 '21 at 08:59
  • I just mean in general with `Create`. I just avoid it because it can block, depending on the scheduler used. I've pretty much found that using the regular operators results in observables that behave properly. – Enigmativity Nov 04 '21 at 09:40
  • @Enigmativity ah, OK. I asked because someone could read your comment, and conclude that the above implementation is flawed. – Theodor Zoulias Nov 04 '21 at 09:49
2

Here's a version that definately doesn't have a race condition:

public static IObservable<T> IfFirstElement<T>(this IObservable<T> source, T expectedFirstElement) =>
    source
        .Publish(published =>
            from x in published.Take(1)
            from y in
                x.Equals(expectedFirstElement)
                ? published.StartWith(x)
                : Observable.Empty<T>()
            select y);

There's the method syntax version:

public static IObservable<T> IfFirstElement<T>(this IObservable<T> source, T expectedFirstElement) =>
    source
        .Publish(published =>
            published
                .Take(1)
                .SelectMany(x =>
                    x.Equals(expectedFirstElement)
                    ? published.StartWith(x)
                    : Observable.Empty<T>()));

I prefer the query syntax, but hey...

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Thanks for this. Is there any chance that the output sequence will include `expectedFirstElement` twice at the start (perhaps because the `published` observable on line 7 receives the notification of the first element a bit late or something)? Or is this impossible? – jack Nov 04 '21 at 08:15
  • This looks like a good implementation. The query syntax makes it less readable IMHO, but this is just personal preference. – Theodor Zoulias Nov 04 '21 at 09:16
  • 1
    @jackdry - No, it is impossible. Since the `x` has already been taken from the `published.Take(1)` before the `published.StartWith(x)` has been subscribed to it can't happen. Even better, Rx guarantees that the next element isn't missed either if it comes immediately after the first. – Enigmativity Nov 04 '21 at 09:37
  • Thanks for the explantion. Is there anywhere I could read more about the guarantees Rx offers? Or do they not have any documenation on that? – jack Nov 04 '21 at 09:49
  • 1
    Upvoted for the method syntax version. – Theodor Zoulias Nov 04 '21 at 09:52
  • @jackdry - I'm not sure where to send you for that. There's plenty of stuff online. It's really that an observable returns zero or more values followed optionally by either a single complete or error message and no two message will overlap. – Enigmativity Nov 04 '21 at 09:56
  • 2
    @jackdry [The Observable Contract](http://reactivex.io/documentation/contract.html) contains the basic stuff, and there is also the old but good [Rx Design Guidelines](https://go.microsoft.com/fwlink/?LinkID=205219) document that goes much deeper (dowloadable PDF). And then there are the innumerable undocumented nuances that make the Rx library so difficult to learn. – Theodor Zoulias Nov 04 '21 at 10:04
  • 1
    @TheodorZoulias - ...but such a powerful tool. – Enigmativity Nov 04 '21 at 10:11