What's up with the Combine framework?

It’s worth pointing out that reactive programming does not inherently necessitate manual bookkeeping. Unfortunately this is how all of the popular libraries are designed and Combine copied that design.

For a count-example, see the talk Do It Yourself FRP. The design used here is to provide a context object that specifies an upper bound for a subscription’s lifetime at the point of subscription. The context is also captured weak. When an event is arrives it is upgraded to a strong reference that is provided to the observing callback (a sink in Combine terminology).

It is possible to go even further than this talk does, having the context object specify a scheduler used to invoke the callback using a ScheduledObject protocol. This approach is inspired but the actor model where objects are associated with a concurrency context. It avoids the potential (and relatively common) mistake of forgetting to use receive(on:) to move to the correct queue.

But what if you want to terminate a subscription before the primary context object is deinitialized? There are a variety of solutions to this. One is to use subcontext keypaths. This approach retains the benefits of providing the primary context to the callback and automatically managing scheduling while allowing the subcontext to be released before the primary context. In this approach the primary context continues to provide an upper bound on the lifetime of the subscription.

Another approach to decoupling subscription lifetime from a context object is cancellation tokens. This approach is very flexible, allowing the subscription lifetime to be bounded in arbitrary ways, while still requiring subscribers to specify a bound of some kind.

Designs that require subscriptions to have a bounded lifetime are really important IMO. It’s relatively easy to accidentally lead subscriptions with the Disposable / Cancellable pattern. Depending on the subscription these leaks can be relatively inconsequential or they can be serious bugs.

In the library I work on, some subscriptions are backed by an event stream coming from the cloud. The stream is filtered on the cloud based on active subscriptions. Some subscription configurations result can result in a lot of data coming across the network. This is absolutely necessary to support some of our use cases, but it is totally unacceptable for these subscriptions to leak. The Disposable / Cancellable pattern is unnecessarily fragile and unworkable in this context.

So I agree that subscription management in Combine is suboptimal and should not require manual bookkeeping to get right. But I think it’s important to distinguish this flaw from reactive libraries. It’s probably too late to change now, but it would have been possible for Combine to go in a different direction. (I would love to be wrong about it being too late to change)

5 Likes

Not really. Dispatch groups are usually wholly contained within the scope of the one method where they're used. With dispose bags I end up with an extra bookkeeping member variable on all my types which use observables.

Also dispatch groups are only needed for more complex, branching cases with Dispatch. With observables I always have to worry about cleanup.

Reactive is perfect if you deal with unsolicited events. But if you trigger an asynchronous request, Promises etc. are often the better and simpler choice to process a response.
I really have hoped, that async/await makes it to the language first to build actors and reactive on top of that.

5 Likes

I agree with those expressing concern about the complexity of Rx frameworks, including Combine. 90% of the time you don't need 90% of their features, leading extreme complexity for simple scenarios. With Combine being integrated into the OS, perhaps there are opportunities for optimization that other libraries don't have. But really I was hoping that Apple would have come up with more clever, high level APIs to minimize the complexity developers see. But perhaps the community will build it, and things like @Published and the other property wrappers will help. I think judicious use of Combine will help manage the complexity of apps using it while still getting some of the benefits.

3 Likes

Combine is a low-level framework. If you did try SwiftUI for instance, there is zero complexity to use the system that is based on Combine. And I guess this will be the case for others high-levels API based on Combine.

2 Likes

As I noted, reactive programming is a popular, modern programming paradigm which many languages bigger than Swift are incorporating in their standard libraries. It would be reasonable to suggest that concepts such as publishers and subscribers should be incorporated in to Swift's standard library, too.

Nothing prohibits Apple optimising the implementation on their own platform. The Swift standard library is also shipped with Apple's OSes and their version of it can benefit from private OS APIs and other optimisations.

Unfortunately, it seems to me that Combine shuts the door to reactive streams in the Swift standard library, at least until Apple are ready/willing to change their current stance (if they ever are).

2 Likes

Are any of the SwiftUI features actually based on Combine? The two bridge pretty seamlessly, but I'm not sure SwiftUI is implemented on top of Combine.

I believe @State is implemented using Combine, and that seems rather necessary for making a SwiftUI interface actually respond to user input.

Binding which is use everywhere in SwiftUI is implemented over Combine.
State change tracking may be too, but it is not exposed in the API, so this would be an implementation detail.

Many piece of SwiftUI are Subscribers, and the user is just supposed to bring a Producer. SwiftUI hides most of the complexity and let the user do the easy job.

2 Likes

Watch the "Data Flow Through SwiftUI" talk for details, but the answer is yes.

Ah yes, especially with the various Publisher integrations. Plus, I can see by the call stacks from my SwiftUI crashes (you can't directly mutate @Published values lest you run into the exclusivity checker), there are bits of Combine in there. So SwiftUI does indeed provide a bit of a layer of Combine for UI stuff, and hopefully I can figure out the rest of the various property wrappers to do the rest.

@Spencer_Kohan sorry for the delay in replying, it's been a busy week!

That’s interesting, we’re having the exact opposite experience. Every PR we put up which refactors a piece of functionality in Rx has resulted in less code & less state. We currently use the MVVM architecture and the majority of our VIewModels are no longer modelled as classes or structs, but literally just a function. This has allowed us to get 100% test coverage in our VIewModels with ease, and so far it’s been a good fit for TDD.

Can I ask how big the teams were working on the projects utilising Rx? Was there significant usage of Subjects?

The first company I worked for which used Rx in 100% of the codebase was a negative experience for me. So much so that I was firmly against using Rx in the project I’m currently working on (I eventually agreed to use it in very small, well defined, holes but it’s been such a pleasant experience we’re deliberately leaking it out into anywhere it makes sense to have it).

The problem I had at the first company was the usage of Rx wasn’t idiomatic, and they hadn’t nailed the on-boarding onto Rx yet so a lot of developers were still grappling with the concepts. I’d be interested to hear any particular pain points in projects you’ve described.

That’s a fair point, I agree with your point about the Observable abstraction. Using the Single trait here may have made more sense. Although I would also argue that in a correctly designed reactive system, whether we use Observable or Single doesn’t really matter.

Another interesting point you made which I'd like to delve into, is your point about Rx not being intuitive if you haven't spent at least some time with Rx. I'm in two minds about that. Do we consider technology a failure if it is not intuitive? In almost all cases I would argue yes, but there are some nuances here. In my opinion, the reason I and others I've worked with have struggled to grasp the concepts behind Rx is due to the paradigm shift. My education in CS was almost entirely spent learning OOP/OOD and applying/using reference sementics and types respectively.

I struggled with FP initially but after stepping back and slowly beginning to understand the paradigm, I firmly believe I have a clearer mental model of programming in general. I would say that as someone coming from an OOP perspective FP is not intuitive at all, but I don't think that makes it any less valuable. I would also say the same thing about Rx/FRP.

I still wouldn’t say that having a dependency with x amount of lines of code, and having Schedulers manage control flow affects the simplicity of code we write. I do agree though that the size of Rx is a problem (not really their fault, being constrained to only the public interface of Swift presents its problems), and having to learn how Scheduler works is just another notch in the notorious Rx learning curve.

I know you disagree, but in my opinion the example provided as an answer to the DispatchGroup scenario shows the benefits of Rx. Having one abstraction for all asynchronous work has been excellent in my experience with Rx. No more having to decide between the delegate pattern/KVO/GCD/injecting closures/completion handlers/Notification Center etc.

I’ll try provide a concrete code example of something I’ve been working on recently. I was implementing a basic form, specifically what happens when the user submits after making some changes. There were a few steps:

  1. Handle the button tap
  2. Get the data from the form
  3. Transform the data into an object we can use to post to the API
  4. Make the API request
let updateContactInformationApiResponse = viewInput
            .submitForm // <- 1
            .withLatestFrom(Observable.combineLatest(viewInput.mobileNumber, viewInput.landlineNumber, viewInput.marketingConsent)) // <- 2
            .map(ContactInformation.init) // <- 3
            .flatMap(updateContactInformation) // <- 4 (updateContactInformation is a closure passed in as a parameter which takes an instance of ContactInformation and makes the network call)
            .share(replay: 1)

The equivalent non-Rx implementation would require more code, and multiple decisions to handle the asynchronous work.

4 Likes

I always thought RXSwift API looks like Android API: onNext, onError, onCompleted... Those make me think of onCreate, onDestroy, onWhatever... It makes it feel RXSwift was just a port of RXJava. Then there are the vaguely named subject types like PublishSubject and BehavioralSubject, which are completely non-intuitive. Combine uses PassthroughSubject and CurrentValueSubject which are completely self-explanatory. There's also the fact that Combine is built into the language, which will always give it an advantage, not just from the seamless integration in code, but it also removes the dependency to a 3rd party library.
But for me, the real clincher is the performance. Combine far outperforms RXSwift in both time and space complexity (Problem Solving with Combine Swift | by Arlind Aliu | Medium). Because Apple is the creator of Combine, it's unlikely that any 3rd party would be able to outperform it, at least not in the long-term, since Apple has access to everything.

1 Like

How is it built into the language? Isn't it just a framework in the Apple platform SDKs?

2 Likes

It is. Combine uses some new language features that may start to appear when Swift 5.1 is released, but it is based on Apple core frameworks. It is, as of today, only available on macOS/iOS/tvOS/watchOS, not Linux, Windows, or Android.

Just like UIKit is a framework.

But UIKit is not part of the Swift language.

2 Likes

Sure. And UIKit isn't built into the language either. I think you are confusing Swift-the-language (and possibly Swift-the-standard-library) and Apple's various frameworks.

1 Like

Also, I forgot to mention. Combine requires you individually memory manage each individual subscription (the return values of type Cancellable). That's exactly what DisposeBag automated. Not having an equivalent isn't a good thing.

What does DisposeBag give you that [AnyCancellable] does not?

1 Like