Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optionals vs Throwing functions

Tags:

Consider the following lookup function that I wrote, which is using optionals and optional binding, reports a message if key is not found in the dictionary

func lookUp<T:Equatable>(key:T , dictionary:[T:T]) -> T? {     for i in dictionary.keys {        if i == key{            return dictionary[i]        }     }     return nil }  let dict = ["JO":"Jordan",    "UAE":"United Arab Emirates",    "USA":"United States Of America" ]  if let a = lookUp( "JO",dictionary:dict ) {     print(a) // prints Jordan  } else {     print("cant find value") } 

I have rewritten the following code, but this time, using error handling, guard statement, removing -> T? and writing an enum which conforms to ErrorType:

enum lookUpErrors : ErrorType {     case noSuchKeyInDictionary }  func lookUpThrows<T:Equatable>(key:T , dic:[T:T])throws {     for i in dic.keys{         guard i == key else {             throw lookUpErrors.noSuchKeyInDictionary         }         print(dic[i]!)        } }  do {     try lookUpThrows("UAE" , dic:dict) // prints united arab emirates } catch lookUpErrors.noSuchKeyInDictionary{     print("cant find value") } 

Both functions work well but:

  • which function grants better performance

  • which function is "safer"

  • which function is recommended (based on pros and cons)

like image 847
Abdullah Ossama Omari Avatar asked Jun 26 '15 16:06

Abdullah Ossama Omari


1 Answers

Performance

The two approaches should have comparable performance. Under the hood they are both doing very similar things: returning a value with a flag that is checked, and only if the flag shows the result is valid, proceeding. With optionals, that flag is the enum (.None vs .Some), with throws that flag is an implicit one that triggers the jump to the catch block.

It's worth noting your two functions don't do the same thing (one returns nil if no key matches, the other throws if the first key doesn’t match).

If performance is critical, then you can write this to run much faster by eliminating the unnecessary key-subscript lookup like so:

func lookUp<T:Equatable>(key:T , dictionary:[T:T]) -> T?  {     for (k,v) in dictionary where k == key {         return v     }     return nil } 

and

func lookUpThrows<T:Equatable>(key:T , dictionary:[T:T]) throws -> T  {     for (k,v) in dic where k == key {         return v     }     throw lookUpErrors.noSuchKeyInDictionary } 

If you benchmark both of these with valid values in a tight loop, they perform identically. If you benchmark them with invalid values, the optional version performs about twice the speed, so presumably actually throwing has a small bit of overhead. But probably not anything noticeable unless you really are calling this function in a very tight loop and anticipating a lot of failures.

Which is safer?

They're both identically safe. In neither case can you call the function and then accidentally use an invalid result. The compiler forces you to either unwrap the optional, or catch the error.

In both cases, you can bypass the safety checks:

// force-unwrap the optional let name = lookUp( "JO", dictionary: dict)! // force-ignore the throw let name = try! lookUpThrows("JO" , dic:dict) 

It really comes down to which style of forcing the caller to handle possible failure is preferable.

Which function is recommended?

While this is more subjective, I think the answer’s pretty clear. You should use the optional one and not the throwing one.

For language style guidance, we need only look at the standard library. Dictionary already has a key-based lookup (which this function duplicates), and it returns an optional.

The big reason optional is a better choice is that in this function, there is only one thing that can go wrong. When nil is returned, it is for one reason only and that is that the key is not present in the dictionary. There is no circumstance where the function needs to indicate which reason of several that it threw, and the reason nil is returned should be completely obvious to the caller.

If on the other hand there were multiple reasons, and maybe the function needs to return an explanation (for example, a function that does a network call, that might fail because of network failure, or because of corrupt data), then an error classifying the failure and maybe including some error text would be a better choice.

The other reason optional is better in this case is that failure might even be expected/common. Errors are more for unusual/unexpected failures. The benefit of returning an optional is it’s very easy to use other optional features to handle it - for example optional chaining (lookUp("JO", dic:dict)?.uppercaseString) or defaulting using nil-coalescing (lookUp("JO", dic:dict) ?? "Team not found"). By contrast, try/catch is a bit of a pain to set up and use, unless the caller really wants "exceptional" error handling i.e. is going to do a bunch of stuff, some of which can fail, but wants to collect that failure handling down at the bottom.

like image 167
Airspeed Velocity Avatar answered Nov 13 '22 09:11

Airspeed Velocity