How do you coordinate observable sequences so one starts only when the other is finished?
I have 3 observable of different types:
var obs1 = ...
var obs2 = ...
var obs2 = ...
I want to do:
obs1.Subscribe( () => obs2.Subscribe( ()=> obs3.Subscribe( () => /* Hide Progress */ )));
But this code is really ugly. Is there some operator to do it? I tried using the And() extension method but I'm not sure that this is the correct way.
Well, if you don't mind introducing TPL, you can just use await:
await obs1;
await obs2;
await obs3;
If you want to observe the values of each while still using await, just add Do:
await obs1.Do(t1 => ...);
await obs2.Do(t2 => ...);
await obs3.Do(t3 => ...);
Does this do what you want?
obs1
.Concat(obs2)
.Concat(obs3)
.Subscribe(x => /* ... */ );
Obviously this only works with cold observables. If your obs2 & obs3 are hot you might miss values.
Enigmativity is correct, though you just have to use Select also.
obs1.Select(t => new { t, (U)null, (V)null })
.Concat(
obs2.Select(u => new { (T)null, u, (V)null }))
.Concat(
obs3.Select(v => new { (T)null, (U)null, v }))
.Subscribe(either =>
{
if (either.t != null) Observe(either.t);
else if (either.u != null) Observe(either.u);
else if (either.v != null) Observe(either.v);
else { throw new Exception("Oops."); }
})
See also a related blog post of mine: The Power of T
If you are only interested in observing obs3, you may want to write like this:
obs1.TakeLast(1)
.SelectMany(x => obs2)
.TakeLast(1)
.SelectMany(y => obs3)
.Subscribe(z => ... ); // z is the same type of obs3's data type
We take the last item from obs1 and when it arrives, we use SelectMany to subscribe to and output obs2. Then we repeatedly taking the last item from the returned Observable, and when the last item arrives, we again use SelectMany to subscribe to and output obs3. Afterwards you can subscribe to the returned Observable and do the handling of obs3 as you like.
Related
I am constructing a reactive pipeline that needs to expand (SelectMany) and then flatten (in this case, ToArray) whilst maintaining access to a piece of state obtained at the beginning of the pipeline.
Here is pseudo-code for what I am attempting:
return Observable
.Start(() => this.GetSearchResults(query))
.SelectMany(results => results.Hits) // results.Hits is a list of IDs. But there is also has a bool property that I want to keep through to the end of my pipeline
.SelectMany(hit => GetById(hit.Id)) // asynchronously load each result
.ToArray() // now need to pull all the results together into a containing data structure, and also include the bool flag from above in it
.Select(resolvedResults => new ...); // need access to both resolvedResults and the bool mentioned in the first comment above
So I'm trying to find a way to cleanly access some state determined at the beginning of the pipeline from the code at the end of the pipeline.
The first thing I tried was using anonymous types to bundle the bool with each result. This quickly got out of hand and was wasteful from a performance perspective.
The second thing I tried was using a subject as follows:
var state = new AsyncSubject<bool>();
return Observable
.Start(() => this.GetSearchResults(query))
.Do(results =>
{
state.OnNext(results.Flag);
state.OnCompleted();
}
.SelectMany(results => results.Hits)
.SelectMany(hit => GetById(hit.Id))
.ToArray()
.Zip(
state,
(results, state) => new ResultContainer(state, results));
This seems to work fine, but feels a little icky to me.
So what I'm wondering is whether there is a cleaner way to manage state in a reactive pipeline.
For reference, here is the actual code (rather than just pseudo-code):
public IObservable<ISearchResults<IContact>> Search(string query, int maximumResultCount = 100, float minimumScore = 0.1F)
{
Ensure.ArgumentNotNull(query, nameof(query));
var moreHitsAvailable = new AsyncSubject<bool>();
return Observable
.Start(
() => this.searchIndexService.Search<IContact>(query, maximumResultCount, minimumScore),
this.schedulerService.DataStoreScheduler)
.Do(
results =>
{
moreHitsAvailable.OnNext(results.MoreHitsAreAvailable);
moreHitsAvailable.OnCompleted();
})
.SelectMany(
results => results
.Hits
.Select(
hit => new
{
Id = hit.Id,
ParsedId = ContactId.Parse(hit.Id)
}))
.SelectMany(
result => this
.GetById(result.ParsedId)
.Select(
contact => new
{
Id = result.Id,
Contact = contact
}))
.Do(
result =>
{
if (result.Contact == null)
{
this.logger.Warn("Failed to find contact with ID '{0}' provided by the search index. Index may be out of date.", result.Id);
}
})
.Select(result => result.Contact)
.Where(contact => contact != null)
.ToArray()
.Zip(
moreHitsAvailable,
(results, more) => new SearchResults<IContact>(more, results.ToImmutableList()))
.PublishLast()
.ConnectUntilCompleted();
}
You could pop out to Query Comprehension Syntax and do something like this
var x = from result in Observable.Start(() => this.GetSearchResults())
let hasMore = result.MoreHitsAreAvailable
from hit in result.Hits
from contact in GetById(hit.Id)
select new { hasMore , contact};
Over to you how to deal with the duplicate hasMore values. As we know it will be just the single distinct value (all true or all false) you could group by.
I have a simple use case where:
Receive a notification of events
Perform some action on the event
Print the content after x interval
How can I do the above step in a single Rx pipeline?
Something like below:
void Main()
{
var observable = Observable.Interval(TimeSpan.FromSeconds(1));
// Receive event and call Foo()
observable.Subscribe(x=>Foo());
// After 1 minute, I want to print the result of count
// How do I do this using above observable?
}
int count = 0;
void Foo()
{
Console.Write(".");
count ++;
}
I think this does what you want:
var observable =
Observable
.Interval(TimeSpan.FromSeconds(1))
.Do(x => Foo())
.Window(() => Observable.Timer(TimeSpan.FromMinutes(1.0)));
var subscription =
observable
.Subscribe(xs => Console.WriteLine(count));
However, it's a bad idea to mix state with observables. If you had two subscriptions you'd increment count twice as fast. It's better to encapsulate your state within the observable so that each subscription would get a new instance of count.
Try this instead:
var observable =
Observable
.Defer(() =>
{
var count = 0;
return
Observable
.Interval(TimeSpan.FromSeconds(1))
.Select(x =>
{
Console.Write(".");
return ++count;
});
})
.Window(() => Observable.Timer(TimeSpan.FromMinutes(0.1)))
.SelectMany(xs => xs.LastAsync());
var subscription =
observable
.Subscribe(x => Console.WriteLine(x));
I get this kind of output:
...........................................................59
............................................................119
............................................................179
............................................................239
Remembering that it starts with 0 then this is timing pretty well.
After seeing paulpdaniels answer I realized that I could replace my Window/SelectMany/LastAsync with the simpler Sample operator.
Also, if we don't really need the side-effect of incrementing a counter then this whole observable shrinks down to this:
var observable =
Observable
.Interval(TimeSpan.FromSeconds(1.0))
.Do(x => Console.Write("."))
.Sample(TimeSpan.FromMinutes(1.0));
observable.Subscribe(x => Console.WriteLine(x));
Much simpler!
I would use Select + Sample:
var observable = Observable.Interval(TimeSpan.FromSeconds(1))
.Select((x, i) => {
Foo(x);
return i;
})
.Do(_ => Console.Write("."))
.Sample(TimeSpan.FromMinutes(1));
observable.Subscribe(x => Console.WriteLine(x));
Select has an overload that returns the index of the current value, by returning that and then sampling at 1 minute intervals, you can get the last value emitted during that interval.
This is related to my other question here. James World presented a solution as follows:
// idStream is an IObservable<int> of the input stream of IDs
// alarmInterval is a Func<int, TimeSpan> that gets the interval given the ID
var idAlarmStream = idStream
.GroupByUntil(key => key, grp => grp.Throttle(alarmInterval(grp.Key)))
.SelectMany(grp => grp.IgnoreElements().Concat(Observable.Return(grp.Key)));
<edit 2:
Question: How do I start the timers immediately without waiting for the first events to arrive? That's the root problem in my question, I guess. For that end, I planned on sending off dummy objects with the IDs I know should be there. But as I write in following, I ended up with some other problems. Nevertheless, I'd think solving that too would be interesting.
Forwards with the other interesting parts then! Now, if I'd like to group a complex object like the following and group by the key as follows (won't compile)
var idAlarmStream = idStream
.Select(i => new { Id = i, IsTest = true })
.GroupByUntil(key => key.Id, grp => grp.Throttle(alarmInterval(grp.Key)))
.SelectMany(grp => grp.IgnoreElements().Concat(Observable.Return(grp.Key)));
then I get into trouble. I'm unable to modify the part about SelectMany, Concat and Observable.Return so that the query would work as before. For instance, if I make query as
var idAlarmStream = idStream
.Select(i => new { Id = i, IsTest = true })
.GroupByUntil(key => key.Id, grp => grp.Throttle(alarmInterval(grp.Key)))
.SelectMany(grp => grp.IgnoreElements().Concat(Observable.Return(grp.Key.First())))
.Subscribe(i => Console.WriteLine(i.Id + "-" + i.IsTest);
Then two events are needed before an output can be observed in the Subscribe. It's the effect of the call to First, I gather. Furthermore, I woul like to use the complex object attributes in the call to alarmInterval too.
Can someone offer an explanation what's going on, perhaps even a solution? The problem in going with unmodified solution is that the grouping doesn't look Ids alone for the key value, but also the IsTest field.
<edit: As a note, the problem probably could be solved firsly by creating an explicit class or struct and then that implements a custom IEquatable and secondly then using James' code as-is so that grouping would happen by IDs alone. It feels like hack though.
Also, if you want to count the number of times you've seen an item before the alarm goes off you can do it like this, taking advantage of the counter overload in Select.
var idAlarmStream = idStream
.Select(i => new { Id = i, IsTest = true })
.GroupByUntil(key => key.Id, grp => grp.Throttle(alarmInterval(grp.Key))
.SelectMany(grp => grp.Select((count, alarm) => new { count, alarm }).TakeLast(1));
Note, this will be 0 for the first (seed) item - which is probably what you want anyway.
You are creating an anonymous type in your Select. Lets call it A1. I will assume your idStream is an IObservable. Since this is the Key in the GroupByUntil you do not need to worry about key comparison - int equality is fine.
The GroupByUntil is an IObservable<IGroupedObservable<int, A1>>.
The SelectMany as written is trying to be an IObservable<A1>. You need to just Concat(Observable.Return(grp.Key)) here - but the the type of the Key and the type of the Group elements must match or the SelectMany won't work. So the key would have to be an A1 too. Anonymous types use structural equality and the return type would be stream of A1 - but you can't declare that as a public return type.
If you just want the Id, you should add a .Select(x => x.Id) after the Throttle:
var idAlarmStream = idStream
.Select(i => new { Id = i, IsTest = true })
.GroupByUntil(key => key.Id, grp => grp.Throttle(alarmInterval(grp.Key)
.Select(x => x.Id))
.SelectMany(grp => grp.IgnoreElements().Concat(Observable.Return(grp.Key)));
If you want A1 instead - you'll need to create a concrete type that implements Equality.
EDIT
I've not tested it, but you could also flatten it more simply like this, I think this is easier! It is outputing A1 though, so you'll have to deal with that if you need to return the stream somewhere.
var idAlarmStream = idStream
.Select(i => new { Id = i, IsTest = true })
.GroupByUntil(key => key.Id, grp => grp.Throttle(alarmInterval(grp.Key))
.SelectMany(grp => grp.TakeLast(1));
Is it possible to achieve something like this:
Process(
() => InstanceA.PropertyA,
() => InstanceB.PropertyB,
() => InstanceC.List.Select(x => x.PropertyC) // obviously not a proper way to do it
);
The third line would replace:
foreach(var item in InstanceC.List)
{
Process(() => item.PropertyC);
}
Last part:
Process(params Func<object>[] args) // body is in this case irrelevant
So, is there a nifty syntactic sugar that I've could use to yield the inner elements of a list back to the outer params argument? Or did I went a step too far?
No but you can concatenate if you really want them to be in the same list.
var firstTwo = new Func<object>[]
{
() => InstanceA.PropertyA,
() => InstanceB.PropertyB
};
Process(firstTwo.Concat(InstanceC.List.Select(x => () => x.PropertyC).ToArray());
This evaluates InstanceC.List earlier than your code and may not be your intention.
I think you are looking for something like this. Haven't compiled it, this may not work.
Process( (new List<Func<Object>> {
() => InstanceA.PropertyA,
() => InstanceB.PropertyB }).Union(
InstanceC.List.Select(x => new Func<Object>(() => x.PropertyC))
).ToArray()
);
EDIT: Fixing up the syntax as I re-read it...
I have two observables. One is from Observable.fromEvent(..), where the underlying event is the user checking a Winforms checkbox. The other is Observable.Interval(..) which I subscribe to in order to do some IO, and I would like to prevent this observable from doing IO, whenever the checkbox is not checked.
I could do it like this:
var gui = new GUI();
var booleans = Observable
.FromEvent<GUI.NewAllowHandler, bool>(
h => gui.NewAllow += h,
h => gui.NewAllow -= h)
Observable.Interval(TimeSpan.FromSeconds(10))
.CombineLatest(booleans, Tuple.Create)
.Where(t => t.Item2)
.Select(t => t.Item1)
.Subscribe(l => DoStuff(l));
but this has the overhead of mixing the booleans in and out of the stream. A nicer way of doing this would be, if I could generate a time-varying value from the booleans variable, which at all times had the value of the last event. Then I could do something like this:
var gui = new GUI();
var booleanState = Observable // typeof(booleanState) == ???
.FromEvent<GUI.NewAllowHandler, bool>(
h => gui.NewAllow += h,
h => gui.NewAllow -= h)
.TimeValue() // hypothetical syntax
Observable.Interval(TimeSpan.FromSeconds(10))
.Where(_ => booleanState)
.Subscribe(l => DoStuff(l));
, which to me seems much closer to the problem statement. Is there anything like this in Rx, or is there anything else, that could make such problems easier to handle?
The Where statement in your interval should work with a properly scoped normal bool:
var booleans = Observable
.FromEvent<GUI.NewAllowHandler, bool>(
h => gui.NewAllow += h,
h => gui.NewAllow -= h)
var isBoxChecked = false;
booleans.Subscribe(t => isBoxChecked = t);
Observable.Interval(TimeSpan.FromSeconds(10))
.Where(_ => isBoxChecked)
.Subscribe(l => DoStuff(l))
Edit: Per your comment, another way of doing it:
intervals = Observable.Interval(TimeSpan.FromSeconds(10));
booleans
.Where(t => t)
.SelectMany(_ => intervals.TakeUntil(booleans))
.Subscribe(l => DoStuff(l))
You need to model the checkbox checked state as Behavior and not as Event stream (because behavior has always a value and this value changes over a period of time - which fits with checkbox checked state). So you can do something like:
var booleans = new BehaviorSubject<bool>(chk.Checked)
var chkEvents = ... //generate boolean observable from checkbox check event
chkEvents.Subscribe(booleans);
Observable.Interval(TimeSpan.FromSeconds(10))
.Where(i => booleans.First())
.Subscribe(i => DoIO());
I'm going to give you two solutions. The first is a very simple and hopefully obvious one using only one observable. The second is a uses both observables.
Since you want to allow the IO only when the box is checked then this is the simplest approach:
Observable
.Interval(TimeSpan.FromSeconds(10))
.Where(_ => gui.IsChecked)
.Subscribe(l => DoStuff(l));
No need at all for the other observable.
But if you really need to use it then the Switch() extension method is your best bet. Try this:
booleans
.Select(b => b == true
? Observable.Interval(TimeSpan.FromSeconds(10))
: Observable.Empty<long>())
.Switch()
.Subscribe(l => DoStuff(l));
It's pretty clean and helps to show that there are empty periods if the checkbox is not ticked.
I hope this helps.