A streamlined Future<Value, Error>
implementation.
Future
A future represents a result of computation which may be available now, or in the future, or never. Essentially, a future is an object to which you attach callbacks, instead of passing callbacks into a function that performs a computation.
Futures are easily composable. Future<Value, Error>
provides a set of functions like map
, flatMap
, zip
, reduce
and more to compose futures.
Quick Start
To attach a callback to the Future
use on(success:failure:completion:)
method:
let user: Future<User, Error>
user.on(success: { print("received entity: \($0)" },
failure: { print("failed with error: \($0)" })
// As an alternative observe a completion:
user.on(completion: { print("completed with result: \($0)" })
By default, all of the callbacks are executed on the main queue (DispatchQueue.main
). To change the queue pass one into on
method:
future.on(queue: .global(), success: { print("value: \($0)" })
Mapping Values
Use familiar map
and flatMap
function to transform the future's values and chain futures:
let user: Future<User, Error>
func loadAvatar(url: URL) -> Future<UIImage, Error> {}
let avatar = user
.map { $0.avatarURL }
.flatMap(loadAvatar)
Mapping Errors
Future
has typed errors. To convert from one error type to another use mapError
:
let request: Future<Data, URLError>
request.mapError(MyError.init(urlError:))
Use flatMapError
to "recover" from an error.
Zip
Use zip
to combine the result of up to three futures in a single future:
let user: Future<User, Error>
let avatar: Future<UIImage, Error>
Future.zip(user, avatar).on(success: { user, avatar in
// use both values
})
Or to wait for the result of multiple futures:
Future.zip([future1, future2]).on(success: { values in
// use an array of values
})
Reduce
Use reduce
to combine the results of multiple futures:
let future1 = Future<Int, Error>(value: 1)
let future2 = Future<Int, Error>(value: 2)
Future.reduce(0, [future1, future2], +).on(success: { value in
print(value) // prints "3"
})
Creating Futures
Using Promise
:
func someAsyncOperation(args) -> Future<Value, Error> {
let promise = Promise<Value, Error>()
someAsyncOperationWithCallback(args) { value, error in
// when finished...
promise.succeed(result: value)
// if error...
promise.fail(error: error)
}
return promise.future
}
Using a convenience init method:
let future = Future<Int, Error> { succeed, fail in
someAsyncOperationWithCallback { value, error in
// succeed or fail
}
}
With a value or an error:
Future<Int, Error>(value: 1)
Future<Int, Error>(error: Error.unknown)
Wait
Use wait
method to block the current thread and wait until the future receives a result:
let result = future.wait()
Synchronous Inspection
class Future<Value, Error> {
var isPending: Bool { get }
var value: Value? { get }
var error: Error? { get }
var result: Result<Value, Error> { get }
}
Cancelation
Pill considers cancellation to be a concern orthogonal to Future
. There are multiple cancellation approaches. There are arguments for failing futures with an error on cancelation, there is also an argument for never resolving futures when the associated work gets canceled. In order to implement cancelation you might want to consider CancellationToken
or other similar patterns.
Anti-Patterns
Avoid nesting futures, as this is the issue that futures are designed to solve:
loadUser().on(success: { user in
loadAvatar(url: user.avatarUrl).on(success: { avatar in
print("avatar loaded")
}
}
Requirements
- iOS 9.0 / watchOS 2.0 / OS X 10.11 / tvOS 9.0
- Xcode 10
- Swift 4.2
License
Pill is available under the MIT license. See the LICENSE file for more info.