[swift-evolution] Pitch: @required attribute for closures

Charles Srstka cocoadev at charlessoft.com
Mon Jun 6 16:19:59 CDT 2016


> On Jun 6, 2016, at 3:48 PM, michael.peternell at gmx.at wrote:
> 
> Yes, ok...
> 
> Well, I cannot easily make everything synchronous. I just can change it in a way that makes a `@required` attribute "optional" ;)
> 
> import Foundation
> 
> enum Result<T> {
>    case success(T)
>    case error(ErrorType)
> }
> 
> enum MyError: ErrorType {
>    case unknownError
>    case corruptData
>    case badStatusCode(Int)
> }
> 
> struct SomeThing {
>    init?(data: NSData) {
>        ...
>    }
> }
> 
> func getSomethingFromTheNetwork(url: NSURL, completionHandler: (Result<SomeThing>) -> ()) { 
>    func toSomeThing(data: NSData?, response: NSURLResponse?, error: NSError?) -> Result<SomeThing> {
>        if let error = error {
>            return .error(error)
>        }
> 
>        if let httpResponse = response as? NSHTTPURLResponse {
>            let statusCode = httpResponse.statusCode
> 
>            if statusCode < 200 || statusCode >= 300 {
>                return .error(MyError.badStatusCode(statusCode))
>            }
>        }
> 
>        guard let data = data else {
>            return .error(MyError.unknownError)
>        }
> 
>        guard let something = SomeThing(data: data) else {
>            return .error(MyError.corruptData)
>        }
> 
>        return .success(something)
>    }
>    let task = NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
>        completionHandler(toSomething(data, response, error))
>    }
> 
>    task.resume()
> }

I’ve written code like that; however, it falls down as soon as you have to nest two or more asynchronous APIs. Consider the following, and assume we don’t have control of SomeThing’s API (or, maybe, SomeThing relies on some other API we don’t control that has to be asynchronous):

import Foundation

enum Result<T> {
   case success(T)
   case error(ErrorType)
}

enum MyError: ErrorType {
   case unknownError
   case corruptData
   case badStatusCode(Int)
}

struct SomeThing {
   make(data: NSData, completionHandler: (SomeThing?) -> ())
}

func getSomethingFromTheNetwork(url: NSURL, completionHandler: (Result<SomeThing>) -> ()) {	
	let task = NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
		if let error = error {
			completionHandler(.error(error))
			return
		}

		if let httpResponse = response as? NSHTTPURLResponse {
			let statusCode = httpResponse.statusCode
			
			if statusCode < 200 || statusCode >= 300 {
				completionHandler(.error(MyError.badStatusCode(statusCode)))
				return
			}
		}
	
		guard let data = data else {
			completionHandler(.error(MyError.unknownError))
			return
		}

		SomeThing.make(data) { something in
			if let something = something {
				completionHandler(.success(something))
			} else {
				completionHandler(.error(MyError.corruptData))
			}
		}
	}
	
	task.resume()
}

Yeah, you can wrap it in yet another function, but if things get complex enough, and you forget to do that somewhere, it’s bad news.

> With a semaphore, I can also make it synchronous. Not sure if this is a good idea though... If the API is already asynchronous, it's probably better to use it that way.

It’s not a good idea. As I mentioned before, Apple recommends against blocking on dispatch queues. It’s not considered good practice. That includes the main queue, by the way;  both Cocoa-Dev and Stack Overflow are filled with questions from people who tried using dispatch_sync or dispatch_async to the main queue to run -[NSOpenPanel runModal], and then got confused when nothing in the panel displayed properly. Using the async methods on NSOpenPanel and returning via a completion handler would have caused this to work properly.

Charles

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160606/63dd6caa/attachment.html>


More information about the swift-evolution mailing list