Skip to content
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

Add a Publisher 0..1 specialization #490

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

bsideup
Copy link

@bsideup bsideup commented May 27, 2020

  • add a marker interface
  • add 5th paragraph to the specification

The TCK additions, examples and other changes can be added as a follow up (@OlegDokuka have volunteered to work on them if needed) to keep this PR small and focused.

* add a marker interface
* add 5th paragraph to the specification
@OlegDokuka
Copy link
Member

@viktorklang any chance to look at this PR?

Cheers,
Oleh

@viktorklang
Copy link
Contributor

viktorklang commented Jun 3, 2020 via email

@NiteshKant
Copy link

Although I agree that other types (single, completable, maybe) are required to express different high level APIs as many different libraries (reactor, rxjava, servicetalk, etc) have demonstrated, I do not think that the proposed change here actually addresses those concerns.

Even if we provide a MonoPublisher it isn’t a fit for Single as Single guarantees exactly one item. The question then would be as to what makes at-most one semantics special as compared to exactly-one when we know both are required for high-level APIs.

@bsideup
Copy link
Author

bsideup commented Jun 4, 2020

@NiteshKant thanks for the feedback!

Even if we provide a MonoPublisher it isn’t a fit for Single as Single guarantees exactly one item

Since Single is "exactly one", it still fits to 0..1 and can/should implement MonoPublisher.

The question then would be as to what makes at-most one semantics special as compared to exactly-one when we know both are required for high-level APIs.

I see the point of "exactly one" and totally understand why some libraries have decided to have separate Single/Maybe types.

That said, I think Single is a separate concern, and here is why:
MonoPublisher defines a specialization/marker of Publisher. It only defines that the Publisher's upper bound of emitted items is limited to one (and request(1) guarantees that).
But at the same time Single defines a behaviour (that must also be enforced), because now we need to define that onComplete without onNext is an error (not the case for MonoPublisher). The consumers will be required to check this contract, the error state will need to be defined (when SinglePublisher emits zero elements), akin to non-positive request, etc etc

I am not saying that we should not have SinglePublisher at all, but I have a feeling that it is not as simple as MonoPublisher (which is a truly low hanging fruit here).

@OlegDokuka
Copy link
Member

I agree with @bsideup. Single, Maybe, Completable are derivable types from MonoPublisher. The goal is to define a generic type representing 0..1 regardless on whether it is guaranteed 1| guaranteed 0 | or maybe 0..1 | (if we talk more on such, we can have Error, which guaranteed to return an error and never onComplete or onNext) But my belief is that it is implementation, which less compatible when there is no 0..1 abstraction. Thus having MonoPublisher is a big step to say - hey, here we have a subtype of Publisher which guarantees to send no more than a single element

@NiteshKant
Copy link

@bsideup

thanks for the feedback!

❤️ , thanks for starting this discussion!

Since Single is "exactly one", it still fits to 0..1 and can/should implement MonoPublisher.

Single defines a behaviour (that must also be enforced)

The value of introducing another type is that they can be used as arguments or return types to public APIs. In this context, the value of Single is that its API contract is strict => it can not terminate without a result. Extending MonPublisher does not provide the same guarantee, so extending MonoPublisher is no different than extending Publisher.

but I have a feeling that it is not as simple as MonoPublisher (which is a truly low hanging fruit here).

Agreed, defining a contract for Single (and friends) has a bigger surface area. My motivation here is to drive a change by value and not by the level of complexity of the change. In that spirit, MonoPublisher adds some value but does not cover all cases hence the question about return on investment.

@OlegDokuka

Single, Maybe, Completable are derivable types from MonoPublisher.

So is MonoPublisher which is derivable from Publisher. I don't see this as a convincing argument. If we do not provide a type which is decoupled from Publisher for these special cases, the value add is trivial.

@OlegDokuka
Copy link
Member

OlegDokuka commented Jun 4, 2020

@NiteshKant

I fully understand your point of trying to derive separate types, but consider the following.

The value of introducing another type is that they can be used as arguments or return types to public APIs.

Is it not that applicable to MonoPublisher even though it is a subclass of the Publisher?

The following statements applicable for MonoPublisher:

  1. MonoPublisher != Publisher
  2. MonoPublisher <| Publisher

That said when you have a Publisher you can not pass it to the API when MonoPublisher is defined as an acceptable type.

