2

I'm trying to combine two observables whose values share some key.

I want to produce a new value whenever the first observable produces a new value, combined with the latest value from a second observable which selection depends on the latest value from the first observable.

pseudo code example:

var obs1 = Observable.Interval(TimeSpan.FromSeconds(1)).Select(x => Tuple.create(SomeKeyThatVaries, x)

var obs2 = Observable.Interval(TimeSpan.FromMilliSeconds(1)).Select(x => Tuple.create(SomeKeyThatVaries, x)

from x in obs1
  let latestFromObs2WhereKeyMatches = …
  select Tuple.create(x, latestFromObs2WhereKeyMatches)

Any suggestions?

Clearly this could be implemented by subcribing to the second observable and creating a dictionary with the latest values indexable by the key. But I'm looking for a different approach..

Usage scenario: one minute price bars computed from a stream of stock quotes. In this case the key is the ticker and the dictionary contains latest ask and bid prices for concrete tickers, which are then used in the computation.

(By the way, thank you Dave and James this has been a very fruitful discussion)

(sorry about the formatting, hard to get right on an iPad..)

superjos
  • 12,189
  • 6
  • 89
  • 134
fsl
  • 831
  • 1
  • 11
  • 24
  • If you only want to produce a notification in the resulting observable whenever the "x" observable has a value, then what is the point of the "y" observable? Your question isn't clear to me. Perhaps including a marble diagram would help. – Dave Sexton Dec 16 '14 at 16:41
  • The code example is just to give a hint at what I'm trying to do.. Which I clearly haven't succeded in.. – fsl Dec 16 '14 at 17:36
  • Yea, code examples that are guesses are usually a bad idea. A specific marble diagram is way better. – Dave Sexton Dec 16 '14 at 17:37
  • Ok, I'll read up on marble diagrams.. Meanwhile I've rewrote the question.. – fsl Dec 16 '14 at 17:45
  • You need to be more specific. It doesn't necessarily have to be in the form of a diagram, but you have to at least specify the ordering of your expectations. A concrete example would help a lot - not a query, but sample input and expected output. – Dave Sexton Dec 16 '14 at 18:00
  • For example, do you mean that when the "x" observable pushes a value, the resulting observable pushes a value immediately - combined with what "y" value? There is no "y" observable yet, so "x" is just combined with some default "y" value, and then a "y" observable is started? Then, when the "x" observable generates its second value, the resulting observable pushes its second value, which is the combination of the "x" observable's second value and the first "y" observable's latest value. Then the first "y" observable is canceled. – Dave Sexton Dec 16 '14 at 18:02
  • I see from your example that what I'm trying to do with the second observable is non sensical. What I'm trying to do is use it as a cache where I can lookup the latest value using the output from the first observable as input to the lookup function.. – fsl Dec 16 '14 at 19:00
  • The question is how to compose these observables together, but I still don't understand what you expect. Unless you provide a concrete example with input and expected output, whether in the form of a marble diagram or not, I doubt anyone will be able to answer your question. – Dave Sexton Dec 16 '14 at 19:28

2 Answers2

2

...why are you looking for a different approach? Sounds like you are on the right lines to me. It's short, simple code... roughly speaking it will be something like:

var cache = new ConcurrentDictionary<long, long>();    
obs2.Subscribe(x => cache[x.Item1] = x.Item2);    
var results = obs1.Select(x => new {
    obs1 = x.Item2,
    cache.ContainsKey(x.Item1) ? cache[x.Item1] : 0
});

At the end of the day, C# is an OO language and the heavy lifting of the thread-safe mutable collections is already all done for you.

There may be fancy Rx approach (feels like joins might be involved)... but how maintainable will it be? And how will it perform?

$0.02

James World
  • 29,019
  • 9
  • 86
  • 120
  • Fundamentally I agree with you, but I'm trying to stretch the limits here. If I or no one else comes up with a fancy ;) approach I will mark this as an answer.. – fsl Dec 16 '14 at 22:08
  • 1
    This kind of answer is really unlike you ;-) I do agree that yours is the most straight-forward approach; however, it seems as though a query solution isn't all that complicated - see my answer. I think he wants a `Merge`-`GroupBy`-`Scan`. I'm thinking of it as really 2 separate queries: `Merge`-`GroupBy` is the first, to get the keys out of the way, and then `Scan` acts like a `CombineLatest` that filters for notifications from only one side. – Dave Sexton Dec 17 '14 at 00:40
  • @DaveSexton I can see why you say it's unlike me - I do often get carried away with Rx ;) - however, [not always](http://www.zerobugbuild.com/?p=337)! Here it just seemed like the pragmatic solution. With more context, there may be a case for a more FRP like solution - but I felt in this case it was definitely the simplest thing that works. Still, I am compelled to +1 your answer, because it is definitely more like me - and it's always interesting to see it done the Rx way! – James World Dec 17 '14 at 00:50
  • I agree with your blog post that `async/await` has certainly taken the place of observables with cardinality=1. I rarely use observables that only push a single value nowadays. But for everything else, I really see no better option. That's where I draw the line. Hammer away when cardinality > 1. – Dave Sexton Dec 17 '14 at 00:58
2

I'd like to know the purpose of a such a query. Would you mind describing the usage scenario a bit?

Nevertheless, it seems like the following query may solve your problem. The initial projections aren't necessary if you already have some way of identifying the origin of each value, but I've included them for the sake of generalization, to be consistent with your extremely abstract mode of questioning. ;-)

Note: I'm assuming that someKeyThatVaries is not shared data as you've shown it, which is why I've also included the term anotherKeyThatVaries; otherwise, the entire query really makes no sense to me.

var obs1 = Observable.Interval(TimeSpan.FromSeconds(1))
                     .Select(x => Tuple.Create(someKeyThatVaries, x));
var obs2 = Observable.Interval(TimeSpan.FromSeconds(.25))
                     .Select(x => Tuple.Create(anotherKeyThatVaries, x));

var results = obs1.Select(t => new { Key = t.Item1, Value = t.Item2, Kind = 1 })
                  .Merge(
              obs2.Select(t => new { Key = t.Item1, Value = t.Item2, Kind = 2 }))
                  .GroupBy(t => t.Key, t => new { t.Value, t.Kind })
                  .SelectMany(g =>
                    g.Scan(
                      new { X = -1L, Y = -1L, Yield = false },
                      (acc, cur) => cur.Kind == 1
                                  ? new { X = cur.Value, Y = acc.Y, Yield = true }
                                  : new { X = acc.X, Y = cur.Value, Yield = false })
                      .Where(s => s.Yield)
                      .Select(s => Tuple.Create(s.X, s.Y)));
Dave Sexton
  • 2,562
  • 1
  • 17
  • 26