Optionals? If We Must

As Rob Napier stated, we don’t know Swift. That’s fine—in fact, that’s great: we get to decide now, when the world is young, what we want it to look like. We can and should look at similar languages for ideas, but a lot of best practices are more community preferences than objective truths. And when it comes to optionals, based in part on the long and complex developer forum discussions on how best to use them, my preference is quickly becoming to avoid them.

• • • • •

Optionals are a tool like any other, and every tool has a use. But coming as we do from Objective-C, we are used to passing nil everywhere: nil as a parameter, nil as a starting value, nil as a logical value, etc. With the nice optional syntax Swift gives us, we can basically turn everything into an optional and have very similar behavior. With implicitly unwrapped optionals, things are even easier: we can use them and never be aware of them. The question becomes: is this wise?

I would argue not. Even the seeming ease of use is an illusion: Swift was designed as a language where nil was not supported, and added support for nothingness via an enum: nil is not a first-class construct. Moreover, trying to handle multiple optionals in the same method body typically ends in tears. When something that common and fundamental to Objective-C is explicitly relegated to second class, it’s worth asking why.

Let me begin with an example where optionals make sense. Here’s our old friend error handling in Objective-C, adapted from an example in the Swift book:

NSError *writeError;
BOOL written = [myString writeToFile:path atomically:NO
                            encoding:NSUTF8StringEncoding 
                               error:&writeError]

if (!written) {
    if (writeError) {
        NSLog(@"write failure: %@", 
              [writtenError localizedDescription])
    }
}

This is an obfuscated situation that optionals clarify. In Swift we could improve matters with an API along the lines of:

// Type annotation added for clarity
var writeError:NSError? = myString.writeToFile(path,
                                    atomically:NO, 
                                      encoding:NSUTF8StringEncoding)

if let error = writeError {
    println("write failure: \(error.localizedDescription)")
}

Here, the optional perfectly describes the situation: either there was an error, or there was nothing. Or does it?

“There was nothing” isn’t really the result of the operation. The true result is that the operation was successful. Using the Result enum I wrote about before, the code takes its full meaning:

// Type annotation added for clarity
var outcome:Result<()> = myString.writeToFile(path, 
                                   atomically:NO, 
                                     encoding:NSUTF8StringEncoding)

switch outcome {
case .Error(reason):
    println("Error: \(reason)")
default:
    break
}

This is slightly more verbose, but much clearer: we are checking an outcome and it can be a success or a failure.1 Over and over again, when I see an optional and I stop thinking in Objective-C terms, I find that the optional, being so generic, obscures the nature of what’s going on.

I see the type system as a way of adding meaning to state. Every type gives us information: an array tells us there is an ordering to the data, a dictionary tells us there is a correspondence between one representation and another, etc. Viewed like this, optionals model one very specific situation: a situation where the presence of something and its absence are inherently meaningful. A good example is interactive IO: it is possible the user gives some input or no input, and both are equally meaningful. But often, when there is presence and absence of something, the absence carries significance beyond itself.2

Because of their catch-all nature, then, I feel the proper reaction to thinking “I should use an optional” is “should I, really?” Odds are the nothingness I’m thinking about representing really means something more than just “nothing”, and code benefits from that meaning being embodied in a dedicated type.

• • • • •

That said, Swift interacts with Objective-C, where passing nil is always an option, prohibited only by convention, documentation—and runtime explosions. So we will often find ourselves dealing with Objective-C methods and functions that take and return optionals. It is what it is, and the fact that we have access to the amazing Cocoa libraries more than makes up for the awkwardness—but that doesn’t mean we should propagate optionals indiscriminately beyond that interaction layer.

Take, for instance, the idea of writing an extension for NSManagedObjectContext that will allow name-based fetching of entities. In Objective-C, it might have the following signature:

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

Trying to access this from a Swift file would show us this signature:

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

That’s an absurd signature to have. To clean it up, let’s begin with two assumptions:

  1. We will always want an entity name.
  2. We will always get a result, even if it’s empty.

We can combine that with our knowledge that we always get NSManagedObjects out of Core Data to get the more sensible:

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

Next, let’s turn our attention to the two sorting parameters. The two optionals, sortedOn and sortAscending, are actually a terrible representation of what we want, because they’re correlated. If we don’t need the objects sorted, we don’t need to specify if the sorting should be ascending or descending. So we turn to our old friend the enum and define:

enum SortDirection {
    case Ascending
    case Descending
}

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

This allows us to rewrite the declaration as:

func fetchObjectsForEntityName(name:String, 
                        sortingRule:SortingRule, 
                             filter:NSPredicate?) -> [NSManagedObject]

We have gone from having five optionals to having only one. Moreover, the sorting rule is more expressive, since it also allows us to pass a closure. As for the last remaining optional, it does carry its full meaning: either there is a filter or there isn’t. One could create a dedicated type (and I originally did just that), but the benefits are slim and the optional syntax would make it more of a burden than a help.

Still, at the end of the day, by reevaluating what our assumptions were, we took a method with five optionals and turned it into one with one, in the process clarifying the problem space. That’s a definite win.

• • • • •

So there it is: I think optionals are useful, but in fewer cases than their ease of use implies. I think the ease exists because Swift must interact with Objective-C, and using optionals as full enums every time would be borderline unbearable; that said, we should not conclude that optionals are to be used to bring Objective-C-style nil handling into Swift.

Indeed, we should not be writing Objective-C in Swift, but instead taking the best we learned from Objective-C and enhancing it with all that Swift has to offer. Then we’ll have something truly special and powerful—with optionals used as needed, but no more.


1 The Swiftz project actually has a better Result type for Cocoa interaction, which wraps an NSError object rather than just a string. I quibble with the use of Value instead of Success, which I find less meaningful, but if you’re looking to write real code, that’s probably the one you want to use. ↩︎

2 In this context, implicitly unwrapped optionals for IBOutlets are a great model: their absence leads to an application-ending error, which is as it should be when IBOutlets are not set. So the fact that one uses them as if they weren’t optional at all is appropriate. ↩︎

 
110
Kudos
 
110
Kudos

Now read this

Immutable Swift

Swift is a new language designed to work on a powerful, mature, established platform—a new soul in a strange world. This causes tension in the design, as well as some concern due to the feeling that the power of Cocoa is inextricably... Continue →