It means that MonoPublisher requires a specific extension that produces at most 1 element.

The same applies to the return type. If an API method returns MonoPublisher it strongly guarantees at most one element and leaves space to receive or not value and receive onComplete or onError. (this may leave a question when it is useful since we may get onComplete which may be meaningless - but this discussion will be at the end).

Following the mentioned idea, having MonoPublisher as a start point you can develop Single | Maybe which is compatible with external libraries that expect MonoPublisher as an input.

Clearly, we loose the semantic of stronger behavior of Single, but we preserve the guarantee of the at most one element which is valuable in the majority of cases.

Sidenote: Completable is semantically redundant since Completable ~= MonoPublisher<Void> ~= Publisher<Void> and we all know that Void is nullable value so we can exclude it as a value of the onNext call, hence all possible states are onError or onComplete which is effectively the same as Completable (see https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/core/CompletableObserver.java).

So, we left with the main question - do we need SinglePublisher type?

This question is addressed to anyone who participates in this thread and the answer should be derived from the following:

  1. How frequently in the real world a user demands strong results vs weak results which may or may not return value?
  2. Is that demand semantic related or performance-related?
  3. What users do with CompletableFuture which has the same resulting semantic as MonoPublisher and if most of the users are satisfied with the CompletableFuture sematic are not them get the same from the MonoPublisher? (Assume Publisher is always exposing the broader surface since it has more undetermined state than MonoPublisher)

Update

Thinking about all of that, I tend to say SinglePublisher can be more demanded.

If we thinking about r2db-spi which has

MonoPublisher connection()

We expect:

  1. A single Connection to be returned.
  2. If there something wrong, we expect an error to be returned
  3. On complete only leaves us in the undetermined state

So I have to say, that maybe we need SinglePublisher... which strongly returns a value or an error

Update 2

On the other hand, we have RSocket project which has interactions such as requestResponse and requestFireAndForget. Having those methods returning Publisher makes meaning contradict to the return type semantic and the method name meaning. And SinglePublisher in that case is inapplicable.

Hence, I tend to say that we may need

SinglePiblisher <| MonoPublisher <| Publishere

which of course more challenging than MonoPublisher but brings more value than just a MonoPublisher which may still leave some questions.

Cheers,
Oleh

@NiteshKant
Copy link

@OlegDokuka

Rite .. all connect APIs, HTTP response APIs, etc need Single more than a Maybe (Mono).

For motivations for Completable, you may find this RxJava issue useful.

RSocket fireAndForget is accurately represented as a Completable BTW.

Drawing analogies for different types from the synchronous world, you have the following return type variants:

  • public Collection<T> getAllUsers() <- return type represented as a Publisher
  • public T getUser(String userId) <- return type represented as a Single
  • public void recordEventForUser(String userId, Event event) <- return type represented as a Completable.

How often do you see similar methods while designing an API? There aren't may ways to tell which is more important than other, all are important I would say.

@OlegDokuka
Copy link
Member

OlegDokuka commented Jun 4, 2020

@NiteshKant

Rite .. all connect APIs,

Agree.

HTTP response APIs

2xx is effective Maybe since there can be empty response so if one would need to respond with the empty result they would always have to invent Single<Result> with the Result subtype ResultEmpty for such purposes.

RSocket fireAndForget is accurately represented as a Completable BTW.

In fact, we are good with Mono<Void> as I said generic allows expand the meaning of the result whilst Single<Void> is impossible.

For motivations for Completable, you may find this RxJava issue useful.

I see the problems...

In the reactor, we usually solve the mentioned as the following

connection.write(Observable.just("hello"))
          .cast(String.class)
          .concatWith(connection.getInput())

as

connection.write(Observable.just("hello"))
          .then(connection.getInput())

or

Flux<Object> merge = Flux.merge(empty(), generateRating());
    

The second is indeed a bit ugly and would require

Flux<String> merge = Flux.merge(((Mono<String>)((Mono)empty())), generateRating());

or

 Flux<String> merge = Flux.merge(empty().then(Mono.empty()), generateRating());

Agree, I can see how completable with generic can help...


public Collection getAllUsers() <- return type represented as a Publisher

agree

public T getUser(String userId) <- return type represented as a Single

disagree, Java has this nullability problem so T can always be null -> effectively output is Maybe

