Figure

A blog about Swift and iOS development.

Clean Optional Parameters

Let's extend NSError to return errors with a domain, code, and description specific to our app:

extension NSError{
  enum MyErrorCodes:Int{
    case UnknownError = 100, 
         ParseError, 
         ParameterError, 
  }

  //We'd like this to be a class variable, but
  //those aren't supported in Swift (yet). So we 
  //use a computed property instead:
  class var myDomain:String{ return "MyDomain" }

  class func 
   myParseError(description:String)->NSError{
    let info = 
       [NSLocalizedDescriptionKey:description]
    return self(
       domain:myDomain, 
       code:MyErrorCodes.ParseError.rawValue, 
       userInfo:info)
  }
}

This makes getting an error pretty convenient:

func parse(inout error:NSError?){
  //...
  error = NSError.myParseError("Parse failed.")
}

But if we use it many places, it might get tedious to continually type a description for the error. Especially if that description is usually "Parse failed."

We could give up on passing it as a parameter and hard-code the description into myParseError. But what if we have a very specific failure and we want to pass the reason for it up to our UI?

What we need is a way to optionally pass a custom param to our method when we have to, but fall back on a sane default when we don't. Optionally being the key word, there.

So we make our param optional. And thanks to nil coalescing operators, we don't even have to (un)wrap the entire thing with if let:

class func 
 myParseError(description:String?)->NSError{
  let info = 
     [NSLocalizedDescriptionKey:
     description ?? "Parse failed."]
  return self(
     domain:myDomain, 
     code:MyErrorCodes.ParseError.rawValue, 
     userInfo:info)
}

Now, if we want a custom description, we pass it in. If we don't, we just pass nil:

NSError.myParseError("Big bada boom!") 
//> "Big bada boom!"
NSError.myParseError(nil) 
//> "Parse failed."

But we're programmers, and therefore inherently lazy. So some of us are already thinking about ways to get rid of that nil:

extension NSError{
  //...
  class func myParseError()->NSError{
    return myParseError(nil)
  }
  class func 
   myParseError(description:String?)->NSError{...}
}

But that's oh-so-ObjC. Let's not forget the marvelous gift Swift has given us in the form of Default Parameter Values. Instead of cluttering up our interface with shell methods, we can add a simple = nil to our original method's signature to give its param a default value:

extension NSError{
  //...
  class func 
   myParseError(description:String?=nil)->NSError{
    let info = 
       [NSLocalizedDescriptionKey:
       description ?? "Parse failed."]
    return self(
       domain:myDomain, 
       code:MyErrorCodes.ParseError.rawValue, 
       userInfo:info)
  }
}

Now, whenever we don't include a parameter in our call, the method defaults to a param value of nil. And everything just works!

NSError.myParseError("Big bada boom!") 
//> "Big bada boom!"
NSError.myParseError() 
//> "Parse failed."

 

Update 3/27/2015:

A few have asked on reddit and twitter if we couldn't specify our default error message directly in the method signature and if that might not be the cleaner way to implement this.

The answers are "Kinda," and "In my humble opinion, no," respectively. Certainly something like the following is tempting:

class func 
 parseError(desc:String="Parse failed.")->NSError{
  let info = 
     [NSLocalizedDescriptionKey:desc]
  //...
}

But this is not the same as the example we lay out above. Our original example uses a default description if our argument is nil. This new example uses a default argument to set our description. It's a subtle distinction. To see the difference, consider what would happen if we passed nil to both:

//Original definition:
myParseError(nil) //> "Parse failed."
//New with default:
parseError(nil) //> nil

Is this a big deal? It depends on what you expect the method to do when given a nil value (or something that may or may not be nil). But I'd argue our original definition is more robust.

Also, setting values in our method declarations can be slippery slope. "Parse failed" seems innocent enough, but what if our error description is a little more realistic?

class func 
 parseError(desc:String="There was a problem parsing your file. Please check the file name and try again.")->NSError{
  let info = 
     [NSLocalizedDescriptionKey:desc]
  //...
}

Or what if our default isn't a literal? Or what if it isn't even a constant?

class func 
 myName(fullName:String=first()+last())->NSError{
  let info = 
     [NSLocalizedDescriptionKey:desc]
  //...
}

This is definitly clever. And let's take a moment to marvel at the fact that the above is even possible in Swift! But it complects our code by mixing data (and business logic!) with our method signature. And that makes it (IMHO) less "clean".

Contrast this with our original implementation. Yes, we set a default value of nil, but if we think about it, nil is already the default value of any optional. If we simply created an "empty" string optional as the parameter's default instead, it would behave exactly the same:

class func 
 parseError(desc:String?=Optional<String>())->NSError{
  let info = 
     [NSLocalizedDescriptionKey:desc ?? "Default"]
  //...
}

We're not creating a new value by giving our parameter a default of nil. We're merely clarifying what the existing default for our parameter type is so our caller can ignore it if it wants. This keeps our parameters simple and easy to reason about.


Hit me up on twitter (@jemmons) to continue the conversation.

Latest | Archives