0

I'm trying to come to grips with Reactive Extensions, but so far I just don't get it. So, I figured I'd try some exercises to get started. The "Hello World!" type examples from rxkoans.codeplex.com are pretty straightforward, so that makes me happy. But then, when I want to do something more complicated, it feels like I don't get this paradigm at all yet...

I found this question closest to what I want to try: How to do I show progress when using Reactive Extensions in C#

Now, suppose I wanted some progress reporting on processing an unknown number of items in some expensive manner. Furthermore, I don't want just the last item in the calculation in my observable but I want all intermediate results. For the sake of keeping it simple at first, I just took an existing example and first got the intermediate results up and running:

    [TestMethod]
    public void TODO2()
    {
        string result = "";
        Calculate2().Subscribe(r => result += r.Result);

        Assert.AreEqual("do1do2do3do1", result);
        // PASSES
    }

    public IObservable<ResultWithProgress<string>> Calculate2()
    {
        return Observable.Create<ResultWithProgress<string>>(obs =>
        {
            Action<int, string, string> report = (pv, r, pt) =>
            {
                obs.OnNext(new ResultWithProgress<string>()
                {
                    Progress = pv,
                    Result = r,
                    ProgressText = pt,
                });
            };

            var query =
                from result1 in Observable.Start(() => Do1()) // Just returns string "do1"
                    .Do(x => report(25, x, "Completed Task1"))
                from result2 in Observable.Start(() => Do2()) // "do2"
                    .Do(x => report(50, x, "Completed Task2"))
                from result3 in Observable.Start(() => Do3()) // "do3"
                    .Do(x => report(75, x, "Completed Task3"))
                from result4 in Observable.Start(() => Do1()) // "do1" again
                select new ResultWithProgress<string>()
                {
                    Progress = 100,
                    Result = result4,
                    ProgressText = "Done!",
                };

            return query.Subscribe(obs);
        });
    }

Then, I figured I'd tackle the fact that I don't have a fixed number of elements/calculations. I started with an array of 4 elements for now, but it could be any number. I don't know beforehand, it's going to be a lazy sequence.

    [TestMethod]
    public void TODO3()
    {
        string result = "";
        Calculate3().Subscribe(r => result += r.Result);

        Assert.AreEqual("do1do2do3do1", result);
        // Doesn't compile yet, see below...
    }

    public IObservable<ResultWithProgress<string>> Calculate3()
    {
        return Observable.Create<ResultWithProgress<string>>(obs =>
        {
            Action<int, string, string> report = (pv, r, pt) =>
            {
                obs.OnNext(new ResultWithProgress<string>()
                {
                    Progress = pv,
                    Result = r,
                    ProgressText = pt,
                });
            };

            int[] bla = new int[] { 1, 2, 3, 4 };
            foreach(var b in bla)
            {
                Observable.Start(() => "BLAH" /* expensive operation here in the future */)
                    .Do(x => report(25 * b, x, "Completed Task1"));
            }

            // Completed!
            obs.OnCompleted();

            // I want an Observable that emits "BLAH" four times 
            // and then signals that it is completed
            // What do I return here? 
            return ?????; 
        });
    }

I'm sure this is just my lack of understanding of Rx, I just started with it. Could someone please help me wrap my mind around what I should be doing to get a stream of results when you don't have a fixed number of operations?

Thank you kindly.

Community
  • 1
  • 1
Diana
  • 789
  • 8
  • 34

1 Answers1

1

In the first sample they built up a query and then returned query.Subscribe(obs). That's the correct way of doing this.

In your code you are explicitly passing values into the observer. You can do this, but it's not recommended as it can create queries that block or don't behave correctly. And when something doesn't behave correctly in Rx it can be hard to fix.

The correct way to do what you want is this:

public IObservable<ResultWithProgress<string>> Calculate3()
{
    return Observable.Create<ResultWithProgress<string>>(obs =>
    {
        var query =
            from b in new int[] { 1, 2, 3, 4 }.ToObservable()
            from x in Observable.Start(() => "BLAH" /* expensive operation here in the future */)
            select new ResultWithProgress<string>()
            {
                Progress = 25 * b,
                Result = x,
                ProgressText = "Completed " + b,
            };

        return query.Subscribe(obs);
    });
}

But this is now just a little overkill so this would suffice:

public IObservable<ResultWithProgress<string>> Calculate3()
{
    return
        from b in new int[] { 1, 2, 3, 4 }.ToObservable()
        from x in Observable.Start(() => "BLAH" /* expensive operation here in the future */)
        select new ResultWithProgress<string>()
        {
            Progress = 25 * b,
            Result = x,
            ProgressText = "Completed " + b,
        };
}

Simple is best.

The only other comment is that your line Observable.Start(() => "BLAH" /* expensive operation here in the future */).Do(x => report(25 * b, x, "Completed Task1")); does nothing. Rx is like regular enumerables in that it is lazily evaluated so until there's a subscription nothing happens.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Wow, this is an entirely new way of looking at LINQ... Is this something I should get used to, doing everything with from ... select? Edit: What do you mean by "queries that block"? The LINQ variant doesn't seem so different to me, it all has to execute sometime, doesn't it? – Diana May 16 '17 at 10:41
  • @Diana - Why do you say that this is a new way of looking at LINQ? – Enigmativity May 16 '17 at 10:43
  • Well, it seems just so odd to me to *have* to consume observables with LINQ? The way I wrote it at first, with the OnNext statements feels much more natural. It seems unnecessarily convoluted... For example, I have a tree-like structure with, at some nodes, subtrees of different types. I would have four nested from-statements, but no way to return all the data, because select just returns a single object. Unless I then do a merge of the returned lists... I've reverted to simple IEnumerable with recursive functions, guess I just haven't found the real use case for Rx yet... – Diana May 17 '17 at 12:21
  • @Diana - Generally speaking you should never have to call `OnNext` - if you're doing that you're possibly doing something wrong. I think you'll find that got the concept of LINQ slightly wrong. LINQ is just a query language that can be applied to any set of objects given you implement the right methods. LINQ based on `IEnumerable` & IQueryable` is well known, but LINQ based on `IObservable` is less well known, but I wouldn't call it "consume observables with LINQ". LINQ is just a way of working with observables in the same way you would use it for enumerables. – Enigmativity May 18 '17 at 00:11
  • @Diana - I also think if you have four nested from statements that a LINQ version will be a lot cleaner than using extension methods. – Enigmativity May 18 '17 at 00:11
  • @Diana - As for RX itself - anytime that you use events, timers or tasks then Rx is a good choice to look at. It's extremely powerful and will likely save you piles of code. The code gets much smaller usually. Take a look at this [answer](http://stackoverflow.com/a/44014915/259769) - the original OP's code was huge. – Enigmativity May 18 '17 at 00:14
  • thanks for your reply. Maybe I'll post my sample sometime to ask if it can be rewritten with Rx and whether that would be useful or not. I'll keep the "events, timers or tasks" in mind. The lightbulb hasn't switched on yet, but I'll keep fiddling until I see the use case clearly. – Diana May 22 '17 at 18:22