public void recordEventForUser(String userId, Event event)

now agree that Completable may simplify derival of types when merging Completable with Maybe and achieve Maybe.

But I need to say that all of these are done for the sake of better performance and in order to avoid Flux -> back to Mono conversion whilst the general semantic for the monoid stream does not exist in the spec at all which is more harmful (can be...)


After all, in the expansion of the semantic, I can see the value of Single | Maybe but I would say both have its place. Even though I'm not sure on the usage demand...

Well, folks invented Kotlin to always have Single and sometimes have Maybe... in the majority of APIs we return Single and sometimes marks methods as Nullable to say Maybe.

That said Single prevails over Maybe in the most of imperative APIs but talking about a network interaction Maybe can be more demanded than Single

@bsideup
Copy link
Author

bsideup commented Jun 5, 2020

I think the discussion got shifted in a wrong direction.

Do we (as a reactive community) need SinglePublisher, for even better type safety? Yes.

Does it mean that we don't need much simpler 0..1 specialization of Publisher called MonoPublisher or whatever? Of course not.

If we ever talk about adding SinglePublisher, I would fight to the end that it should extend MonoPublisher. Which means that MonoPublisher is the first step in all this discussion, and, unlike SinglePublisher, MonoPublisher is easy to add.

Plus, if somebody questions the value of MonoPublisher, I would like to point out that Project Reactor existed for many users, where you only get 0..n and 0..1 Publishers. That's the API's bare minimum needed to express most of the reactive apps, and it served very well for far.

Extending MonPublisher does not provide the same guarantee, so extending MonoPublisher is no different than extending Publisher.

I think "no different" is a stretch here. SinglePublisher should extend MonoPublisher, so that the consumer of it knows that it is finite, as opposed to an infinite Publisher of something that will never end and you can't do things like do X then Y.


The bottom line is that we should not open the can of worms. Let's focus on MonoPublisher (keeping in mind that both SinglePublisher and Completable are special cases (1..1 and 0..0) of MonoPublisher - this is important!) and the value it brings.
There were enough discussions and the last I remember is that more or less everyone agreed that we could use a type that helps describing "at most one" Publishers.
More (if needed and agreed) Publisher types can be added as a follow up (but MonoPublisher is basically a prerequisite for them)

@OlegDokuka
Copy link
Member

OlegDokuka commented Jun 5, 2020

Just to clarify.

I think the discussion got shifted in the wrong direction.

Apologies for those long-reading, but I guess I tried to get @NiteshKant point.
My general PoV has not changed, and pervious was more on understanding the general use-case.

the general semantic for the monoid stream does not exist in the spec at all which is more harmful

As I stated, this is the main reason for this PR.

Do we (as a reactive community) need SinglePublisher, for even better type safety? Yes.

Does it mean that we don't need much simpler 0..1 specialization of Publisher called MonoPublisher or whatever? Of course not.

Agree with @bsideup and propose to raise the follow-up discussion on SinglePublisher demand once we are done with the Monoid one.

@NiteshKant
Copy link

@bsideup I believe any of the 1 or less items types should not extend Publisher at all as the demand is implicit with subscribe(). Both RxJava and ServiceTalk follow this model.

@bsideup
Copy link
Author

bsideup commented Jun 5, 2020

@NiteshKant I am afraid this will hurt the adoption.

Also, they remain valid Publishers, so why shouldn't they extend?
We're not changing the semantics, and the request(1) is still required before they start doing the work.

@OlegDokuka
Copy link
Member

@NiteshKant what is your word on not extending Publisher? Were there any historical reasons to have it not a subtype?

I can see a performance reason, so having it as a separate type allows to get rid of redundant calls, but do you have anything in mind apart from performance optimizations?

@NiteshKant
Copy link

NiteshKant commented Jun 5, 2020

I am not saying that this change is incorrect in terms of semantics but it isn't perfect from an API design point of view.
This change is correct semantically but it isn't perfect from an API design point of view. The biggest disadvantage for me is that there is request-n semantics for at most one item. As shown by RxJava and ServiceTalk, interaction with such entities is easier without the request-n semantics, request-1 is implicit upon subscribe.

@bsideup
Copy link
Author

bsideup commented Jun 6, 2020

@NiteshKant

isn't perfect from an API design point of view
interaction with such entities is easier without the request-n semantics, request-1 is implicit upon subscribe.

As much as I would like to reduce the overhead and simplify subscribing on such specialized Publishers, I am afraid it has a much bigger scope than just adding a marker overhead and, given the inclusion of RS into JDK, may require double as much effort.
Before submitting the proposal we evaluated multiple options and then reduced it to the minimal change that has the best "change size vs impact" balance, while also keeping a room for improvements for later.

I will be first to support the next initiative to apply the optimizations (RS spec 1.1?), but I have a feeling that it will take much longer, while MonoPublisher can go into a patch release and already bring a lot of value to both library/framework developers (easier to express their APIs) and users (easier to understand the consumed APIs).

WDYT?

@DougLea
Copy link
Contributor

DougLea commented Jun 6, 2020

Here's a counter-proposal. Add method:

void Publisher.subscribe(Subscriber<? super T> subscriber, long initialRequests, long maximumRequests);

Notes:

  • This is in the spirit of "0-RTT" improvements in network protocols (QUIC, TLS1.3, etc); the same rationales hold of removing one or more request-response sequences.
  • As usual requests of Long.MAX_VALUE mean effectively unbounded.
  • The current one-arg version is equivalent to subscribe(s, 0, Long.MAX_VALUE).
  • It applies to the motivating use cases, and may also improve latency in other common use cases
  • It can be default-implemented without creating a subinterface.
  • No type-introspection is needed (instead track maxRequests)

Open issues:

  • Require (vs allow) auto-close when maxRequests hit?

@viktorklang
Copy link
Contributor

viktorklang commented Jun 6, 2020 via email

@bsideup
Copy link
Author

bsideup commented Jun 6, 2020

@DougLea are you suggesting that we add this method to MonoPublisher or Publisher?

@viktorklang

I’d like to explore what is possible without marker interfaces.

Although I like where the idea of adding initialRequested goes, I would like to remind that the optimizations wasn't the top reason to add MonoPublisher.

Currently, there is no way to express 0..1 result with RS types.
Yes, we can optimize the routine of subscribing on 0..1 Publishers. But there is still a big problem of not being able to express 0..1 result type in libraries' public APIs that do not want to depend on some RS implementation that most of the time come with Maybe/Mono/Uni/Whatever type.

So I am kindly asking to keep in mind this important UX (not performance!) issue.

@DougLea
Copy link
Contributor

DougLea commented Jun 6, 2020

@viktorklang I contemplated adding to subscription along these lines, but it seems better in all ways to overload a subscribe method: no RTTs if remote, and no thread-safety issues if local; plus simpler default implementability. Or maybe I'm missing something?

@OlegDokuka
Copy link
Member

OlegDokuka commented Jun 6, 2020

@viktorklang @DougLea the purpose of that PR is to add an interface that identifies a Publisher of a single type (similar to CompletableFuture but preserving reactive streams semantic such as lazy subscription, backpressure, etc.).

Unfortunately, a Subscriber (even though such can specify that demand is exactly one element) has no relationship to the API, which users expose to expose a stream of elements.

Optimizations that expose the subscriber demand in another way are out of spec, and far more than Reactive-Streams Spec should define (my point of view).

@NiteshKant
Copy link

I like the initial and max requested/elements proposals as it has both positives, 0-RTT and the ability for subscriber to assume there can be only one (or less) items. It also has the advantage that we do not have to add new interfaces.

Perhaps maxExpected is more appropriate when coming from a Subscriber to express that a Subscriber only expects n items ever and if there are more it will be regarded as an error (mono/single case).

@NiteshKant
Copy link

I would agree with @viktorklang that these values detached from a Subscriber implementation will be confusing if we assume that the entity calling subscribe() can be different than the one implementing a Subscriber which is common for intermediary layers (function composition cases).

I think having two additional methods on a Subscriber will be cleaner:

  • initialRequested()
  • maxExpected()

@NiteshKant
Copy link

Additional constraints on the new methods would be that they will only be called before calling onSubscribe(); eliminating the potential sequencing ambiguity between request-n from Subscription and initialRequested().

@NiteshKant
Copy link

@bsideup If Subscriber can signal to the Publisher that it only expects a single item ever then the different types for API clarity can continue to be handled by the libraries on top by adapting the Subscriber implementations. So I think this proposal achieves what you are looking for.

@bsideup
Copy link
Author

bsideup commented Jun 7, 2020

@NiteshKant

If Subscriber can signal to the Publisher that it only expects a single item ever then the different types for API clarity can continue to be handled by the libraries on top by adapting the Subscriber implementations. So I think this proposal achieves what you are looking for.

You're talking about the runtime. I am talking about the compile time / method signatures.

Can we please stop ignoring the important aspect of being able to determine the 0..1 nature by looking at method's return type?

Let's stop thinking about the optimizations for a second and think about the users who are struggling because they can't express / identify finite 0..1 methods just by using the RS.

  • Is there an overhead when you subscribe on 0..1 sources? Yes.
  • Are the end users (not RS frameworks devs) talking about it and asking to fix it ASAP? Not really.
  • Is there a strong demand from the end users to be able to express / identify 0..1 return type? Yes.

@graemerocher
Copy link

graemerocher commented Jun 7, 2020

Totally with @bsideup on this one, the fact there is no type in RS that allows detection of a single causes no end of pain for library authors and users.

It causes problems in numerous areas such as runtime detection of the reactive type. In Micronaut for example we had to write a isSingle method that uses a horrible hack and forces us to dynamically depend on concrete implementations:

In addition as @bsideup said there is no way to write an API that isn't bound to a particular reactive library that expresses that the API emits a single value. To workaround this limitation we had to introduce an annotation in Micronaut called @SingleResult:

Which we are then forced to use to identify APIs that emit a single item. For example:

https://github.com/micronaut-projects/micronaut-core/blob/e9378a1adf1873bbb30889b74a5498c6cab89eca/runtime/src/main/java/io/micronaut/discovery/DiscoveryClient.java#L40

All of these items make it harder than it should be to allow interoperability between Reactive libraries which is the fundamental goal that this PR is trying to resolve and surely a fundamental goal of Reactive Streams.

I would absolutely love to see MonoPublisher (or whatever it is called), it would solve many, many problems for users and library authors.

@graemerocher
Copy link

One more comment, this PR is also non-breaking as it introduces a new interface that can be gradually adopted by libraries. The discussion has veered towards introducing a new method which IMO is much more invasive and a greater breaker change for people that have adopted Reactive Streams.

@DougLea
Copy link
Contributor

DougLea commented Jun 7, 2020

@bsideup and others: I'm addressing the protocol side of the lack of control for initially:n, max:m. Solving this makes it simpler to include corresponding specialized types if so desired; even ad-hoc non-standardized ones.

While I'm at it: There's a difference between cancelling a subscription versus versus hitting max request bound versus cancelling a CompletableFuture: In the first case, you will eventually stop getting them. In the second, you won't get them at all past bound, and in the third case, you may get them but they will throw exceptions upon use. The second version probably most commonly applies but is not currently supported.

A further aside, there was a vocal contingent (including some RS contributors) opposing including cancel in CompletionStage interface. One argument is along the lines of above, but I should have realized with RS that there should be some compensating RS features such as this.

@OlegDokuka
Copy link
Member

Hey @DougLea.

Thank you for the clarification.

@bsideup and others: I'm addressing the protocol side of the lack of control for initially:n, max:m. Solving this makes it simpler to include corresponding specialized types if so desired; even ad-hoc non-standardized ones.

I belive such improvements are general enough and not fully related to this topic, thus I guess it is better to have a separate discussion for it.

WDYT?

Cheers,
Oleh

@simonbasle
Copy link

I've refrained as much as possible from commenting on the PR (and issue) so as not to pile up the discussion with Reactor's pov, but here are my most important thoughts on the points raised during these past few days:

  1. the notion that imperative methods like T method() translate to a Single in the reactive world is overlooking how often T is nullable (eek! or an Optional, or a Null Object 😅)

  2. Expressing the 0..1 semantic definitely looks like the low hanging fruit

  3. the protocol change proposal is more intrusive, more involved in terms of adoption by RS libraries and doesn't solve the type/usefulness to APIs aspect, which is fundamentally what we're after

  4. that said, I think @DougLea 's proposal opens up another very interesting discussion at the protocol level: the ability for a Subscriber to communicate the sum total of its demand in one call (or at least communicate to the Publisher that is won't need more than these last N elements). This amounts to an alternative to request+cancel that communicates that this is a "happy path". Think differentiating a cancellation originating from take vs from timeout. This is a more general and impacting change though, and should be probably discussed in another issue.

