New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experimental Proposal of rx.Task #2641
Conversation
Adds `rx.Task` as a "scalar Observable" for representing work with a single return value. See ReactiveX#1594 rx.Future/Task This provides a type similar to `Future` in that it represents a scalar unit of work, but it is lazy like an `Observable` and many `Task`s can be combined into an `Observable` stream. Note how `Task.zip` returns `Task<R>` whereas `Task.merge` returns `Observable<R>`. NOTE: This is for experimentation and feedback at this time. Items requiring review and work that I'm particularly aware of: - naming of `OnExecute` - naming of `TaskObserver` (this one in particular I don't like) - design and implementation of `Task.Promise` - should the public `lift` use the `Observable.Operator` or should that only be for internal reuse? - should we have a public `lift` that uses a `Task.Operator`? - the `Task.toObservable` implementation right now is efficient but will likely break something so it likely needs to change to use `subscribe` - implementation of this merge variant: `Task<T> merge(final Task<? extends Task<? extends T>> source)` - several operators currently just wrap as `Observable` to reuse existing operators ... is that okay performance wise? - Javadocs Examples of using this class: ```java import rx.Observable; import rx.Task; import rx.Task.Promise; public class TaskExamples { public static void main(String... args) { // scalar synchronous value Task<String> t1 = Task.create(t -> { t.onSuccess("Hello World!"); }); // scalar synchronous value using helper method Task<Integer> t2 = Task.just(1); // synchronous error Task<String> error = Task.create(t -> { t.onError(new RuntimeException("failed!")); }); // executing t1.subscribe(System.out::println); t2.subscribe(System.out::println); error.subscribe(System.out::println, e -> System.out.println(e.getMessage())); // scalar Tasks for request/response like a Future getData(1).subscribe(System.out::println); getDataUsingPromise(2).subscribe(System.out::println); // combining Tasks into another Task Task<String> zipped = Task.zip(t1, t2, (a, b) -> a + " -- " + b); // combining Tasks into an Observable stream Observable<String> merged = Task.merge(t1, t2.map(String::valueOf), getData(3)); Observable<String> mergeWith = t1.mergeWith(t2.map(String::valueOf)); zipped.subscribe(v -> System.out.println("zipped => " + v)); merged.subscribe(v -> System.out.println("merged => " + v)); mergeWith.subscribe(v -> System.out.println("mergeWith => " + v)); } /** * Example of an async scalar execution using Task.create * <p> * This shows the lazy, idiomatic approach for Rx exactly like an Observable except scalar. * * @param arg * @return */ public static Task<String> getData(int arg) { return Task.create(s -> { new Thread(() -> { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } // deliver value s.onSuccess("Data=" + arg); }).start(); }); } /** * Example of an async scalar execution using a Task.Promise * <p> * This shows how an eager (hot) process would work like using a Future. * * @param arg * @return */ public static Task<String> getDataUsingPromise(int arg) { Task.Promise<String> p = Promise.create(); new Thread(() -> { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } // deliver value p.onSuccess("Data=" + arg); }).start(); return p.getTask(); } } ```
/cc @jspahrsummers who prototyped this as well in ReactiveCocoa and Found It Lacking(tm) |
@paulcbetts Thanks for getting involved. What is the concern of pursuing this and what didn't work about the prototype in ReactiveCocoa? This is attempting to address one of the most common requests and complaints about using Reactive Extensions – that everything is modeled as a vector when some things naturally fit a scalar representation, such as request/response IO. This approach does not try and force behavior of a |
By the way, this is being pursued because unlike Rx.Net which had It is understood that without async/await on the JVM that the full power of the Rx.Net |
@benjchristensen I don't have any concerns personally, I just remember JSS initially being all for this but then after trying to build stuff with it, was frustrated. Wanted to potentially save you a similar journey if you end up agreeing |
Wish I remember the details of why, lemme see if I can find that PR |
My exploration of similar things was spread over several different RAC PRs, so it'd be hard to find one succinct thing to link to. To summarize, the main problem I ran into is that it's really hard to compose single-item streams with variable-item streams. You end up doing a lot of type juggling, in which the professed safety (of knowing it's a single item) can easily be lost. Adding operators to automatically lift Fundamentally, though, I'd ask (rhetorically): does this actually simplify anything, or does it merely make it easier for users who are familiar with futures/promises and want to reuse that knowledge? YMMV, but I strongly prefer to design for simplicity and minimizing the number of different concepts in play, because familiarity can always be learned, but complexity is harder to avoid. |
Thanks @jspahrsummers for the feedback. Your reasoning and experience fits our thinking over the past 2 years on this topic and is why for so long this idea has been deferred. The evolution of thinking after using RxJava for a couple years is that libraries that expose public APIs can benefit from being able to communicate if something is scalar or vector – despite the "functional" argument that a scalar is just a "list of one". We are able to choose to use When composition occurs that results in multiple values then the type changes to an Another consideration is that APIs that expose data access are generally separate from the consuming code that composes them, and it is these data access APIs where the scalar Task<T> getData() versus Observable<T> getData()
There is one thing I think it simplifies that is more than just making it easier due to familiarity. It communicates when a user does NOT need to think about multiple values, and that does indeed simplify code. A
An
The mental model for these extra possibilities do definitely increase the complexity and are a big part of the pushback for using RxJava that I have seen for request/response models. These more complex models do still end up happening as soon as composition occurs, but now that composition is declared by the consumer, and the consumer knows the contract of the producer to be simpler if it is a For example, in "service layer" type code we could have getters like this: Task<T> getDataA(arg);
Task<R> getDataB(arg);
Observable<R> getDataC(arg); This is now simpler to understand because the types narrow the scope for
Would support of composition like the following appease that issue? Task<Integer> h = Task.just(1);
Observable<Integer> o = Observable.just(2, 3, 4);
h.mergeWith(o).subscribe(System.out::println);
My thinking has evolved to think that trying to force everything to be a vector results in increased complexity of a different kind that can be addressed by a slight increase in complexity of the RxJava APIs (2 types instead of 1). Static types communicate common information that simplifies the mental model and constrains available states and operations that can be applied. This seems worth the increase in API surface area. |
I violently agree with capturing as much information as possible in the type system (thus our ongoing attempts to capture the hot/cold distinction in types for RAC), so I don't have any objections there.
Yeah, methods like these definitely help a lot. But how would a method like It's not that these peculiar cases are unsolvable, of course. My concern is simply be that there are so many weird composition cases like this that the end result will just be confusion. I think in some, subliminal sense, this could also be a disincentive toward using Observables—like a desire to keep everything in one “world”—which might lead users to miss out on the expressive power of stream-based programming. Ultimately, though, this is just speculation on my part, and you know your target audience much better than I do. I don't want to discourage something that works and that you believe to be the right design decision. 😄 |
That is actually a pretty strong argument that I missed. We did have these cases whenever we would emit records from a database through Rx. Here the problem was that for the receiver it was unintuitive to deal with the "successful query, but nothing found" case, as this resulted (as per the duality with iterators, which our storage library is built upon) in a sequence that would simply complete and exit. What we did to work around this was to convert the sequence with So, I agree that |
Thanks both @mttkay and @jspahrsummers for your input on this. Very helpful.
I agree with this concern, which is why I value feedback and consider this purely an experiment at this time and want to try and make the code work and see if it's good or not.
I don't see why it wouldn't since The ability to design the
Yes it likely will, but I don't necessarily see that as a bad thing in the same way that we don't consider a method signature of
This is exactly the type of stuff we shouldn't have to do and is far more awkward. An
How would you want this to work? In a scalar model, would you treat this as an error, or would you have a
It seems to me that this should be an easy thing to distinguish. It's either always single-valued, or it's multi-valued.
Can you provide more information on what you mean with this question? |
I don't think treating this as an error works well for us, since error is a terminal state. From our app point of view, it is not really an error anyway. It simply means there was nothing to satisfy your query, which is a perfectly reasonable "result". Modeling this as
I guess what I'm asking is: will there be specialized classes such as Task specific Observers that only work with tasks? What is the degree of interchangeability between Task and Observable? If I would have to go back to my client code and touch a lot of code in the presentation layer because a sequence changed from N to 1, then that would be a smell to me. Right now, I can go to my storage objects and completely alter the number of emissions, order of emission, or even rate of emission without touching anything in the observing layers. So I guess what I'm asking is, if I switch an object emitting something from Observable to Task, what amount of change will trickle up through the system because of that? |
I share this view.
Since the apps I work on don't generally have a good standard for this we very often end up modeling optional states like these individually. For example, things that involve user authentication or search results will have a container type that is returned so that we don't use exceptions as part of the control flow. For example: Observable<SearchResult<T>> search(String query);
SearchResult {
boolean hasResults();
Observable<T> results();
}
Don't know, hence this discussion. In this proposal I do have a See this for
My intent is for
This would never be required. If a codebase or team is okay treating everything as sequences then
That may be, but it doesn't mean the consumers are ready for it. This is an implicit data contract that virtually every codebase I've ever seen struggles with. If they used to get one item and now receive two, will their code work? I've seen production outages caused by UIs assuming 3 elements from a list and receiving 2 or 4. The
Putting aside the static typing change which would require a recompile, the |
Other than reusing internal operators of RxJava, does this rx.Task have to be part of core RxJava instead of being a separate project? |
If it's going to exist I think it should exist here in core RxJava since it is a foundational type. |
Since @benjchristensen asked me to weigh in on this, I'll post about my experiences here. Some backstory: At our company Q42, we're a team of 4 developers simultaneously working on an Android and iOS app for a client. None of us had worked with RxJava before, and we had only a bit of experience with Rx and Tasks for .NET. We first developed the iOS app in Swift, using a promises library (which I wrote). The app only does simple network calls, and for the few places where we needed something reactive we used CoreData and NSNotifications. When we started developing the Android app, we used RxJava, because that was easily integrated with Retrofit. While we did customise the UI for Android, we tried to keep the architecture of the code as similar as possible to the iOS codebase. However we kept running in to issues. Network calls not happening because we didn't subscribe. Or happing twice (because we subscribed twice). We forgot to attach error handlers, or didn't cache the result of a flatMap. All these issues are of course solvable, and we did solve them, but it felt like we kept doing the same things over and over again. Specifically, since we were just porting Swift code, all we wanted (and needed) was the simpeler promise semantics. For that reason, we finally decided to just write a Promise wrapper around an Observable. This enforces promise semantics that are similar to the promises we use on iOS. It does things like immediately subscribe, to ensure the network call gets kicked off and cache the result. This is by no means code I would suggest others to use, it is very specific to our use case. But we are quite happy with it. In summary, for our use case we didn't need the full power of Observables, and we build something around it to make our client code both simpler and easier to understand. Unfortunately, I don't have enough experience with RxJava to judge if adding a |
Thanks @tomlokhorst for sharing your usage experience. |
ping @headinthebox for your input on this |
Task.create looks like simply Observable.create. Also Task is an additional concept that requires much new additional code (and documentation) and it's not obvious what is a reason to have Task? |
It's a formalization of scalar observables (further enforced by the type On Mon, Mar 16, 2015 at 9:41 PM Sergey Mashkov notifications@github.com
|
What about different approach?
public static abstract class SingleItemObserver<T> implements Observer<T> {
@Override public final void onNext(T t) {
// now Observer should somehow unsubscribe from Observable or ignore next calls
onSuccess(t);
}
public abstract void onSuccess(T t);
@Override public final void onCompleted() {
// no impl required
}
}
1st version of It's evolution of the application, it should not be hard. With But, yes, sometimes (actually, pretty often) you want to receive only one result and don't care about I will definitely won't use Just some thoughts. |
@artem-zinnatullin what about the proposed addition gives problems switching? It "is" an Observable and easily switches between them. That's the point of it. |
I spoke with @headinthebox and we are both okay with this addition except for the name. Both 'Task' and 'Future' are used elsewhere to represent eager async scalar computation. We need a name that differentiates. I'm not a fan of 'Single' or 'SingleObservable'. 'ScalarObservable' is correct but feels like a mouthful. Thoughts on naming? |
@benjchristensen as I see from files in the PR, it's not an I want to suggest to move decision "to work with Stream of data or work with one emission of data" to the end of the chain — into About naming: |
@benjchristensen Could you elaborate on the need and use cases for this "Task" to be lazy by default? Since it can only ever produce one value, it seems especially weird that, that value is "mutable". Out of these three functions, the last one seems least useful.
Or is this merely lazy-by-default to keep it semantically closer to Observables? |
The current proposal is similar to F#'s and Darts async in that you build a computation and then execute it. See http://tomasp.net/blog/async-csharp-differences.aspx for an explanation and the benefits of the "cold" model. |
RE: naming - This seems like the Command pattern so perhaps |
@artem-zinnatullin Yes, but that's kind of the point. My experience with consuming code is that changing an If you don't want to use this new type, that's fine, it's not for you then :-)
That doesn't solve the desired use case which is communicating via the public API that something is scalar. The |
@tomlokhorst That's one reason. The more important reason though is so that compositional graphs can be lazily defined and then subscribed to multiple times, just like with Observables. The reason for being "cold" is exactly the same as If a There are already hot types like This fits with what @headinthebox stated above: "build a computation and then execute it" |
Naming options include:
Despite not liking how long the name is, |
I don't like And thanks @benjchristensen and @headinthebox for expanding on the hot vs cold here. While I don't yet see a practical use case, I can definitely see the theoretical use case. That means a practical use case will probably pop up in a little while. |
LazyTask?
Although I’d personally be fine with just Task.
|
+1 for |
Hi @benjchristensen, I fully agree with the need to have a scalar observable type, especially when exposing such type in application public API. The main thing that confuses me in your implementation is this embedded About the naming:
Thanks in advance for your feedback. |
Deferred might get confused with defer/OnSubscribeDefer though? On Thu, Mar 19, 2015 at 11:12 AM, Sébastien Deleuze <
Matthias Käppler Engineer Twitter: https://twitter.com/mttkay Skype: matthias-sc SoundCloud Ltd. | Rheinsberger Str. 76/77, 10115 Berlin, Germany | +49 Managing Director: Alexander Ljung | Incorporated in England & Wales Capture and share your music & audio on SoundCloud |
@mttkay Indeed that may perhaps be an issue. I am not sure it makes sense, and there may be some backward compatibility impact, but since |
I'm not a fan of the use of the word scalar for this purpose. I think it's use in programming is unfortunate. I was very confused by the the term when I encountered it in the codebase ages ago because a scalar in mathematics is just a number (something that has scale). +1 Single |
if it is going to be Async, I would expect it to be full asynchronous from beginning to end. How it is going to handle over 100 items, would it be same as Observable? |
I don't like |
Interesting discussion! @benjchristensen have you considered a |
just here for the bikeshed part: I like |
+1 for this proposal I'm a fan of |
The issue I have with |
What do you intend? The point of the different types if that they are different. Even the
Yes I think we should have that. I don't have it in this PR as I was going for a bare minimum to get started, but that's definitely a valid operator to have here. |
@benjchristensen I understand purposes of such For simplicity, let's call it Pros:
Cons and questions:
Extra addition: loadSomething() // returns Observable<T>
.subscribe(value -> doSomething(value));
loadSomething() // returns Single<T>
.subscribe(value -> doSomething(value)); As you can see, with lambdas it does not matter what is it: I agree, that Just saying... |
No I'm not sure I want this, but for the sake of the debate I'd like to counter a couple of your cons:
We could consider Singles to simply be constrained Observables under the hood, hence not violating DRY.
Exactly. These are not questionable - they are a definite no-go for
I argue that most newbies will encounter Single before Observable, hence facing a simpler, API more easy to learn.
So don't implement it.
I'd say only implement Single.from(), not Single.just(). It doesn't make sense for Singles. |
I think rx should be open to singles, too. sure, Single works!
|
Just tried RxNetty, now I understand need in Scalar But for example with RxNetty you need Scalar Probably, for backend systems which use HTTP instead of some socket-based solutions, Naming: |
|
Replacing with #3012 |
Adds
rx.Task
as a "scalar Observable" for representing work with a single return value.See #1594 rx.Future/Task
This provides a type similar to
Future
in that it represents a scalar unit of work, but it is lazy like anObservable
and manyTask
s can be combined into anObservable
stream. Note howTask.zip
returnsTask<R>
whereasTask.merge
returnsObservable<R>
.NOTE: This is for experimentation and feedback at this time.
Items requiring review and work that I'm particularly aware of:
OnExecute
TaskObserver
(this one in particular I don't like)Task.Promise
lift
use theObservable.Operator
or should that only be for internal reuse?lift
that uses aTask.Operator
?Task.toObservable
implementation right now is efficient but will likely break something so it likely needs to change to usesubscribe
Task<T> merge(final Task<? extends Task<? extends T>> source)
Observable
to reuse existing operators ... is that okay performance wise?Examples of using this class: