4

After perusing through forums and Swift documentation (not completely, I admit), it appears that instead of try-catch mechanisms, in Swift we are encouraged to write code that is more safe from exceptions. In light of that, I have a question about a sample API, and would like to learn how to more safely handle this situation:

For example, I can create the following class using the NSDecimalNumberHandler:

class MathWhiz {

    init() {
    let defaultBehavior: NSDecimalNumberHandler =
    NSDecimalNumberHandler.defaultDecimalNumberHandler()
    }
    func add(op1: String, op2: String) ->NSDecimalNumber {
        return NSDecimalNumber.decimalNumberWithString(op1).decimalNumberByAdding(NSDecimalNumber.decimalNumberWithString(op2))
    }
}

If I use the following, I get a number:

let brain = MathWhiz()
brain.add("1", op2: "1e127")

However, if I cause an overflow exception,:

brain.add("1", op2: "1e128")

I will crash the program as expected.

So, my question is, the API raises exceptions, but I don't handle them here. There are other posts out there of people pointing out that Swift does not have exception handling, but this question is seeking a nice way to handle this problem in the way that the language creators were thinking it should be done. Is there a recommended way to handle this without having to write my own code to check for overflow, underflow, loss of precision, ect...? I am wanting the NSDecimalNumberHandler to do that for me.

H_H
  • 605
  • 1
  • 9
  • 13

3 Answers3

6

If you are designing a function (or method) in Swift, you have at least 3 choices for dealing with errors:

Choice 1: Return an Optional Type

If your function might fail, and this happens on a regular basis, then consider returning an optional type variable. For example, in your case, your method add could return an NSDecimalNumber? instead of a plain NSDecimalNumber. In that case, your method would check for everything that could go wrong, and return nil in those situations. Overflow and underflow would return nil, and all other cases would return an NSDecimalNumber. The callers would have to check for and unwrap the optional NSDecimalNumber like this:

let brain = MathWhiz()
if let sum = brain.add("1", op2: "1e127") {
    println("the result was \(sum)")
} else
    println("something went wrong with MathWhiz add")
}

Choice 2: Return an Enumerated Type

If you want to return more information about the thing that went wrong, you could create an enumerated type with a value for each error and one for success that embeds the answer. For example, you could do:

enum MathWhizResult {
    case Overflow
    case Underflow
    case Success(NSDecimalNumber)
}

Then add would be defined to return MathWhizResult:

func add(op1: String, op2: String) -> MathWhizResult

In the case of an error, add would return .Overflow or .Underflow. In the case of success, add would return Success(result). The caller would have to check the enumeration and unpack the result. A switch could be used for this:

switch (brain.add("1", op2: "1e128")) {
case .Overflow
    println("darn, it overflowed")
case .Underflow
    println("underflow condition happened")
case .Success(let answer)
    println("the result was \(answer)"
}

Choice 3: Choose not to handle errors explicitly

Unpacking the result in the first two choices might be too much overhead for an error that is very rare. You could chose to just return a result, and let the caller deal with the possibility of an underflow or overflow condition. In that case, they would have to check for these conditions themselves before calling add. The benefit is, it they know that their program will never cause an underflow or overflow (because they are dealing with single digit numbers for instance), they are not burdened with unpacking the result.


I created a small app to demonstrate how you could do this with NSDecimalNumbers. I created a Single View Application in Xcode. In the ViewController in the StoryBoard I added 3 TextFields (one each for operand 1, operand 2, and the result) and a Button that I labelled +.

ViewController.swift

import UIKit

class ViewController: UIViewController {
    @IBOutlet var operand1 : UITextField!
    @IBOutlet var operand2 : UITextField!
    @IBOutlet var result   : UITextField!

    var brain = MathWhiz()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func addButton(sender : UIButton) {
        var op1 = operand1.text
        var op2 = operand2.text

        // Perform the add with the contents of the operand fields.
        // Print the answer, or "No Result" if add returns nil.
        if let answer = brain.add(op1, op2: op2)?.description {
            result.text = answer
        } else {
            result.text = "No Result"
        }
    }
}

MathWhiz.swift

import UIKit

// Declare that we implement NSDecimalNumberBehaviors so that we can handle
// exceptions without them being raised.
class MathWhiz: NSDecimalNumberBehaviors {
    var badException = false

    // Required function of NSDecimalNumberBehaviors protocol
    func roundingMode() -> NSRoundingMode {
        return .RoundPlain
    }

    // Required function of NSDecimalNumberBehaviors protocol
    func scale() -> CShort {
        return CShort(NSDecimalNoScale)
    }

    // Required function of NSDecimalNumberBehaviors protocol
    // Here we process the exceptions
    func exceptionDuringOperation(operation: Selector, error: NSCalculationError, leftOperand: NSDecimalNumber, rightOperand: NSDecimalNumber) -> NSDecimalNumber? {
        var errorstr = ""

        switch(error) {
        case .NoError:
            errorstr = "NoError"
        case .LossOfPrecision:
            errorstr = "LossOfPrecision"
        case .Underflow:
            errorstr = "Underflow"
            badException = true
        case .Overflow:
            errorstr = "Overflow"
            badException = true
        case .DivideByZero:
            errorstr = "DivideByZero"
            badException = true
        }
        println("Exception called for operation \(operation) -> \(errorstr)")

        return nil
    }

    // Add two numbers represented by the strings op1 and op2.  Return nil
    // if a bad exception occurs.
    func add(op1: String, op2: String) -> NSDecimalNumber? {
        let dn1 = NSDecimalNumber(string: op1)
        let dn2 = NSDecimalNumber(string: op2)

        // Init badException to false.  It will be set to true if an
        // overflow, underflow, or divide by zero exception occur.
        badException = false

        // Add the NSDecimalNumbers, passing ourselves as the implementor
        // of the NSDecimalNumbersBehaviors protocol.
        let dn3 = dn1.decimalNumberByAdding(dn2, withBehavior: self)

        // Return nil if a bad exception happened, otherwise return the result
        // of the add.
        return badException ? nil : dn3
    }
}
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • thank you for your answer; this seems like a nice solution. Will go try it now. – H_H Jun 17 '14 at 19:32
  • Hmm, in #1 and #2, how would one check for errors (in the add function) before returning nil? -- it seems this leads me back to the start of the circle, where I am wanting the check for overflow, but not include the crash that was coming with it (in Swift, that is). – H_H Jun 18 '14 at 14:20
  • Overflow happens when `a + b > maxint`. Subtracting `b` from both sides, you get `a > maxint - b`. So return `nil` in that case. Well that's for integers, but you get the idea. – vacawama Jun 18 '14 at 14:30
  • Thank you. One other thing I notice is that NSDecimalNumber class has a var called NSDecimalNumberExactnessException, which throws an exception if there is an exactness (loss of precision) error. Not sure if it is out of bounds to ask how that is done? – H_H Jun 18 '14 at 15:37
  • It looks like you can define a class that conforms to `NSDecimalNumberBehaviors` protocol and implements `exceptionDuringOperation()` and set that as the default behavior on the `NSDecimalNumber` class with `NSDecimalNumber.setDefaultBehavior`. Then your `exceptionsDuringOperation()` method will be called if an exception happens. Then you return nil. – vacawama Jun 18 '14 at 15:58
  • I implemented the exception handling for `NSDecimalNumber`s. See the detailed example above. – vacawama Jun 19 '14 at 02:38
  • I cannot begin to thank you enough for that awesome example! I have been working on this a lot; and, I think you'd be proud of me -- almost had the protocol implemented, and I believe your example will fill in the missing pieces where I am currently stumped on. Thank you : ) – H_H Jun 19 '14 at 03:04
  • You're welcome. I learned a great deal myself in generating this example. I personally am very happy with Swift. Please mark my answer as correct by selecting the green check mark under the number next to my answer. – vacawama Jun 19 '14 at 03:31
  • I have marked this answer as correct (sorry for taking extra time to do that). Thank you for the included examples and options. – H_H Jul 08 '14 at 14:29
  • Why do you force unwrapping for returned value in function Add? Is this a typo or am I missing something ? – Dominique Vial Jan 28 '15 at 13:16
  • I wrote this 6 months ago. In hindsight, returning NSDecimalNumber? from add is a better idea. – vacawama Jan 28 '15 at 13:24
  • I updated the code to work with Xcode 6.1. Several things have changed in Swift since the beta days. Please up vote my answer if you find it helpful. – vacawama Jan 28 '15 at 13:55
1

Well, you're using an ObjC API. So just handle the exceptions inside ObjC. Write an ObjC class that handles the exceptions and returns values for Swift code to consume.

One possibility would be to write MathWhiz as an ObjC class and return an inout NSError parameter (ie do it the way Core Data does, take an **NSError) and fill it with the proper value when you hit a recoverable error. Then you can eat exceptions from NSDecimalNumber and convert them into NSError values.

You could also write an entire NSDecimalNumber wrapper for Swift consumption that you then use in place of NSDecimalNumber in your Swift code. Perhaps you could overload operator + and its siblings for this class, and then work out how to represent the various possible errors without exceptions.

anisoptera
  • 1,098
  • 7
  • 17
  • thank you. Perhaps the best way would be to use the Objective-C version I already wrote, and study Core Data to implement the parameters and return values the way that you mentioned. Apple said they support all of the current APIs in Swift, but I'm not sure if they made it clear how we should handle exceptions from their objective-C APIs, if we're not using Objective-C (my example above is an exercise using Swift to see how it compares with what I did in Objective-C, and I wanted to see how easily I could convert my work to Swift). – H_H Jun 17 '14 at 19:27
  • Yeah, I'm not sure that they adequately answered that question. My guess is that we'll see Swift equivalents of those APIs that don't use exceptions. – anisoptera Jun 17 '14 at 20:26
1

update answer for swift 2.0

As you mentioned, now Swift 2.0 support with try, throw, and catch keywords.

here is official announcement.

Error handling model: The new error handling model in Swift 2.0 will instantly feel natural, with familiar try, throw, and catch keywords. Best of all, it was designed to work perfectly with the Apple SDKs and NSError. In fact, NSError conforms to a Swift’s ErrorType. You’ll definitely want to watch the WWDC session on What’s New in Swift to hear more about it.

e.g

func loadData() throws { }

func test() {
    do {
        try loadData()
    } catch {
        print(error)
    }
}