@DougLea
Copy link
Contributor

DougLea commented Jun 8, 2020

My reservation with the SinglePublisher proposal is that response bounds are not necessarily properties of a Publisher or a Subscriber, but instead parameters of a protocol (i.e., Subscription), that cannot currently be expressed. Adding SinglePublisher without fixing this invites further flailing.

@NiteshKant
Copy link

From my POV, extending Publisher for 0..1 types isn’t the most appropriate design as request-n is implicit upon subscribe. However, if we do go that route, I agree with @DougLea that establishing response bounds should be the first step. All proposals here are opt-in so there is no backward incompatibility.

@NiteshKant
Copy link

@simonbasle it looks like null == absence of an element is a common understanding in the reactor world. I am afraid it breaks down when you consider that null can be emitted by a Publisher; would that mean it does not count towards request-n?

@viktorklang
Copy link
Contributor

@NiteshKant null is not a valid element in RS: https://github.com/reactive-streams/reactive-streams-jvm#2.13

@OlegDokuka
Copy link
Member

OlegDokuka commented Jun 8, 2020

@simonbasle it looks like null == absence of an element is a common understanding in the reactor world. I am afraid it breaks down when you consider that null can be emitted by a Publisher; would that mean it does not count towards request-n?

@NiteshKant

In reactor null is used for 1 case - indicating an element when there is an ASYNC fusion mode, so this is a signal to drain the underlying queue, otherwise it is invalid

Sent with GitHawk

@bsideup
Copy link
Author

bsideup commented Jun 8, 2020

@NiteshKant

From my POV, extending Publisher for 0..1 types isn’t the most appropriate design as request-n is implicit upon subscribe

MonoPublisher does not change the semantics of Publisher that it extends. There is no implicit request-n.

Also, the primary goal of MonoPublisher is to be able to know that such Publisher producers at most one item (mostly for the end users that need to know it).
To optimize (with instanceof) or not is left to the frameworks and outside of the scope of this PR. It just a nice side effect of having this change - the runtime introspection of Future-like Publishers. That said, I'd be happy to see a follow up on this topic!

@NiteshKant
Copy link

@viktorklang ahha ... thanks!

@simonbasle sorry about that point then 🙂

I guess nullable return types are a Maybe making the variant list:

  • public Collection<T> getAllUsers() <- return type represented as a Publisher
  • public T getUser(String userId) <- return type represented as a Single. Error if T is null
  • @Nullable public T getUserIfExists(String userId) <- return type represented as a Maybe/Mono
  • public void recordEventForUser(String userId, Event event) <- return type represented as a Completable.

@jroper
Copy link
Contributor

jroper commented Aug 17, 2020

How does this change relate to the stated goal of Reactive Streams (from the README):

It is the intention of this specification to allow the creation of many conforming implementations, which by virtue of abiding by the rules will be able to interoperate smoothly, preserving the aforementioned benefits and characteristics across the whole processing graph of a stream application.

It should be noted that the precise nature of stream manipulations (transformation, splitting, merging, etc.) is not covered by this specification. Reactive Streams are only concerned with mediating the stream of data between different API Components. In their development care has been taken to ensure that all basic ways of combining streams can be expressed.

Note that there is nothing in the stated goal about providing an end-developer facing API, in fact, the second paragraph explicitly excludes such high level API concerns from the specification, and states that the spec is only about low level mediation, ie, runtime concerns. The goal of increased type safety seems to be one that I believe is focused on end-developer experience, not low level mediation, and so this use case, I believe, is not applicable to Reactive Streams.

The JDK already has an asynchronous mono type - CompletionStage. Its semantics are well defined, it supports multiple implementations, it provides much stronger type safety from the point of view of enforcing that only a single element is emitted, and it's even suitable for use as an end developer type. If the goal is to deliver increased type safety across implementations, why not use that? This seems to be an attempt to compete with that.

@OlegDokuka
Copy link
Member

OlegDokuka commented Aug 17, 2020

@jroper

The JDK already has an asynchronous mono type - CompletionStage. Its semantics are well defined, it supports multiple implementations, it provides much stronger type safety from the point of view of enforcing that only a single element is emitted, and it's even suitable for use as an end developer type. If the goal is to deliver increased type safety across implementations, why not use that? This seems to be an attempt to compete with that.

