Skip to content

Instantly share code, notes, and snippets.

@andersio
Last active August 16, 2018 15:10
Show Gist options
  • Save andersio/dd76b25d7ce803376dab0a188a8c147b to your computer and use it in GitHub Desktop.
Save andersio/dd76b25d7ce803376dab0a188a8c147b to your computer and use it in GitHub Desktop.

We have been through six major iterations of ReactiveSwift since the beginning of ReactiveCocoa 3.0. It is perhaps the time for us to take a step back and review our fudemental hot-observable-centric model that offers an explicit distinction between hot and cold observable.

As a consumer of an observable

Pragmatically speaking, an API consumer given an observable never really cares about anything other than being pushed with events once subscribed.

We currently offer two primitives which are distinct from each other in two areas:

  1. Having triggered a side effect upon subscription or not; and
  2. Whether an observable multicasts its events or not.

From the said API consumer perspective, both these areas are unfortunately NOT an attribute of an observable itself. They have always been the semantics of the API vending the observable, since (1) an API consumer has neither late effect on these semantics, nor (2) has it ever mattered in the whole lifecycle of observation as a consumer. A consumer just expects events, and there is nothing more than that about an observable.

For example, let's consider this piece of API as a consumer:

public protocol ChatService {
    func listAll() -> S<[Conversation], ChatError>
    func fetchMessages(for conversation: Conversation.ID) -> S<[Message], ChatError>
}

Does it really matter to you as an API consumer what S is in particular? As long as it vends the events as the ChatService API promised as part of its interface, whether it is cold or hot does not affect the consumption of the observable.

Even if you know that S has a particular semantic, in most cases one has aboslutely zero control over the behavior, since it is all up to however the owner/vendor wishes its observables to behave. The "exception" is only when the owner/vendor has extra API surface that could affect these observables, but then it is beyond the concern of any observable primitive, as it is the semantic of the API who just happens to use these primitives to deliver the results.

So from this point of view, we have been effectively encouraging our users to leak their API implementation details to their API consumers, despite having offered no value to any of the API consumer.

As a producer of an observable

Where the split matters is when one looks from a producer perspective — it offers an explicit documentation value in whether a particular observable your API vended is hot or cold.

This is however the only advantage it has. It is weighed against a variety of issues, including but not limited to:

  1. Twice the API surface

    Users are provided with two sets of almost-identical API given the split.


  2. Interoperability


    While we have been improving in this area over time, interoperating between entities has not been easy, especially in light of the limitations of the Swift generics model.

  3. Conceptual confusion

    With (1) and (2), the distinction has been a source of confusion for beginners to grasp.


  4. Implementation details

    As mentioned in the previous section, these observable semantics are implementation details of the API from an API consumer perspective.

As a framework implementor

The on-going maintenance cost of an API surface twice large is growing. For example, to improve the interoperability experience without disruption in contextual lookup, we have to introduce a more specific overload to every generic operator.

Does hot observables have to the centre of the universe?

The explanation actually works both ways:

  1. Hot observables are the base.

    Cold observables build a "sandboxed" graph of hot observables for every subscription, and execute a starting side effect afterwards.


  2. Cold observables are the base.

    A hot observable is just a cold observable connecting you with an event source as its starting side effect.

We just happened to choose the first explanation, motivated by its documentation value for API producers, and build the entire framework around it.

Could we arrive at a better future?

We all love ReactiveSwift for its simplicity as compared to competing frameworks. But even the simple ReactiveSwift itself still carries baggages that have been recognized as a source of confusion, and might have been less sensible as we iterate, learn and recalibrate our direction.

For us to move forward with a better development experience, I considered these being two important criteria:

  1. Does a split in this form offer us any value in spite of all downsides it has caused?
  2. Is it justifiable for the framework to make a drastic disruptive turn after six major iterations?

What the better future could be?

?

@leonid-s-usov
Copy link

wow this is radical
.
.
.
a good one! Needs time to digest.

@leonid-s-usov
Copy link

Having slept over it I don't think we are anywhere close to THE P R O B L E M. Everything's cool ;)

I believe that the division between signal and signal producer is great and helpful. Maybe there is a better way to handle it, like by creating an "unsafe" mixed signal and make some kind of type wrappers for it to make the intent explicit in the code? But that's implementation details.
More than that, isn't this the task of a framework and its contributors to engineer some useful APIs, especially if it takes effort and ingenious tooling to produce and maintain the APIs?

IMO, the only "issue" we may be facing now - having this interface in place we realise that it's time to go and implement the next layer providing the missing / wanted functionality and behaviour.

Let's get back to a RACSignal for a moment, and, as a consumer of an API - can you tell if its safe to subscribe for it twice, or not?
You may only trust your API provider and think "ok, these guys must know what they are doing. So let me subscribe twice to this potentially non-idempotent action and hope that they won't execute it again just because I want to update this other component of mine"

Your example above simply manifests the concern which has brought myself to this forum about a week ago - I was in a search for a Promise like API on top of RAS.
That's it. The ChatService interface your are giving simply requires this clean "single value replaying producer" which may be implemented following the Promise pattern. Please check out this proposal

Whether it's the hot or cold observable which is selected to "be the centre of the universe", we still clearly need the distinction between the two.

If our users need that, we may provide a wrapper which hides the difference but that's should be built on top and with the help of the clearly separated base classes.

@liscio
Copy link

liscio commented Aug 16, 2018

Below are my answers to your "two important criteria."

  1. Does a split in this form offer us any value in spite of all downsides it has caused?

I interpret this question as, "Does it still make sense to expose both a hot & cold observable, represented by Signal and SignalProducer?"

And my answer is Yes. The simple fact is that there are plenty of situations where you would want to expose a Signal over a SignalProducer. I don't do it very frequently, but there are enough examples in my code where this was the best way to solve a particular problem.

  1. Is it justifiable for the framework to make a drastic disruptive turn after six major iterations?

As someone that has quite a lot of code that depends on ReactiveSwift these days, I have yet to find such a major flaw in its design/terminology/etc that has caused me to complain loudly, or flee to another pattern/library, etc.

It has all the pieces I need—hot & cold signals, and a large collection of operators that let me control and manipulate them.

It also gives us plenty of rope to hang ourselves with. But is that such a bad thing? Look at Grand Central Dispatch as a great example of this. When it was introduced, we had an amazing API that helped us build massively parallel software! But it's easy to:

  • Accidentally spawn dozens (and thousands) of threads
  • Deadlock
  • Call UI code from a background thread
  • Etc…

Just because it's easy to create trouble while using ReactiveSwift, or "not familiar enough", doesn't mean that something is fundamentally wrong with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment