How would you implement Javascript Style Promises in Swift

Hi,

There are roughly two ways to design a function in Swift:

  1. asynchronous (using a completion)
  2. synchronous (using a return value)

Synchronous functions can also run on a background thread to not block UI and still return a value (to avoid endless completion handlers stacked in one another).
Trying to avoid:

doA({
     doB({
         doC({ 
            // ...
         })
     })
})

Getting to this:

DispatchQueue.global(qos: .background) {
   let a = doA()
   let b = doB()
   let c = doC()
}

Looks much better right? Only visually. What if doC() does not need a nor b?
It waits for the executions to finish before executing. Which is bad. In JS this is called an await/async hell.

One could argue why not do both:

doSomething()
doNotWait({

})
doNoWaitEither({

})
doSomethingThatWaits()

However then you always need to use completion handlers to not block and potentially sync if you want to block the execution.

And if you suddenly want to wait for doNotWait you need to wrap it in DispatchGroup or DispatchSemaphore to handle synchronous execution - potentially in multiple parts of your program.
And as soon as you make it synchronous, you need to accept that it blocks other functions that are absolutely not related.

In Javascript there is Promise.all([...]), which waits on all functions in the array to return, without blocking one or the other from executing.
I was wondering if this can be implemented in Swift.

Options would be:

  • Threads
  • Completion Handlers and a DispatchGroup / DispatchSemaphore
  • Future Extension (Combine Framework) [iOS 13 only :confused:]

Any other idea how you would approach this issue?

Here is my use case:

I defined my own API for my server so all requests are synchronous, which means each API function returns the data from the API and then I have something like this:

// on a background queue
API.setVersion(AppSettings.appVersion)
API.getToken(for: user)
API.getSettings()

And neither of those functions needs to wait on the others to complete.

Any help / ideas appreciated!

1 Like

Take a look at PromiseKit, or my preference, RxSwift.

As someone who's always on the lookout for better ways to represent async work, and is currently on the market again, I'm glad this topic has come around again. In fact, I'll likely be creating a new topic about promise-like representations using Combine, but I think there are some general points of discussion here.

There are a few things I almost always want from an async representation:

  • It must be encapsulated so it can be passed around.
  • It must be cancellable.
  • It must be easily chainable.
  • It must, at least optionally, allow synchronous access to its values, if only for testing.
  • It must be flexible enough to encapsulate any work, from low level tasks to high level UI actions.
  • Its learning curve should be as gentle as possible, or at least make it possible to create easier API on top of it.

My favorite for a long time was ProcedureKit, which was one of the Operation-based libraries to come out of @davedelong's WWDC talk, and I presented on it at Swift Summit 2016 (video seems to have disappeared from the internet). I built a few different apps using it, and it was pretty good. My biggest issue with it was that it didn't have a great encapsulation for work completion built in, aside from the various procedure observers. However, I was able to implement a simple promise-type to give me that, and it worked pretty well for my custom procedure classes. The library was very powerful and could even let me do things like chain a user prompt into a location from CLLocationManager into a network request, which I haven't seen from other libraries at all. However, due to the complexity of subclassing Operation reliably, especially early on and in Swift, it's quite complex internally (if starting today it would probably be simpler) and takes a bit of work to customize the way I'd want. Unfortunately it hasn't been well maintained over the last year as the main developer has been busy, and outside developers like myself haven't had time either.

PromiseKit is one of the myriad of promise libraries implemented in Swift. I've used it over the last year due to work on an old Obj-C project which used it. I appreciated its bridging into Obj-C, which most libraries don't do, but that ability certainly makes the library more complex than other promise libraries. Promises provide a nice, easy encapsulation to pass around, which made working with it pretty easy. However, promise libraries don't usually offer the level of system integration I'd want, especially after using ProcedureKit for so long. I do appreciate the simplicity of the promise representation vs. others, but it can still take some time to understand the full extent of the API, as the various promise libraries often have different APIs, even for doing the same things.

Reactive libraries, like RxSwift, ReactiveSwift, and Combine, meet few of my personal requirements, and are so hideously complex that it can take quite a while just to understand them for most developers. However, Apple has given us Combine, so I'm trying to adapt it to my use. Creating a simple encapsulation with access to the values it receives isn't very easy. With SwiftUI though, Combine makes a lot of sense, as it provides direct links to the various SwiftUI observer and state APIs, so plumbing things together can be easy, as long as you do it in the recommended way.

Ultimately async encapsulation is only half the story in modern state management. The other half is being able to observe the result of work without being passed a value, allowing for easy unidirectional data flow and UI updates, whether through bindings or simple closures. This is one area where reactive libraries shine (relatively), as their APIs can be thought of as a combination of async work and observation. With other systems I've always had to combine the async work with a different system for observation (usually a NotificationCenter-based system).

6 Likes
Terms of Service

Privacy Policy

Cookie Policy