Share
Embed
Michael Gray: Futures and Promises, or how I learned to stop worrying and love the GCD
Presented at AltConf 2016
Realm
June 13, 2016
Transcript
50 slides
-
What are doing • What the heck is a Promise?
a Future? • How is FutureKit different from all the others? • Some Advanced Used cases. -
Lots of Options • FutureKit (Swift) • PromiseKit (Objective-C/Swift) •
BrightFutures (Swift) • Bolts/BFTask (Objective-C) • … -
blocks / closures • Introduced into Objective-C for 10.6, iOS4.
• SO MUCH Better than delegate protocols! • iOS 5.0 SDK started to include ‘callback blocks’ in api calls instead of delegate protocols! • GCD made easy: dispatch_async()! - -
blocks / closures func asyncFunction(callback:(Result) -> Void) -> Void {
dispatch_async(queue) { let x = Result("Hello!") callback(x) } } -
The rise of the callback block • Apple introduces lots
of API calls that use callbacks. • AFNetworking becomes the #1 Objective-C library! Cause it uses callbacks instead of horrible NSURLConnection delegates! • We can get our stuff out of the main q (dispatch_async()) -
Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"]) .response { request, response, data,
error in print(request) print(response) print(data) print(error) } classic example -
The problem of callbacks • 1 - Where does my
callback run? Is it in a background queue? main queue? Could it be either? Do I have to add my own defensive dispatch_async? • 2 - Error Handling? Two different callbacks? Enumerations? • 3 - What if I cancel? Should my callback still be called? Do I get an error? (Never been consistent..) -
func getMyDataObject(url: NSURL,callback:(object:MyCoreDataObject?,error: ErrorType) -> Void) { Alamofire.request(.GET, url, parameters:
["foo": "bar"]).responseJSON { response in switch response.result { case .Success: managedContext.performBlock { let myObject = MyCoreDataObject(entity: entity!, insertIntoManagedObjectContext: managedContext) do { try myObject.addData(response) try managedContext.save() dispatch_async(dispatch_get_main_queue(), { callback(object: myObject, error: nil) }) } catch let error { callback(object: nil,error: error) } } case .Failure(let error): callback(object: nil,error: error) } } } -
Futures & Promise • Why Futures/Promises? • Escape from Callback
Hell. • The functions that define callbacks = anti-pattern -
Promise vs Future • Why are there are two terms?
Can’t we all just agree? • Javascript - Promises. then()/catch()/finally() • Scala (Future/Promise pair). onComplete()/onSuccess()/onFail() -
Future vs Promise (FutureKit) • Future - Consumer interface. Usually
returned from a function, instead of a callback. TypeSafe. A consumer can then “attach” completion blocks to create even more interesting Futures. • Promise - the Producer interface. Is used to create a Future. Think - “Always keep your Promises”. (Always complete your promises, or your code will hang). Can be used to ‘wrap’ any existing callback code and convert it into a Future. -
FutureKit func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> { let executor =
Executor.ManagedObjectContext(managedContext) Alamofire.request(.GET, url, parameters: ["foo": "bar"]) .futureJSONObject() .onSuccess(executor) { response -> MyCoreDataObject in let myObject = MyCoreDataObject(entity: entity!, insertIntoManagedObjectContext: managedContext) try myObject.addData(response) try managedContext.save() return MyCoreDataObject } } } -
onComplete let newFuture = sampleFuture.onComplete { (result: FutureResult<Int>) -> Completion<String>
in switch result { case let .Success(value): return .Success(String(value)) case let .Fail(err): return .Fail(err) case .Cancelled: return .Cancelled } } -
FutureResult<T> public enum FutureResult<T> { case Success(T) case Fail(ErrorType) case
Cancelled } A FutureResult is the FINAL value that a Future will return (when it is done) -
Completion<T> public enum Completion<T> { case Success(T) case Fail(ErrorType) case
Cancelled case CompleteUsing(Future<T>) } A Completion type is a value that can ‘complete’ a Promise! -
onSuccess let asyncFuture5 = Future(.Background) { () -> Int in
return 5 } let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in let five = value print("\(five)") return five } -
onSuccess to new type let stringFuture = Future<String>(success: "5") stringFuture
.onSuccess { (stringResult:String) -> Int in let i = Int(stringResult)! return i } .map { intResult -> [Int] in let array = [intResult] return array } .onSuccess { arrayResult -> Int? in return arrayResult.first! } -
Futures are VERY composable! func coolFunctionThatAddsOneInBackground(num : Int) -> Future<Int>
{ // let's dispatch this to the low priority background queue return Future(.Background) { () -> Int in let ret = num + 1 return ret } } let stringFuture = Future<Int>(success: 5) stringFuture .onSuccess { intResult -> Future<Int> in return coolFunctionThatAddsOneInBackground(intResult) } .onSuccess { return coolFunctionThatAddsOneInBackground($0) } -
Promise let namesPromise = Promise<[String]>() let names = ["Skyler","David","Jess"] namesPromise.completeWithSuccess(names)
let namesFuture :Future<[String]> = namesPromise.future namesFuture.onSuccess(.Main) { (names : [String]) -> Void in for name in names { print("Happy Future Day \(name)!") } } -
func getCoolCatPic(catUrl: NSURL) -> Future<UIImage> { let catPicturePromise = Promise<UIImage>()
let task = NSURLSession.sharedSession().dataTaskWithURL(catUrl) { (data, response, error) -> Void in if let e = error { catPicturePromise.completeWithFail(e) } else { if let d = data, image = UIImage(data: d) { catPicturePromise.completeWithSuccess(image) } else { catPicturePromise.completeWithErrorMessage("didn't understand response from \ (response)") } } } task.resume() // return the promise's future. return catPicturePromise.future } -
What about Error Handling • You want a function that
‘fetches’ a ‘Model’ object from server. • Make an API call (Networking Errors). Parse JSON (Json Parsing errors) Save to the DB (File IO Errors, DB Validation Errors). • As you add more logic, you will have more potential errors. -
Why isn’t FutureKit ErrorType specific? • A Future that defines
it’s ErrorType.. undermines composability. • By forcing consumers to ‘expect’ any error, it means you can modify any Futures. • side effect: You MUST add error handling. -
built in ‘catch’ handling • All FutureKit handlers onComplete/onSuccess/onFail/onCancel/ map..
all ‘catch’! • Just ‘throw’ errors (no try/catch needed). -
onFail func getMyDataObject(url: NSURL) -> Future<MyCoreDataObject> { let executor =
Executor.ManagedObjectContext(managedContext) Alamofire.request(.GET, url) .futureJSONObject() .onSuccess(executor) { response -> MyCoreDataObject in let myObject = MyCoreDataObject(entity: entity, insertIntoManagedObjectContext: managedContext) try myObject.addData(response) try managedContext.save() return MyCoreDataObject }.onFail { error in // alert user of error! } } -
onFail • onFail does NOT CONSUME ERRORS. Always use
onComplete to remap Errors. • Only adds side effects. • Calling onSuccess chains without onFail or onSuccess = compilers warning! -
Cancellation • Cancellation is it’s OWN result type. • Cancellations
are NOT errors - they are normal and expected behavior. • onCancel() handler doesn’t pollute your onFail() handlers. -
Cancellation let f = asyncFunc1().onSuccess { return asyncFunc2() } let
token = f.getCancelToken() token.cancel() -
onRequestCancel let p = Promise<Void>() p.onRequestCancel { (options) -> CancelRequestResponse<Void>
in // start cancelling return .Continue } -
Cancellation public extension AlamoFire.Request { public func future<T: ResponseSerializerType>(responseSerializer s:
T) -> Future<T.SerializedObject> { let p = Promise<T.SerializedObject>() p.onRequestCancel { _ in self.cancel() return .Continue } self.response(queue: nil, responseSerializer: s) { response -> Void in switch response.result { case let .Success(t): p.completeWithSuccess(t) case let .Failure(error): let e = error as NSError if (e.domain == NSURLErrorDomain) && (e.code == NSURLErrorCancelled) { p.completeWithCancel() } else { p.completeWithFail(error) } } } return p.future } } -
The new Swift anti-patterns • NO MORE functions that need
callback properties! • Futures with ‘optional’ return values - Use Fail instead! • Let functions define their own execution context! -
Executors • Where do my callback blocks run? • Executors
- (ExecutionContext, Bolt Executor). • (Hint): it’s a usually just CGD queue. -
Executor enumeration: • .Main .UserInteractive .UserInitiated .Default .Utility .Background •
Each mirrors the Build in GCD qos_class of the same name. -
Executors in Action let asyncFuture5 = Future(.Background) { () ->
Int in return 5 } let f = asyncFuture5.onSuccess(.Main) { (value) -> Int in let five = value print("\(five)") return five } -
Special Executors • .Immediate - my block can run anywhere!
Use with care, but good for simple value-mapping. .MainImmediate - Will try to avoid unneeded dispatch_async if already in the Main .MainAsync - always does a dispatch_async() .Queue(q) - your own custom dispatch_queue .OperationQueue(opq) - wraps NSOperationQueue .ManagedObjectContext(c) - wraps ‘performBlock()’ -
Extra Smart • .Current = Use the Executor that is
currently running! (If not running in Executor, than maps to .Main or .Async). May not work correctly with .Custom • .Primary = The Context to use if you don’t define one (default = .Current) Configurable • .Main = .MainImmediate/.MainAsync (default: .MainImmediate). • .Async = Configurable ‘not-main-queue’. Default: .Default (qos default) • .Custom = build your own! -
let ex: Executor = .Background let f = ex.execute {
let data = expensiveToComputeData() return data } execute() (instead of a Promise) -
combineFutures let shiftFuture = model.getShift() let vehicleFuture = model.getVehicle() combineFutures(shiftFuture,
vehicleFuture) .onSuccess { (shift,vehicle) -> Void in print("\(shift),\(vehicle)") } -
CompletionType protocol public protocol CompletionType { associatedtype T var completion
: Completion<T> { get } } Can be used to ‘wrap’ any of your favorite Async objects (ex: AlamoFire request) -
Caching Futures NSCache.findOrFetch(key, expireTime:NSDate? = nil, onFetch: () -> Future<T>)
you can ‘cache’ the result of an operation - BEFORE it’s complete. -
FutureBatch • When combineFuture() doesn’t work. • Can take an
‘array’ of Futures (of any size) and perform parallel execution. • Much more control over failures and cancellations of sub futures. -
FutureFIFO • A simple ‘queue’ of Asynchronous operations let fifo:
FutureFIFO() let f:Future<UIImage> = fifo.add { return functionThatBuildsUImage() } -
NSOperations FutureOperation<T> - Build an NSOperation using a Future NSOperationQueue.add<T>(block:
() throws -> (Future<T>)) -> Future<T> let opQueue: NSOperationQueue let f:Future<UIImage> = opQueue.add { return functionThatBuildsUImage() } -
Kill Your Delegate Protocols! • Define interactive modal components that
single ‘future’ property that returns a result. • Need more than one type of result? Use an enumerated value!. • example Model viewControllers that return their results via a Future. -
Reactive? public extension SignalProducerType { public func mapFuture<U>(transform: Value ->
Future<U>?) -> SignalProducer<U?, Error> } Do I use a Signal/Sequence or a Future? Future -> Signal A-OK! Signal -> Future Caution!