CompletionStage does not represent a lazy-evaluated type. Moreover, as was discussed earlier, CompletionStage missing the fundamental capabilities like cancellation. Thus, this is another valuable disadvantage from my standpoint.

The goal of increased type safety seems to be one that I believe is focused on end-developer experience, not low level mediation, and so this use case, I believe, is not applicable to Reactive Streams.

Indeed, this is a higher-level API, but the world needs it. It has shown the demand for many projects and all well know reactive libraries implemented such a type.

That said, if not reactive-streams would be a holder of such API, then other initiatives will be created instead to provide a common standard Reactive Type for a lazy, cancellable stream of a single or empty value.

@bsideup
Copy link
Author

bsideup commented Aug 17, 2020

@jroper thanks for joining the conversation!

The JDK already has an asynchronous mono type - CompletionStage
This seems to be an attempt to compete with that.

While CompletionStage is indeed a popular type used for representing the result of some asynchronous action, there are two things that make me still want to have MonoPublisher:

  1. lack of cancellation. CompletionStage does not define anything for cancelling the operation, and IMO the strongly defined cancellation is one of the things that make Reactive Streams so nice. And even if toCompletableFuture() is used, due to CompletableFuture's implementation where cancel would just mark it as "ignore the result" and continue waiting for the result, there is a high chance that it will be ignored by the end user since he is not used to cancelling CompletableFutures.
  2. weak protocol - although CompletionStage does not enforce the "hot" nature of it, the JDK uses it as a hot source. If one wants to have a lazy CompletionStage, they need to write quite some code (especially on Java 8), while RS' Publisher (and MonoPublisher, respectively) clearly defines the cold behaviour.

@bsideup
Copy link
Author

bsideup commented Aug 17, 2020

Note that there is nothing in the stated goal about providing an end-developer facing API, in fact, the second paragraph explicitly excludes such high level API concerns from the specification, and states that the spec is only about low level mediation, ie, runtime concerns. The goal of increased type safety seems to be one that I believe is focused on end-developer experience, not low level mediation, and so this use case, I believe, is not applicable to Reactive Streams.

Regarding this one: I would maybe agree if not the Processor type which is just a convenient helper interface that extends both Publisher and Subscriber. Also, I'd say I've seen waaay less usages of Processor in the wild compared to places where MonoPublisher could be used and helpful.

the spec is only about low level mediation, ie, runtime concerns
The goal of increased type safety seems to be one that I believe is focused on end-developer experience, not low level mediation, and so this use case, I believe, is not applicable to Reactive Streams.

the 0..1 specialization is a very important runtime concern that allows the implementations to differentiate potentially infinite Publisher from a singular result. Not only it allows to greatly optimize the operators (e.g. by avoiding a queue, or when it comes to network), but also helps communicating such Publishers between different frameworks (and most of the reactive frameworks have the 0..1 specialization that is not interchangeable at the moment and has to be treated as any other Publisher).

@graemerocher
Copy link

Agreed with @bsideup @OlegDokuka

Even if it is stated in the spec that MonoPublisher shouldn't be used a public contract and only at runtime (which is fine IMO), it is a critical missing piece. There are many cases where the runtime implementation can and should adapt based on whether the emitter is 0..1 or not and surely this is what RS is all about, making it easy for frameworks and tools to implement this kind of interop between reactive libraries, unless this spec is just lip service and in reality developers should just choose one (RxJava, Reactor, Akka etc. ) and stick with it and not expect to be able to interop correctly with other libraries.

@jroper
Copy link
Contributor

jroper commented Aug 18, 2020

@OlegDokuka

Indeed, this is a higher-level API, but the world needs it. It has shown the demand for many projects and all well know reactive libraries implemented such a type.

Even more so, what about map? map is probably the single most common API provided by reactive libraries and used by users, and the world needs it more than MonoPublisher, yet, it's not, and will never be part of Reactive Streams. I don't see how the needs for these types in user facing APIs relates to a need for these types in the Reactive Streams API. If I saw a codebase that was making liberal use of more than one Reactive Streams implementation, I would flag that as a problem. Reactive Streams is an integration API, it's for use at the edges, it allows multiple different technologies to implement the spec to make it trivial to integrate them with any other streaming library. As an end user, when you include a library that doesn't use the reactive streams implementation that you're using generally, then when you get a reactive streams type back from it, you immediately wrap it in your implementations types, and then use your implementations high level methods.

@graemerocher

unless this spec is just lip service and in reality developers should just choose one (RxJava, Reactor, Akka etc. ) and stick with it and not expect to be able to interop correctly with other libraries.

Are you saying that currently, RxJava, Reactor and Akka don't interop correctly between each other when using 0..1 elements? Because if that's the case, we need to put the brakes on right now and fix that, the APIs as they stand today are designed to be able to handle the 0..1 case, indeed there are TCK tests that ensure this.

@bsideup

Regarding this one: I would maybe agree if not the Processor type which is just a convenient helper interface that extends both Publisher and Subscriber. Also, I'd say I've seen waaay less usages of Processor in the wild compared to places where MonoPublisher could be used and helpful.

I agree that Processor is not very useful, but that's not a reason why MonoPublisher should be included, it's a reason why Processor should be removed.

the 0..1 specialization is a very important runtime concern that allows the implementations to differentiate potentially infinite Publisher from a singular result. Not only it allows to greatly optimize the operators (e.g. by avoiding a queue, or when it comes to network), but also helps communicating such Publishers between different frameworks (and most of the reactive frameworks have the 0..1 specialization that is not interchangeable at the moment and has to be treated as any other Publisher).

I agree that some optimisations can be done - though there are plenty of other optimisations too that could be done if we made even more richer types, for example, publishers that are backed by finite in memory collections have some pretty significant optimisations that can be made, failed publishers, subscribers that never back pressure, subscribers that just cancel, subscribers that only take a single element, etc. Where would we stop? And again this is where I come back to the purpose of reactive streams. The primary purpose is interop, performance is important but it is a secondary concern. Facilitating the primary purpose is ensuring that the API is as simple as possible. The fact that reactive streams is only 4 interfaces with 7 methods between them is key to achieving this goal (and as mentioned, one of these interfaces shouldn't even be there). Adding 20% more interfaces will complicate this, it will introduce more edge cases which need to be carefully discussed, and the semantics specified.

The 0..1 publisher optimisations can be implemented without MonoPublisher, simply by providing a helper method that wraps a Publisher in a given frameworks 0..1 publisher interface. Then, that framework can define its own semantics for what to do when the user erroneously passes a 0..n publisher to that method. And that's where I believe performance optimisations belong, it's up to each framework to decide what can be optimised and how it will be optimised. MonoPublisher starts pushing those performance concerns back into the Reactive Streams spec, which muddies the purpose of Reactive Streams.

@bsideup
Copy link
Author

bsideup commented Aug 18, 2020

Are you saying that currently, RxJava, Reactor and Akka don't interop correctly between each other when using 0..1 elements? Because if that's the case, we need to put the brakes on right now and fix that

Sorry, but I don't think we're going to have a healthy and productive discussion if we mock each other and distort words. That's clearly not what @graemerocher said in his comment, and I bet you know that.


The 0..1 publisher optimisations can be implemented without MonoPublisher, simply by providing a helper method that wraps a Publisher in a given frameworks 0..1 publisher interface

You can't do this optimization in RxJava without knowing about Reactor's type.

it's up to each framework to decide what can be optimised and how it will be optimised

Yes, but without a common type, there isn't anything that helps the frameworks to "decide what can be optimised and how it will be optimised"

I agree that some optimisations can be done - though there are plenty of other optimisations too that could be done if we made even more richer types, for example, publishers that are backed by finite in memory collections have some pretty significant optimisations that can be made, failed publishers, subscribers that never back pressure, subscribers that just cancel, subscribers that only take a single element, etc. Where would we stop

We stop at the very low hanging fruit, and it is the MonoPublisher interface. It costs nothing to add, does not change the semantics, but immediately unlocks quite a few possibilities.

Adding 20% more interfaces will complicate this, it will introduce more edge cases which need to be carefully discussed, and the semantics specified.

I would agree here if we change the semantics for 0..1 Publisher, but we don't. What edge cases are you talking about, do you have anything concrete in mind?
Note that we did not propose any new type that would reduce the overhead of subscribe/onSubscribe/request/onNext. Although it is a huge performance optimization, it will indeed require a lot of thinking, edge case hunting, etc etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants