I'm proposing the introduction of an inverted scope for the "guard let..." pattern. The problem occurs most commonly when propagating errors to completion handlers.
Example using URLSession.dataTask(with:completionHandler:):
class Network {
let session: URLSession
func callServer(completion: @escaping (Data?, Error?) -> Void) {
let task = urlSession.getDataTask(request: request) { (data, response, error in
if let error = error {
log("ERROR: \(error)")
completion(nil, error)
// Oops, I forgot to return here
}
... additional error checking, deserialization, and data validation ...
completion(data)
}
}
Using the if-let pattern above allows me to have a non-optional "error" variable, however it becomes easy to miss the intended return.
If I switch to guard-let, the non-optional "error" variable is not available while in the "else" scope. This is a minor inconvenience in the log example above, but if I wish to use more type-safe call signatures, it requires me to use force-unwraps:
class Network {
let session: URLSession
func callServer(completion: @escaping (Result<Data,Error>) -> Void) {
let task = urlSession.getDataTask(request: request) { (data, response, error in
guard error == nil else {
log("ERROR: \(error!)") // why must I force-unwrap here?
completion(.failure(error!))
return
}
... additional error checking, deserialization, and data validation ...
completion(data)
}
}
By switching to 'guard', I can now enforce return in the error handler, but I'm left with an optional when I know it is not.
If I wish to enforce returns as well as having an unwrapped optional, I'm left with having to do something like this:
class Network {
let session: URLSession
func callServer(completion: @escaping (Result<Data,Error>) -> Void) {
let task = urlSession.getDataTask(request: request) { (data, response, error in
guard error == nil else {
guard let error = error else { assertFailure("This can never happen"); return }
log("ERROR: \(error)")
completion(.failure(error))
return
}
... additional error checking, deserialization, and data validation ...
completion(data)
}
}
I propose either:
- a new keyword (e.g.) nguard that allows the scope of the 'let' variable to be inverted, or
- guard to be smarter about knowing that I've tested specifically for a nil case and therefore automatically lose optionality in the respective scope.
An example usage of nguard would be:
class Network {
let session: URLSession
func callServer(completion: @escaping (Result<Data,Error>) -> Void) {
let task = urlSession.getDataTask(request: request) { (data, response, error in
nguard let error = error else {
log("ERROR: \(error)") // error is non-optional here
completion(.failure(error))
return
}
// error is optional here... the compiler can also infer that error must be nil here.
... additional error checking, deserialization, and data validation ...
completion(data)
}
}
The introduction of a new keyword would have minimal impact on existing code. The "n" denotes negative or inverted scope. (Also because nguard is punny -- I'm sure someone else can come up with a better keyword/modifier)