What's up with the Combine framework?

From my PoV it is very simple, I guess neither Apple or other companies want to spend time to cover not their platforms. At the same time if somebody want to use some open sourced reactive framework then he/she already has plenty of choices (Rx, ReactiveCocoa, etc).

I understand that DisposeBag is a convenience tool for working with observables. What I'm speaking about is that the amount of bookkeeping in RxSwift in general (DisposeBag being a best case) is high when compared to other methods. It feels like going back to manual memory management.

The backlash if they had done significant engineering and API change on Rx would have dwarfed the complaints about Combine.

5 Likes

huh?

Binary size is a concern but not the main. The functionality is to big for chaining simple tasks. DispatchGroup does a much more lightweight approach for small tasks. I always get stuck after some days of trying RxSwift. I think Combine has the same problems but maybe not the stack trace problem. Have to experiment.

This is a bit of a trap, though. Dispatch is fine for the simple stuff, until your requirements grow (which they always seem to) to a point where you find yourself painted into a corner called callback hell.

What is it that typically gets you stuck?

This is not what I mean. Callback can get a mess but RxSwift in every project I use is worse because of the stack trace for one and second because of the naming convention.

Lets first clarify the code currently needed to do 3 parallel request and know one response


let dispatchGroup = DispatchGroup()

// Every object below has its own Queue and a different Model for its result

let request1 = Request(dispatchGroup)
let request2 = Request(dispatchGroup)
let request3 = Request(dispatchGroup)

var result1: Type1?
var result2: Type2?
var result3: Type3?

request1.attempt { result in result1 = result /*...*/}
request2.attempt { result in result2 = result /*...*/}
request3.attempt { result in result3 = result /*...*/}

// The code below will only get called when all 3 are finished
dispatchGroup.notifyMain { 
// or something else. Point is this is type safe, all results have been set and on an error I can handle that in the individual handles above
print(result1 + result2 + result3) 
exit(EXIT_SUCCESS)
 }

dispatchMain()

So to your second question this is where I always get stuck. I try to write this code that actually works with RxSwift. The problems I experience are

  1. I never know how to start a request. I have to read a few minutes and then I now a request is called something more abstract its an Observable.
  2. Then I create 3 observables for my request. Every observable has a different data type as a response.
  3. But then I still need to start the request. Above this is calling attempt on the request. But for RxSwift I need to create a publisher I think.
  4. So I have the publisher and then I need to combine the result of all 3 in one. I have combineLatest or anything else but then the types gets messed up.
  5. So I have to map inbetween to something common so I can then print it together
  6. Then I worry about memory management an try to figure out where to put the dispose bag.

I get stuck in figuring out why my code using DispatchGroup is not simple enough. Because my code is

  • type safe
  • has a readable stack trace while debugging
  • memory is managed by swift

I would really appreciate any opinion on this because I would like to know but I do not see it. Thanks

2 Likes

Hello. Without seeing an example of your Rx code I can’t gauge how far along you are with it, so if this is either condescending or too complicated I apologise. I’d be happy to explain further or chat about Rx/Combine.

This is an example of how I would tackle your problem using FRP, in this case RxSwift.

typealias CombinedResponses = (Result<String, RequestFailure>, Result<Bool, RequestFailure>, Result<Int, RequestFailure>)

func exampleWithTrigger() -> Observable<CombinedResponses> {
    let requestTrigger: Observable<Void> = .just(())

    let request1: () -> Observable<Result<String, RequestFailure>> = { .just(.success("1")) }
    let request2: () -> Observable<Result<Bool, RequestFailure>> = { .just(.success(false)) }
    let request3: () -> Observable<Result<Int, RequestFailure>> = { .just(.success(1)) }

    return requestTrigger
        .flatMap { _ in
            return Observable.zip(request1(), request2(), request3())
        }
}

func exampleWithoutTrigger() -> Observable<CombinedResponses> {
    let request1: () -> Observable<Result<String, RequestFailure>> = { .just(.success("1")) }
    let request2: () -> Observable<Result<Bool, RequestFailure>> = { .just(.success(false)) }
    let request3: () -> Observable<Result<Int, RequestFailure>> = { .just(.success(1)) }

    return Observable.zip(request1(),
                          request2(),
                          request3())
}

You can subscribe to the results of either of these functions to kick off the three network requests and have access to all three responses.

Your points about the stack trace are valid, but I've found the debugging tools included with RxSwift have helped me solve all my issues up until now without having to rely on the stacktrace. In particular the .debug() method.

When you say you want your memory to be managed by Swift, using DisposeBags does not mean we're manually managing memory. When we subscribe to an Observable in in RxSwift the return type of the Subscription is Disposable which exposes one method, dispose(). This allows us to do things like cancel in flight network requests when we're no longer interested in an Observable.

Dispose bags give us a convenient way to store Disposables, and when the Dispose bag is itself deallocated it calls dispose() on each Disposable in its deinit.

These examples are fairly trivial, so if it would help I'm more than happy to flesh them out a bit.

3 Likes

Hey doozMen, I appreciate your time for writing that up.

There are fair criticisms in there, but they're too heavily mixed in with a familiarity for one framework (Dispatch), and not the other, making it a completely unfair comparison.

Further, this example is rather cherry-picked, choosing a case that Dispatch is ideal for. If you add any real world complexities (read: necessities), you'll find it getting MUCH harder using Dispatch. Some examples include:

  1. Retry logic, with back off (exponential + random jitter)
  2. Throttling API requests
  3. Actually using the resulting value. Because this is async code, you can't just return it (well, you can, with Rx ;) ), so you're forced to use some other scheme. In this example, print is acting like a completion handler, which is fine for a demo, but horrible for real life projects. You just end up closure parameters for every async API, and you find yourself in the pits of call back hell (farewell nice stack traces)

point-by-point response:

  1. It's not obvious, and I think the Rx docs are lacking, but this point is more of a testament to your familiarity with Dispatch, and non-familiarity with Rx.

  2. Didn't you imply at the end of your post that Rx isn't type safe? :face_with_monocle:

  3. Nope, just a triggering observable, see mikevelu's example

  4. What do you mean by "types gets messed up"?

  5. This would be an issue in a language like Java, where you'd be forced to write a class (with constructor, fields, getters/setters), but in languages with anonymous types (like tuples in Swift), this is no problem at all. If anything, it's a benefit, you have one programmatic entity (one tuple value) for one conceptual entity (one aggregated async computation result).

  6. It depends on the usage context, but possibly yes. I think this is one area where Rx docs are lacking, because people have this notion that dispose bags are some kind of complicated voodoo, when they're really just an array of subscriptions, held by the dispose bag on your behalf to keep them alive.

...

  • Could you elaborate on this? What part of Rx isn't type safe?

  • In this trivial example, yes. But I think that's an unfair comparison. Put this in the context of the callback hell you would find it in, and then compare.

  • Is this implying that Dispose bags aren't Swift code? I'm not exactly sure of the argument here. I don't see the difference (in terms of inconvenience) between a dispatch group and a dispose bag. They have almost the same syntax (one initialization, followed by one call per thing that's involved in the computation)

1 Like

So, honestly, I have a problem with the Combine framework. To quote Wikipedia (emphasis added):

In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change.

Reactive streams are a programming paradigm; it's a real stretch to consider them part of the OS. There is a big difference between Combine and OS libraries such as SwiftUI or CryptoSwift. Many other languages are incorporating streams/observables in their standard/core libraries (e.g. Java 9, ECMAScript, .Net).

To put it another way: if the community proposed their own reactive stream design for the standard library, I think it would be met with broad support. The primary reason nobody has done so is due to lack of a real plan for concurrency.

If Apple is adopting the reactive paradigm, it means that lots of Swift code will be using it. If they don't want to upstream Combine, I think it would be worth exploring our own design for reactive streams in the standard library.

Another thing to consider is: what would happen if we did propose adding reactive streams to the standard library? Would Apple oppose it because Combine also exists?

9 Likes

Isn't it exactly the "make our own" attitude that is the reason Apple products are so eminently usable?

I love RxSwift but they had to write everything on top of the public face of Swift, with all that entails for performance and debugging. Apple are said to have been working on Combine since before Swift, and they can use and make internal APIs that makes Combine significantly faster than what RxSwift could ever achieve.

InMost of all, I have a hard time seeing Apple building crucial, not to say transformative, internal frameworks on an external open source project maintained by a very small group of people, no matter how dedicated they have been.

Something that would have made more sense would be to recruit the maintainers, but I'm not sure that would be appreciated either.

But why shouldn't Apple make their own? They can do it much better than anyone else.

It's interesting if the community could create an open source version with the same APIs as Combine Framework that would be available on lower iOS versions and other platforms that don't have support for it.

2 Likes

I’m actually working on this thing (mostly for educational purposes). It’s very much work in progress, and you can do little with it, but the groundwork is laid. I’ve also set up the cross-platform testing and benchmarking infrastructure.

Feel free to try it: https://github.com/broadwaylamb/OpenCombine

11 Likes

mikevelu and @AlexanderM thanks for the informative responses. Caught up in work this week. Will come back to this interesting topic this weekend. Have a good week.

1 Like

Apparently they have been working on Combine for 6 years and it is said to be built on lower level stuff than Foundation. I'm not sure why you are so unhappy with Combine that you want to build your own alternative, but given the enormous effort Apple has put into it, and their unmatched access to the language internals, whatever the community builds will be vastly inferior.

In my opinion, the GCD example you'e responding to is much more readable than this reactive implementation. The Rx example has much more boilerplate, and relies on an understanding of the implicit behavior of observables, which is not at all obvious. I'm not sure why I would want to give up readable stack-traces and simple, imperative code for... which benefits exactly?

With all due respect, I don't understand how this could be considered preferable in any way.

My issue with DisposeBags is not that they are "complicated voodoo", but that subscription management is a layer of manual book-keeping which is required in Rx, but not in other paradigms (as demonstrated by @doozMen's example). It feels like a step backwards: like going back to manual memory management after GC/ARC.

With all due respect, my experience with Rx in large projects is that it doesn't save you from any sort of "hell" - you still end up with lots of closures and indirection. The main difference is that with Rx your stack-trace isn't helpful, and you are working at higher levels of abstraction, so it's less easy to reason about your code. Instead of problems being the result of my own faulty logic, they emerge from something like an incorrect assumption about how a reactive chain is executing.

You could say that "good Rx code" doesn't have these issues, but the same could be said for well written callback-driven code. It's not at all clear to me what concrete benefits are provided by Rx which justify the downsides.