Error Handling in Swift - Sword and Magic

Original author: Alexandros Salazar
  • Transfer
If from a distance you can see the big picture, then you can understand the essence in the vicinity. Concepts that seemed distant and, frankly, strange to me while experimenting with Haskell and Scala, when programming with Swift, become dazzlingly obvious solutions to a wide range of problems.

Take here error handling. A specific example is the division of two numbers, which should throw an exception if the divisor is zero. In Objective-C, I would solve the problem like this:

NSError *err = nil;
CGFloat result = [NMArithmetic divide:2.5 by:3.0 error:&err];
if (err) {
    NSLog(@"%@", err)
} else {
    [NMArithmetic doSomethingWithResult:result]

Over time, this began to seem like the most familiar way of writing code. I don’t notice which squiggles I have to write and how indirectly they are related to what I really want from the program:

Give me back the meaning. If it doesn’t work out, then let me know so that the error can be handled.

I pass parameters, dereference pointers, return a value in any case, and in some cases then ignore it. This is unorganized code for the following reasons:

  • I speak machine language - pointers, dereferencing.
  • I myself must provide the method with a way that it will notify me of an error.
  • The method returns a certain result even in case of an error.

Each of these points is a source of possible bugs, and Swift solves all these problems in its own way. The first point, for example, in Swift does not exist at all, since it hides all the work with pointers under the hood. The remaining two points are solved using transfers.

If an error can occur during the calculation, then there can be two results:

  1. Successful - with return value
  2. Unsuccessful - preferably with an explanation of the cause of the error

These options are mutually exclusive - in our example, dividing by 0 causes an error, and everything else returns a result. Swift expresses mutual exclusion using " enumerations ." Here is the description of the calculation result with a possible error:

enum Result {
    case Success(T)
    case Failure(String)

An instance of this type can be either a label Successwith a value, or Failurewith a message describing the reason. Each case keyword is described by a constructor: the first takes an instance T(result value), and the second String(error text). This is what the earlier Swift code would look like:

var result = divide(2.5, by:3)
switch result {
    case Success(let quotient):
    case Failure(let errString):

A little more authentic, but much better! The design switchallows you to associate values ​​with names ( quotientand errString) and access them in code, and the result can be processed depending on the occurrence of an error. All problems resolved:

  • No pointers, but dereferencing even more so
  • No need to pass divideextra parameters to functions
  • The compiler checks to see if all enumeration options are processed.
  • Since quotientthey errStringturn into an enumeration, they are declared only in their branches and it is impossible to refer to the result in case of an error

But most importantly - this code does exactly what I wanted - calculates the value and processes the errors. It is directly related to the task.

Now let's look at a more serious example. Suppose I want to process the result - get the magic number from the result, find the smallest prime divisor from it and get its logarithm. There is nothing magical in the calculation itself - I just chose random operations. The code would look like this:

func magicNumber(divisionResult:Result) -> Result {
    switch divisionResult {
        case Success(let quotient):
            let leastPrimeFactor = leastPrimeFactor(quotient)
            let logarithm = log(leastPrimeFactor)
            return Result.Success(logarithm)
        case Failure(let errString):
            return Result.Failure(errString)

It looks easy. But what if I want to get from a magic number ... a magic spell that matches it? I would write like this:

func magicSpell(magicNumResult:Result) -> Result {
    switch magicNumResult {
        case Success(let value):
            let spellID = spellIdentifier(value)
            let spell = incantation(spellID)
            return Result.Success(spell)
        case Failure(let errString):
            return Result.Failure(errString)

Now, however, I have an expression in each function switch, and they are about the same. Moreover, both functions handle only a successful value, while error handling is a constant distraction.

When things begin to repeat themselves, it is worth considering a method of abstraction. And again, there are the right tools in Swift. Enumerations can have methods, and I can get rid of the need for these expressions switchusing the method mapfor enumeration Result:

enum Result {
    case Success(T)
    case Failure(String)
    func map

(f: T -> P) -> Result

{ switch self { case Success(let value): return .Success(f(value)) case Failure(let errString): return .Failure(errString) } } }

The map method is named because it converts toResultResult

, and it works very simply:

  • If there is a result, the function is applied to it. f
  • If there is no result, the error is returned as is

Despite its simplicity, this method allows you to work real miracles. Using error handling inside it, we can rewrite our methods using primitive operations:

func magicNumber(quotient:Float) -> Float {
    let lpf = leastPrimeFactor(quotient)
    return log(lpf)
func magicSpell(magicNumber:Float) {
    var spellID = spellIdentifier(magicNumber)
    return incantation(spellID)

Now the spell can be obtained like this:

let theMagicSpell = divide(2.5, by:3).map(magicNumber)

Although you can get rid of methods altogether:

let theMagicSpell = divide(2.5, by:3).map(findLeastPrimeFactor)

Isn't it cool? All the need for error handling is removed inside the abstraction, and I only need to specify the necessary calculations - the error will be forwarded automatically.

This, on the other hand, does not mean that I will never have to use the expression again switch. At some point, you will either have to output an error, or pass the result somewhere. But this will be one single expression at the very end of the processing chain, and intermediate methods should not care about error handling.

Magic, I tell you!

This is not just academic “knowledge for the sake of knowledge”. Abstraction of error handling is very often used in data transformation. For example, often you need to get data from the server, which come in the formJSON(error string or result), convert them to a dictionary, then to an object, and then transfer this object to the UI level, where several more separate objects will be created from it. Our listing will allow you to write methods as if they always work on valid data, and errors will be thrown between calls map.

If you've never seen such tricks before, think about it for a while and try to tinker with the code. (For some time, the compiler had problems with generating code for generalized enumerations, but perhaps everything is already compiling). I think you will appreciate how powerful this approach is.

If you are good at math, you probably noticed a bug in my example. The logarithm function is not declared for negative numbers, and type values Floatmay be so. In this case, it logwill return not just Float, but rather . If we pass such a value to map, then we will get a nested one , and working with it so simply will not work. For this, there is also a trick - try to come up with it yourself, but for those who are too lazy - I will describe in the next article.ResultResult

Also popular now: