A monadic Future design pattern implementation in Swift language,
using libdispatch and POSIX
mutexes and condition variables. Design inspired from
Scala’s
scala.concurrent.Future.
Only the essential features are in place currently. We’re using the
library in production, and it appears to work as expected. There’s a
benchmark that acts as a stress test, see Performance
below.
Requirements
Mac OS X >= 10.9
Xcode >= 9.0 (Swift 4.0)
Installation
CocoaPods
CocoaPods is a centralized dependency manager
for Cocoa libraries. It integrates libraries as embedded frameworks to
your project. This requires that the minimum deployment target of your
project is iOS 8.0 or OS X 10.9.
MiniFuture is available as a pod. Add the following
line to your project’s Podfile in order to have the library as a
dependency:
pod 'MiniFuture', '~> 0.5.0'
And run pod install.
In your source code, import MiniFuture to use the library.
Manually
Copy all files under Source directory to your project. You have to
figure out how to upgrade the library yourself.
Characteristics
A future is a promise of a computation that will complete eventually. We
use Try<T> value type to wrap these computation results. It’s an
enumeration with two members, success<T> and failure<Error>. The
first is meant for the caller of the Future to signify successful
computation, with the result of the computation as the associated value
of type T. The latter member signifies computation failure, with
Error as the associated value.
For example, the following future will complete with .success(1)
eventually:
let fut = Future<Int>.async {
Thread.sleep(forTimeInterval: 0.2)
return .success(1)
}
assert(succeeded.get() == Try.success(1))
The basic operation to compose Futures is with Future#flatMap(_:). The
method signature is:
flatMap<U>(f: T throws -> Future<U>) -> Future<U>
It takes in a closure, f. If the current future completes with a
success value, f gets called with T as its argument. The return
value of f is the next future computation. Later, if that future
completes with a success value, the closure passed to that future gets
called. This is the asynchronous call chain of the futures.
If the current future completes with a failure value, f will not be
called. This short-circuits the call chain of the futures.
The closure f may throw an error when called. If that happens, the
return value of f will be a future completed with a failure value
containing the error.
Try<T> and the automatic handling of thrown errors is borrowed from
Scala 2.10.
All asynchronous operations run in libdispatch’s default global
concurrent queue. Closures passed to Future#flatMap(_:),
Future#map(_:), Future#onComplete(_:), and Future.async(_:) are
always executed in a queue worker thread. Use proper synchronization
when accessing shared state via references captured in the closures.
Usage
To get a Future job running, use Future.succeeded(_:) and
Future.failed(_:) to wrap immediate values. These return
ImmediateFuture objects, a Future implementation class already
completed with success or failure value.
Use Future.async(_:) for asynchronous jobs that compute the value
later in a queue worker thread. You pass a block to async(_:) and
return either a Try.success<T> or Try.failure<Error> value from
it. The Future implementation class here is AsyncFuture.
For adapting existing asynchronous interfaces with Futures, use
Future.promise(_:). This returns a PromiseFuture object, a promise
kind of Future implementation class. Pass the Future to an existing
asynchronous interface, and in the completion handler of the interface,
complete the Future with success (Future#resolve(_:)) or failure
(Future#reject(_:)). You can immediately return a PromiseFuture to
code expecting Futures and let the PromiseFuture object complete
later.
You can complete a PromiseFuture with the result of another future,
too. Call PromiseFuture#completeWith(_:), passing another future as
the argument. Once the future completes, the promise completes with the
same result as the future.
When you get a handle to a Future, use Future#flatMap(_:) or
Future#map(_:) to compose another Future that depends on the completed
result of the previous Future. Use Future#get() to wait for the result
of a Future. Use Future#onComplete(_:) to add a callback for
side-effects to be run when the Future completes.
Example
extension String {
var trimmed: String {
return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
func excerpt(_ maxLength: Int) -> String {
precondition(maxLength >= 0, "maxLength must be positive")
if maxLength == 0 {
return ""
}
let length = characters.count
if length <= maxLength {
return self
}
return self[startIndex..<characters.index(startIndex, offsetBy: maxLength-1)].trimmed + "…"
}
}
/**
* Request a web resource asynchronously, immediately returning a handle to
* the job as a promise kind of Future. When NSURLSession calls the completion
* handler, we fullfill the promise. If the completion handler gets called
* with the contents of the web resource, we resolve the promise with the
* contents (the success case). Otherwise, we reject the promise with failure.
*/
func loadURL(_ url: URL) -> Future<Data> {
let promise = Future<Data>.promise()
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
if let err = error {
promise.reject(err)
} else if let d = data {
promise.resolve(d)
} else {
promise.reject(AppError.failedLoadingURL(url))
}
})
task.resume()
return promise
}
/**
* Parse data as HTML document, finding specific contents from it with an
* XPath query. We return a completed Future as the handle to the result. If
* we can parse the data as an HTML document and the query succeeds, we return
* a successful Future with the query result. Otherwise, we return failed
* Future describing the error.
*
* Because this function gets called inside `Future#flatMap`, it's run in
* background in a queue worker thread.
*/
func readXPathFromHTML(_ xpath: String, data: Data) throws -> Future<HTMLNode> {
let doc = try HTMLDocument.readData(asUTF8: data)
let node = try doc.rootHTMLNode()
let found = try node.forXPath(xpath)
return Future.succeeded(found)
}
let wikipediaURL = URL(string: "https://en.wikipedia.org/wiki/Main_Page")!
let featuredArticleXPath = "//*[@id='mp-tfa']"
let result = loadURL(wikipediaURL)
/* Future composition (chaining): when this Future completes successfully,
* pass its result to a function that does more work, returning another
* Future. If this Future completes with failure, the chain short-circuits
* and further flatMap methods are not called. Calls to flatMap are always
* executed in a queue worker thread.
*/
.flatMap { try readXPathFromHTML(featuredArticleXPath, data: $0) }
/* Wait for Future chain to complete. This acts as a synchronization point.
*/
.get()
switch result {
case .success(let value):
let excerpt = value.textContents!.excerpt(78)
print("Excerpt from today's featured article at Wikipedia: \(excerpt)")
case .failure(let error):
print("Error getting today's featured article from Wikipedia: \(error)")
}
See more in Example/main.swift. You can run the examples:
$ make example
# xcodebuild output…
./build/Example
Excerpt from today's featured article at Wikipedia: Upper and Lower Table Rock are two prominent volcanic plateaus just north of…
Performance
There’s a benchmark in Benchmark/main.swift. It builds up complex
nested Futures (the futEnd variable in the code) in a loop 2000 times
(numberOfFutureCompositions) and chains them into one big composite
Future (the fut variable). Then the benchmark waits for the Future to
complete.
We repeat this 500 times (numberOfIterations) to get the arithmetic
mean and standard deviation of time spent completing each composite
Future.
Compile it with Release build configuration, which enables -O compiler
flag. Then run it from the terminal.
Example run with MacBook Pro (MacBookPro11,3) 2.6 GHz Intel Core i7
Haswell, 16 GB 1600 MHz DDR3:
$ make benchmark
# xcodebuild output…
./build/Benchmark
iterations: 500, futures composed: 2000
warm up: 71 ms (± 2 ms)
measure: 71 ms (± 2 ms)
Apple Swift version 4.0.3 (swiftlang-900.0.74.1 clang-900.0.39.2)
Target: x86_64-apple-macosx10.9
Total memory consumption of the process stayed below 20 MB.
Future work
Implement Future cancellation and timeouts
Implement more composition operations on Futures
License
MiniFuture is released under the MIT license. See LICENSE.txt for
details.
MiniFuture
A monadic Future design pattern implementation in Swift language, using libdispatch and POSIX mutexes and condition variables. Design inspired from Scala’s scala.concurrent.Future.
Only the essential features are in place currently. We’re using the library in production, and it appears to work as expected. There’s a benchmark that acts as a stress test, see Performance below.
Requirements
Installation
CocoaPods
CocoaPods is a centralized dependency manager for Cocoa libraries. It integrates libraries as embedded frameworks to your project. This requires that the minimum deployment target of your project is iOS 8.0 or OS X 10.9.
MiniFuture is available as a pod. Add the following line to your project’s
Podfile
in order to have the library as a dependency:And run
pod install
.In your source code,
import MiniFuture
to use the library.Manually
Copy all files under
Source
directory to your project. You have to figure out how to upgrade the library yourself.Characteristics
A future is a promise of a computation that will complete eventually. We use
Try<T>
value type to wrap these computation results. It’s an enumeration with two members,success<T>
andfailure<Error>
. The first is meant for the caller of the Future to signify successful computation, with the result of the computation as the associated value of typeT
. The latter member signifies computation failure, withError
as the associated value.For example, the following future will complete with
.success(1)
eventually:The basic operation to compose Futures is with
Future#flatMap(_:)
. The method signature is:It takes in a closure,
f
. If the current future completes with a success value,f
gets called withT
as its argument. The return value off
is the next future computation. Later, if that future completes with a success value, the closure passed to that future gets called. This is the asynchronous call chain of the futures.If the current future completes with a failure value,
f
will not be called. This short-circuits the call chain of the futures.The closure
f
may throw an error when called. If that happens, the return value off
will be a future completed with a failure value containing the error.For example:
Try<T>
and the automatic handling of thrown errors is borrowed from Scala 2.10.All asynchronous operations run in libdispatch’s default global concurrent queue. Closures passed to
Future#flatMap(_:)
,Future#map(_:)
,Future#onComplete(_:)
, andFuture.async(_:)
are always executed in a queue worker thread. Use proper synchronization when accessing shared state via references captured in the closures.Usage
To get a Future job running, use
Future.succeeded(_:)
andFuture.failed(_:)
to wrap immediate values. These returnImmediateFuture
objects, a Future implementation class already completed with success or failure value.Use
Future.async(_:)
for asynchronous jobs that compute the value later in a queue worker thread. You pass a block toasync(_:)
and return either aTry.success<T>
orTry.failure<Error>
value from it. The Future implementation class here isAsyncFuture
.For adapting existing asynchronous interfaces with Futures, use
Future.promise(_:)
. This returns aPromiseFuture
object, a promise kind of Future implementation class. Pass the Future to an existing asynchronous interface, and in the completion handler of the interface, complete the Future with success (Future#resolve(_:)
) or failure (Future#reject(_:)
). You can immediately return aPromiseFuture
to code expecting Futures and let thePromiseFuture
object complete later.You can complete a
PromiseFuture
with the result of another future, too. CallPromiseFuture#completeWith(_:)
, passing another future as the argument. Once the future completes, the promise completes with the same result as the future.When you get a handle to a Future, use
Future#flatMap(_:)
orFuture#map(_:)
to compose another Future that depends on the completed result of the previous Future. UseFuture#get()
to wait for the result of a Future. UseFuture#onComplete(_:)
to add a callback for side-effects to be run when the Future completes.Example
See more in
Example/main.swift
. You can run the examples:Performance
There’s a benchmark in
Benchmark/main.swift
. It builds up complex nested Futures (thefutEnd
variable in the code) in a loop 2000 times (numberOfFutureCompositions
) and chains them into one big composite Future (thefut
variable). Then the benchmark waits for the Future to complete.We repeat this 500 times (
numberOfIterations
) to get the arithmetic mean and standard deviation of time spent completing each composite Future.Compile it with Release build configuration, which enables
-O
compiler flag. Then run it from the terminal.Example run with MacBook Pro (MacBookPro11,3) 2.6 GHz Intel Core i7 Haswell, 16 GB 1600 MHz DDR3:
Total memory consumption of the process stayed below 20 MB.
Future work
License
MiniFuture is released under the MIT license. See
LICENSE.txt
for details.