Coming from a RAC background I'm thinking it might be the time to make the switch to RXSwift.
I though I'd reach out with a question first though: One of my reservations about Rx is how Observable takes the place of both Signal and SignalProducer. Using 2 different types for theses similar (identical?) concepts makes it explicitly obvious when work happens. How do responsible RxSwift devs handle this complexity/gray area? Is it not as big a deal, in practice, as the RAC community makes it out to be?
Hi @joedaniels29 ,
Since this issue references other projects and can potentially provide a fruitful playground--and I really don't want to waste time and energy on internet wars--I'm really sorry if I'll need to lock this conversation if something goes wrong with this thread.
Expressed opinions are my own, and I can't and don't speak in the name of entire organization.
I'm not a RAC expert, so if there are misinterpreted or wrong claims, I can assure you they weren't made with bad intentions. Since you obviously know more about RAC then me, feel free to correct me.
When you say
Using 2 different types for theses similar (identical?) concepts makes it explicitly obvious when work happens.
... at first glance it makes perfect sense, but once you scratch even shallow under the surface, I don't think this is true or practical. But maybe it is for somebody, I'm just expressing my current opinion here.
ObservableType is more or less equivalent to SignalProducer. There is no compile time guarantee that SignalProducer performs "work", neither performing "work" without a specific context makes sense IMHO.
e.g.
Let's say you have some kind of network API that makes some HTTP request. How would we communicate that this API performs work or not. We could either make it Signal or SignalProducer.
I think most people would choose SignalProducer ofc, HTTP request surely performs side effects and "work", and besides, only SignalProducers have retry APIs.
But what if you are wrapping a GET HTTP request. GET http request should be idempotent and shouldn't ideally cause any server side side effects or work. GET request is usually used just to observe some server state once and immediately unregister. Should it be Signal then, probably not, even though it doesn't perform any work.
There isn't a conceptual difference between making a GET HTTP request, and reading a value from main device memory. The logical operations is the same, only the mechanism is different.
When you consider GET HTTP request on TCP level, yep, side effects are being performed, ports assigned, traffic sent and received, work performed and side effects done.
When you consider GET HTTP request on HTTP or application level, there aren't any side effects or work.
Both "errors" and "work" have no meaning IMHO without context and level of detail you are interested in.
Even when you look at RAC internal code, property has producer: SignalProducer https://github.com/ReactiveCocoa/ReactiveCocoa/blob/cca119d6e95e91ffd2993566e644ac349eeae621/ReactiveCocoa/Swift/Property.swift#L137, and observing property surely doesn't cause side effects ... right ... maybe? ... ¯_(ツ)_/¯ I'm assuming property is there because Signal can't emit initial value on it's own.
Another example ... In the same way like Rx has shareReplay, RAC has replayLazily.
https://github.com/ReactiveCocoa/ReactiveCocoa/blob/cca119d6e95e91ffd2993566e644ac349eeae621/ReactiveCocoa/Swift/SignalProducer.swift#L1429
First observer/subscription will maybe perform "work", second observer/subscription surely won't.
The bottom line is, SignalProducers don't provide you with any additional guarantees then Observable sequences as far as I can tell. Work can be performed, or not.
RAC >= 3.0 has another concept of Signal, and it is finally stateful like the name suggests. Kudos for that, because my brain was so confused when calling Observable in RAC Signal.
So why RxSwift doesn't have something like Signal?
I personally see some problems with Signals.
Their disposal semantics is totally different from SignalProducer semantics. Because of that compositional disposal of Signals, or Signal + SignalProducer is destroyed. For example, you can't automatically unregister from observing something using Signals when there aren't any observers. Using Signals destroys declarative nature of your code IMHO.
Signal also has a hardcoded publish strategy which is not useful for stateful entities. shareReplay strategy is a lot better suited for modeling stream like concepts. But even if it had shareReplay strategy, it would still be hardcoded. The reason why current publish Signal strategy isn't useful for modeling streams is because if you for example bind stream to user interface, it will be initially blank until something changes.
Sincerely, I haven't found cases where I needed something like Signal because I always need to unregister observing mechanism when I'm done, and that means compositional disposal.
When I need some concept that models stateful observable steam and want compile time guarantees that subscriptions/work is shared, I use Driver.
Tried to explain it here
We rely more on users creating their own concepts on top of RxSwift, or just using plain Observable sequences if properties are obvious and can be easily deducted from operator chain.
The closest thing in RxSwift to Signal is PublishSubject. PublishSubject has a guarantee that no work is being performed when observer subscribes.
The difference is that there are no PublishSubject transformations analog to Signal transformations. That's probably because nobody found them useful, but maybe they are. Can't really prove they aren't useful.
There is also a tiny problem with having a stateful core Signal/PublishSubject running through all parts of reactive systems (SignalProducers). Signals cause significant performance penalties since a lot of (possibly kernel level) synchronizations need to be performed all the time.
Thank you so much for taking the time for this explanation! I'll close this for now, but I presume this post will help others 👍 💯
Most helpful comment
Hi @joedaniels29 ,
Since this issue references other projects and can potentially provide a fruitful playground--and I really don't want to waste time and energy on internet wars--I'm really sorry if I'll need to lock this conversation if something goes wrong with this thread.
Expressed opinions are my own, and I can't and don't speak in the name of entire organization.
I'm not a RAC expert, so if there are misinterpreted or wrong claims, I can assure you they weren't made with bad intentions. Since you obviously know more about RAC then me, feel free to correct me.
When you say
... at first glance it makes perfect sense, but once you scratch even shallow under the surface, I don't think this is true or practical. But maybe it is for somebody, I'm just expressing my current opinion here.
ObservableTypeis more or less equivalent toSignalProducer. There is no compile time guarantee thatSignalProducerperforms "work", neither performing "work" without a specific context makes sense IMHO.e.g.
Let's say you have some kind of network API that makes some HTTP request. How would we communicate that this API performs work or not. We could either make it
SignalorSignalProducer.I think most people would choose
SignalProducerofc, HTTP request surely performs side effects and "work", and besides, onlySignalProducershaveretryAPIs.But what if you are wrapping a GET HTTP request. GET http request should be idempotent and shouldn't ideally cause any server side side effects or work. GET request is usually used just to observe some server state once and immediately unregister. Should it be
Signalthen, probably not, even though it doesn't perform any work.There isn't a conceptual difference between making a GET HTTP request, and reading a value from main device memory. The logical operations is the same, only the mechanism is different.
When you consider GET HTTP request on TCP level, yep, side effects are being performed, ports assigned, traffic sent and received, work performed and side effects done.
When you consider GET HTTP request on HTTP or application level, there aren't any side effects or work.
Both "errors" and "work" have no meaning IMHO without context and level of detail you are interested in.
Even when you look at RAC internal code, property has
producer: SignalProducerhttps://github.com/ReactiveCocoa/ReactiveCocoa/blob/cca119d6e95e91ffd2993566e644ac349eeae621/ReactiveCocoa/Swift/Property.swift#L137, and observing property surely doesn't cause side effects ... right ... maybe? ... ¯_(ツ)_/¯ I'm assuming property is there becauseSignalcan't emit initial value on it's own.Another example ... In the same way like Rx has
shareReplay, RAC hasreplayLazily.https://github.com/ReactiveCocoa/ReactiveCocoa/blob/cca119d6e95e91ffd2993566e644ac349eeae621/ReactiveCocoa/Swift/SignalProducer.swift#L1429
First observer/subscription will maybe perform "work", second observer/subscription surely won't.
The bottom line is,
SignalProducersdon't provide you with any additional guarantees thenObservablesequences as far as I can tell. Work can be performed, or not.RAC >= 3.0 has another concept of
Signal, and it is finally stateful like the name suggests. Kudos for that, because my brain was so confused when callingObservablein RACSignal.So why RxSwift doesn't have something like
Signal?I personally see some problems with
Signals.Their disposal semantics is totally different from
SignalProducersemantics. Because of that compositional disposal ofSignals, orSignal+SignalProduceris destroyed. For example, you can't automatically unregister from observing something usingSignals when there aren't any observers. Using Signals destroys declarative nature of your code IMHO.Signalalso has a hardcoded publish strategy which is not useful for stateful entities.shareReplaystrategy is a lot better suited for modeling stream like concepts. But even if it hadshareReplaystrategy, it would still be hardcoded. The reason why current publishSignalstrategy isn't useful for modeling streams is because if you for example bind stream to user interface, it will be initially blank until something changes.Sincerely, I haven't found cases where I needed something like
Signalbecause I always need to unregister observing mechanism when I'm done, and that means compositional disposal.When I need some concept that models stateful observable steam and want compile time guarantees that subscriptions/work is shared, I use
Driver.Tried to explain it here
We rely more on users creating their own concepts on top of RxSwift, or just using plain
Observablesequences if properties are obvious and can be easily deducted from operator chain.The closest thing in RxSwift to
SignalisPublishSubject.PublishSubjecthas a guarantee that no work is being performed when observer subscribes.The difference is that there are no
PublishSubjecttransformations analog toSignaltransformations. That's probably because nobody found them useful, but maybe they are. Can't really prove they aren't useful.There is also a tiny problem with having a stateful core
Signal/PublishSubjectrunning through all parts of reactive systems (SignalProducers).Signalscause significant performance penalties since a lot of (possibly kernel level) synchronizations need to be performed all the time.