Optional types? Only if you really need to

Original author: Alexandros Salazar
  • Transfer
As Rob Napier wrote , we do not know Swift . And it's okay - in fact, it's even great: we have the opportunity to decide for ourselves what this young world will become next. We can (and should) look back at similar languages ​​in search of ideas, although many good practices are more of a community preference than an objective truth. Judging by the long and intense conversations in the developer forums about when and how to use the optional types, I increasingly prefer not to contact them at all.

Optional types are the same tool as everyone else. According to the habit fixed on Objective-C, we usenilwherever you hit - as an argument, default value, boolean value and so on. Using the nice syntax for optional types provided by Swift, you can turn almost anything into an optional type, and work with it in much the same way. Since optional types are unpacked implicitly , it’s still easier: you can use them and not even guess about it. But the question arises - is this reasonable?

I would say no. Even the apparent ease of use is deceiving - Swift was designed as a language without support nil, and the concept of “lack of meaning” was added as an enumeration.nilnot an object of the first kind. Moreover, working with several values ​​of an optional type in one method often leads to a code that you can’t look at without tears. When something was so fundamental in Objective-C, and now expelled from a list of objects of the first kind, it’s interesting to understand the reasons.

Let's start with an example where optional types fall into place. Here is the error handling we've seen in Objective-C for a long time:

NSError *writeError;
BOOL written = [myString writeToFile:path atomically:NO
                            encoding:NSUTF8StringEncoding 
                               error:&writeError]
if (!written) {
    if (writeError) {
        NSLog(@"write failure: %@", 
              [writtenError localizedDescription])
    }
}

This is a confusing situation that optional types help clarify. In Swift, we could write better:

// Тип указан явно для легкости чтения
var writeError:NSError? = myString.writeToFile(path,
                                    atomically:NO, 
                                      encoding:NSUTF8StringEncoding)
if let error = writeError {
    println("write failure: \(error.localizedDescription)")
}

Here the value of the optional type perfectly describes the situation - either there was an error, or there was nothing. Although ... is it?

Actually, not “nothing happened”, but quite successful writing of data to a file took place. Using the enumeration ResultI wrote about earlier , the meaning of the code would correspond to the entry:

// Тип указан явно для легкости чтения
var outcome:Result<()> = myString.writeToFile(path, 
                                   atomically:NO, 
                                     encoding:NSUTF8StringEncoding)
switch outcome {
case .Error(reason):
    println("Error: \(reason)")
default:
    break
}

The text is bigger, but more visual: we check the result of the operation , and it can be successful or unsuccessful 1 . When I see an optional type and stop thinking in Objective-C categories, I increasingly realize that its abstractness hides the essence of what is happening from us.

In my understanding, a type system is a way to give states meaning. Each type has some meaning - an array conveys a sequence of data, a dictionary - the relationship between two representations, and so on. If you look from this point of view, then optional types describe one rather specific case - when the presence of value and its absence are important in themselves. A good example is interactive I / O: the user may or may not enter data, and both states are equally important. But more often it happens that the lack of value speaks of something more than 2 .

When I get the idea to use a generic type, I meet her with skepticism. Often the absence of the value that I am trying to express actually means something else, and the code becomes better if a special type is described for this absence.

• • • • •

On the other hand, Swift code can interact with Objective-C code, and there transfer is nilprohibited only by convention, notes in the documentation and unexpected crashes while the program is running. Such is life, and the ability to use the wonderful Cocoa library more than compensates for these inconveniences - but this does not mean that optional types should be thoughtlessly released outside the interactivity layer.

For example, try to write an extension for NSManagedObjectContext. In Objective-C, the signature would be something like this:

- (NSArray *)fetchObjectsForEntityName:(NSString *)newEntityName
                              sortedOn:(NSString *)sortField
                         sortAscending:(BOOL)sortAscending
                            withFilter:(NSPredicate)predicate;

When trying to access this method from Swift, the signature would look like this:

func fetchObjectsForEntityName(name:String?, 
                           sortedOn:String?, 
                      sortAscending:Bool?, 
                             filter:NSPredicate?) -> [AnyObject]?

This is an absolutely absurd signature. To understand, let's first take a few assumptions:

  • The name of the entity is always needed.
  • We always want to get a result, even if it turns out to be empty


Knowing that Core Data always returns to us NSManagedObject, we can make the signature more meaningful:
func fetchObjectsForEntityName(name:String, 
                           sortedOn:String?, 
                      sortAscending:Bool?, 
                             filter:NSPredicate?) -> [NSManagedObject]

Now let's take a look at the two options for sorting. Two optional types are a terrible choice, as their meanings are somehow related. Maybe we don’t want to sort anything, but we still have to specify the sort order. To do this, we turn to the good old listing and write the following:

enum SortDirection {
    case Ascending
    case Descending
}
enum SortingRule {
    case SortOn(String, SortDirection)
    case SortWith(String, NSComparator, SortDirection)
    case Unsorted
}

Of the five optional types, one remains. In addition, the sorting rule has become more expressive, since it is now possible to use closure in it. The last remaining optional type passes exactly what is required - the filter is either there or not. You can, of course, rewrite it as a separate type (at first I did), but the advantages turned out to be insignificant.

After rethinking our ideas about the principle of code operation, we threw out four unnecessary optional types, simultaneously making the code more visual. This is a definite success.

• • • • •

To summarize: I believe that optional types are needed, but not at all as often as it might seem by their ease of use. The reason for this ease is the need to interact with Objective-C code. If you wrap the parameters of an optional type into enumerations, it will be impossible to use it, but you don’t have to write in Swift as if you were still writing in Objective-C. It’s better to take the most useful concepts that we learned in Objective-C, and improve them with what Swift offers. Then the result will be something really powerful - with optional types only in those places where they are really needed, but not beyond that.

Notes

1. In the Swiftz projecta more competent Result type has been declared for interacting with Cocoa, in which the error is represented by a type object NSError, and not just a string. I would find fault with the fact that they use a less meaningful label Valueinstead Success, but if you want to write real code, most likely you should use this library.

2. Here, an optional example would be implicitly decompressed optional types for IBOutlets: if no value is specified, then the whole application closes as a result of the error, and it should be so. Therefore, it is quite logical to use IBOutletsit as if this value is not optional at all.

Also popular now: