I've spent the better part of the day banging my head against the wall over this, and I think I finally understand it. Here's the question I wish had existed this morning when I started looking.
For background, I have several years of experience working with C++ and Python, and I've recently started learning Swift for non-iOS development. Everything I will show here seems to behave the same on a MacBook Pro as on my Ubuntu PC. I'm running Swift 5.4, compiling and running with Swift Package Manager in the command line.
I've read through several articles about using Promises in Swift, and it's not adding up. The examples they show make it seem like you can just call firstly
, chain together a few .then
calls, and then tie it all up with a .done
and a .catch
, and everything will just work. When I try this, I mostly get a bunch of errors, but sometimes I get lucky enough to have it compile, only to find that nothing is happening when I run it.
Here's an example main.swift
file that illustrates my confusion.
import PromiseKit
firstly {
Promise<String> { seal in
print("Executing closure")
seal.fulfill("Hello World!")
}
}.done { str in
print(str)
}
When I run this, it just prints Executing closure
, even if I add a sleep after it, or if I call .wait()
on the result. Why does the firstly
block execute but not the done
block?
Here's another one. It took me a while just to get it to compile, but it still doesn't do what I expect it to:
import Foundation
import PromiseKit
let url = URL(string: "https://stackoverflow.com/")!
enum SampleError: Error {
case noResponse
case badCode(Int)
case couldNotDecode
}
firstly {
Promise<Data> { seal in
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
return seal.reject(error)
}
guard let data = data, let response = response as? HTTPURLResponse else {
return seal.reject(SampleError.noResponse)
}
guard (200..<300).contains(response.statusCode) else {
return seal.reject(SampleError.badCode(response.statusCode))
}
seal.fulfill(data)
}.resume()
}
}.then { data -> Promise<String> in
Promise<String> { seal in
if let str = String(data: data, encoding: .utf8) {
seal.fulfill(str)
} else {
seal.reject(SampleError.couldNotDecode)
}
}
}.done { str in
print(str)
}.catch { error in
print("Error: \(error)")
}
I want this to print the HTML contents of the Stack Overflow home page, but instead it prints nothing.
It seems that I have all the setup in place, but the Promises are not running. How to I get them to actually execute?
It seems that the problem here is that all the online articles focus on iOS development, and don't really address Promises in a desktop/server executable. Simple scripts are intended to terminate, whereas mobile apps are designed to run indefinitely.
If you dig into the Thenable
code, you will find that .then
, .done
, .catch
, and so forth have an argument for a DispatchQueue
, which, digging deeper, defaults to DispatchQueue.main
. Because the top-level code in these examples is utilizing the main thread, the main DispatchQueue
never gets an opportunity to perform the work assigned to it. Both of these examples can be easily made to execute the chain of Promises by yielding the main thread to the dispatch system by calling dispatchMain()
at the very end.
However, dispatchMain()
never returns, so it's not a good choice for an executable that is intended to run to completion. The better solution is to designate an alternate queue to execute each of theses promises. For a single task, we can use a semaphore in a .finally
block to signal that all the work is done. For multiple tasks, I suspect you would need to use a DispatchGroup
.
Here's an updated version of the first example:
import Foundation
import PromiseKit
let queue = DispatchQueue.global()
let semaphore = DispatchSemaphore(value: 0)
firstly {
Promise<String> { seal in
print("Executing closure")
seal.fulfill("Hello World!")
}
}.done(on: queue) { str in
print(str)
}.catch(on: queue) { error in
print("Error: \(error)")
}.finally(on: queue) {
semaphore.signal()
}
semaphore.wait()
And here is the working version of the second one:
import Foundation
import PromiseKit
let queue = DispatchQueue.global()
let semaphore = DispatchSemaphore(value: 0)
let url = URL(string: "https://stackoverflow.com/")!
enum SampleError: Error {
case noResponse
case badCode(Int)
case couldNotDecode
}
firstly {
Promise<Data> { seal in
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
return seal.reject(error)
}
guard let data = data, let response = response as? HTTPURLResponse else {
return seal.reject(SampleError.noResponse)
}
guard (200..<300).contains(response.statusCode) else {
return seal.reject(SampleError.badCode(response.statusCode))
}
seal.fulfill(data)
}.resume()
}
}.then(on: queue) { data -> Promise<String> in
Promise<String> { seal in
if let str = String(data: data, encoding: .utf8) {
seal.fulfill(str)
} else {
seal.reject(SampleError.couldNotDecode)
}
}
}.done(on: queue) { str in
print(str)
}.catch(on: queue) { error in
print("Error: \(error)")
}.finally (on: queue) {
semaphore.signal()
}
semaphore.wait()
Interestingly, the firstly
block seems to execute synchronously on the main thread. It occurs to me that if you had a lot of tasks to run, it might be best to do as little work as possible inside of the firstly
handler, and free up your main thread so it can spin up the tasks on the other threads more quickly.