A Swift Promise

Although having been known since 1976, the futures and promises pattern gets a new breath of life as the collective programming body of knowledge moves away from thread-spawning into preference of asynchronous processing (refer to the c10k problem). It appears that node.js stack and community have been in the forefront of this renaissance of promises attributed to the number of subsequent libraries in other languages that emulates how JavaScript does asynchronous-ness.  Probably this is rooted  from the single-threaded nature of JavaScript as a programming language and not many other imperative languages has direct support of parallelism (most, maybe except for Ada, has parallelism mostly done as library calls and not built into the compiler).

Promise to deliver this vehicle

In the sphere of Apple computers programming, there have been a number of libraries (e.g. BrightFutures) or frameworks (e.g. ReactiveCocoa) implementing the future/promise pattern. However you rightly wouldn’t want to include such complex libraries or frameworks into your project when your needs are fairly modest — even if they are open source. Adding more dependencies into your project means increasing the risk of maintenance burden later on. There’s no guarantee that these libraries are going to be maintained to remain compatible with changes in the operating system API or language features (e.g. Swift 3.x to Swift 4.x). There’s not even any guarantee that the maintainers will keep their own API stable, and this impacts your code and could be detrimental to your own productivity during maintenance (this latter part is probably more relevant for those developing products but less so in the world of one-shot consulting gigs). Hence if you expect to maintain the project for a long period of time — over two operating system releases or so — you should practice restraint on adding 3rd party dependencies.

I’ve recently found futures/promises to be useful in state preservation and restoration, notably with respect of coordinating UI state restoration of a tvOS app with loading the associated data from persistent storage. Having a promise in place of the persistent store object greatly simplifies code paths handling different orders that persisted data can be shown on-screen.  This was part of my Wake on LAN app for the Apple TV and a future object is used to wrap references to the primary persistence manager.

Normally without state restoration, data are loaded first and then the user interface is displayed second. However when the user interface was just restored, then the it gets created and shown before the time data are loaded from persistent storage. During restoration time, as soon as the application completes initialization, every view controller that was preserved gets restored — regardless whether the underlying data store was ready or not during restore time. In turn, those user interface elements are expected to configure itself from information that was given out when it was preserved. Likely the persistent store would only be available after restoration has completed. That’s why those view controllers would need to handle two cases of data loading time — the normal case in which data were readily available at viewDidLoad and the restoration case in which data were only available some time later after it had been shown on screen.

User Interface Restoration Comparison

This diagram compares the “typical” case vs the “restoration” case. Typically the persistence layer (e.g. Core Data stack) have been set up and the data items are loaded prior to displaying the view. However the restoration mechanism instantiates, restores, and displays the user interface prior to handing it back to application code. Hence during restoration, views gets initialized with dummy data obtained from the restoration information and then reloads itself from the “official source” when the latter becomes available.

Since my needs of futures for this project were pretty modest, I shied away from bringing in complex libraries for it. Indeed, my bare-bones implementation for future/promises is only about 100 lines. The public interface of the class was loosely inspired by Java’s CompletableFuture class but only with the methods that I needed at the time.

//
// CompletableFuture.swift
//
//
// Copyright 2017 Sasmito Adibowo
// http://cutecoder.org
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import Foundation
/**
Holds the result of an asynchronous operation.
This is roughly based on Java's CompletableFuture but only implement a small subset of it.
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html
*/
public class CompletableFuture<PromisedType> {
var promisedObject : PromisedType?
let promiseQueue = DispatchQueue(label:"com.basilsalad.promiseCompletion")
var completionBlocks : [(_:PromisedType) -> Void]?
let completionQueue : DispatchQueue
public var canceled : Bool {
get {
var v = false
promiseQueue.sync {
guard promisedObject == nil else {
return
}
v = completionBlocks == nil
}
return v
}
}
public init(completionQueue: DispatchQueue = .global() ) {
completionBlocks = Array()
self.completionQueue = completionQueue
}
/**
Returns the promised object or nil if it doesn't exist yet.
*/
public func getNow() -> PromisedType? {
var result = nil as PromisedType?
promiseQueue.sync {
result = self.promisedObject
}
return result
}
/**
If not already completed, sets the value returned by get() and related methods to the given value.
Else does nothing.
*/
public func complete(value : PromisedType) {
promiseQueue.async {
guard let completionBlocks = self.completionBlocks,
self.promisedObject == nil else {
// already completed or canceled
return
}
self.promisedObject = value
completionBlocks.forEach({ (handler: @escaping (PromisedType) -> Void) in
self.completionQueue.async {
handler(value)
}
})
self.completionBlocks = nil
}
}
public func cancel() {
promiseQueue.async {
guard self.completionBlocks != nil else {
return
}
self.completionBlocks = nil
}
}
/**
Adds to the list of blocks to call when the value is completed.
If the promise is already completed, the block will be called immediately.
The block will be called asynchronously from the `completionQueue`.
*/
public func addCompletion(handler: (@escaping (_ :PromisedType) -> Void) ) {
promiseQueue.async {
if let value = self.promisedObject {
// already completed
self.completionQueue.async {
handler(value)
}
} else {
// enqueue
if self.completionBlocks != nil {
self.completionBlocks!.append(handler)
}
}
}
}
}
//
// CompletableFutureTest.swift
//
// Copyright 2017 Sasmito Adibowo
// http://cutecoder.org
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import XCTest
@testable import tvCommon
class CompletableFutureTest: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testSingleBlockResponse() {
let testValue = NSNumber(value: 42)
let testFuture = CompletableFuture<NSNumber>()
let triggerPeriod = TimeInterval(0.1)
let completeExpectation = expectation(description: "Completion triggered")
testFuture.addCompletion { (givenValue : NSNumber) in
XCTAssertEqual(testValue, givenValue,"Given Value does not equal test value")
completeExpectation.fulfill()
}
DispatchQueue.global().asyncAfter(deadline: .now() + triggerPeriod) {
testFuture.complete(value: testValue)
}
waitForExpectations(timeout: triggerPeriod * 2, handler: nil)
}
func testTwoBlockResponse() {
let testValue = NSNumber(value: 42)
let testFuture = CompletableFuture<NSNumber>()
let triggerPeriod = TimeInterval(0.1)
let completeExpectation1 = expectation(description: "Completion 1 triggered")
testFuture.addCompletion { (givenValue : NSNumber) in
XCTAssertEqual(testValue, givenValue,"Given Value does not equal test value")
completeExpectation1.fulfill()
}
let completeExpectation2 = expectation(description: "Completion 2 triggered")
testFuture.addCompletion { (givenValue : NSNumber) in
XCTAssertEqual(testValue, givenValue,"Given Value does not equal test value")
completeExpectation2.fulfill()
}
DispatchQueue.global().asyncAfter(deadline: .now() + triggerPeriod) {
testFuture.complete(value: testValue)
}
waitForExpectations(timeout: triggerPeriod * 2, handler: nil)
}
func testCancel() {
let testFuture = CompletableFuture<NSNumber>()
let triggerPeriod = TimeInterval(0.1)
let cancelCheckDone = expectation(description: "Cancellation check done")
testFuture.addCompletion { (givenValue : NSNumber) in
XCTFail("Completion shouldn't get called. givenValue: \(givenValue)")
}
DispatchQueue.global().asyncAfter(deadline: .now() + triggerPeriod) {
testFuture.cancel()
}
DispatchQueue.global().asyncAfter(deadline: .now() + triggerPeriod * 2) {
XCTAssertTrue(testFuture.canceled, "Cancellation failed")
cancelCheckDone.fulfill()
}
waitForExpectations(timeout: triggerPeriod * 3, handler: nil)
}
}

The class uses two dispatch queues internally. One is a privately-owned serial queue for coordinating access to the promised object and the corresponding list of promise completion handlers. Whereas the other dispatch queue is for invoking the completion handlers when the promise gets fulfilled. That last one defaults to the global concurrent queue and can be overridden at construction time to use any other queue. This is to fulfill the Cocoa convention of completion handlers being called “in the main queue or an arbitrary queue”. Since it’s not a user interface class, it doesn’t make sense to default it to the main queue.

The diagram below shows how, the future object collects registration on promise completions when the object hasn’t been fulfilled. Upon completion of the promise, the completion handlers gets called with the resulting object. If the completion registration gets called after fulfillment of the promise, the future object would simply call the given promise completion handler. Registration on promises can come from any queue (thread) since the object would just dispatch the request into the internal serial queue. Similarly the completion handler gets called on the global queue (unless another queue was specified), regardless of whether the promise was fulfilled when the registration was made.

Time Diagram of promise completion

In the above diagram, a, b, and c are clients wanting the promised object. Both a and b requests the object prior to the promise being fulfilled, hence their completion handlers are queued and retained. However client c made the request when the object is already available, and thus its completion handler gets called almost immediately. Registrations to get the promised object can be done from any dispatch queue since the future would simply re-dispatch to an internal serial queue to prevent any race condition. However, the callbacks are done on the callback queue (by default is a global concurrent queue) regardless of whether the request was queued or not, to ensure uniformity of the behavior.

Last but not least, the class was written in Swift 3.1 (Xcode 8 era). BSD-licensed for use in your project. Hopefully there wont be any big changes when Xcode 9 hits.

That’s all for now, take care.



Avoid App Review rules by distributing outside the Mac App Store!


Get my FREE cheat sheets to help you distribute real macOS applications directly to power users.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

Avoid Delays and Rejections when Submitting Your App to The Store!


Follow my FREE cheat sheets to design, develop, or even amend your app to deserve its virtual shelf space in the App Store.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

0 thoughts on “A Swift Promise

Leave a Reply