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?

?

@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