Occasional Blog New Content on a Sometimes Basis

Swift, Objective-C, and nil

Nothing

Objective-C has never really been happy with the concept of nothing. It uses C's type system, exposing the bare layout of the computer's memory as sequences of bits in an otherwise somewhat high-level object-oriented language. Most types are objects, which are passed around as pointers. And, as we know from C, these pointers are just numbers pointing at sections of memory. Occasionally Cocoa exposes a few structs like NSRange, enums like UIApplicationState, or bitmasks like UIViewAnimationOptions. C concerns itself with the pushing and pulling of these chunks of bits and leaves us software developers to give the bits meaning. And we, as humans who do math, have decided to let zero represent the absence of value. Since then zero has been called nil, NULL, and Nil in a handful of different places, and chances are if you've had Xcode open for more than five minutes you've seen one of these.

Problems

Was this a good idea? As it turns out, nil already gives us a few problems. There's no way to disambiguate between a NSInteger that happens to be zero or a NSInteger that doesn't exist, since they're both represented by zero. If we're dealing with something like a resizeable array, we might use nil to indicate where our array's contents end and the nonsense garbage memory beyond begins. In this case we won't be able to insert nil into our array without marking the insertion point as the end of the array. Collections from Foundation won't let us insert nil at all, choosing death by assertion failure over the corruption of their internal data structures. With NSMutableDictionary, we now have the strange case where returning nil when queried is valid but inserting nil is not. To make things even more confusing, setValue:forKey: won't crash if inserting nil, but setObject:forKey: will.

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"hello?"];           // returns nil
dict[@"goodbye!"] = nil;   // crashes, probably

This is something Cocoa folk get used to very quickly because dictionaries are used everywhere and the assertion failure message is relatively straightforward. What if we want to indicate somehow that a number doesn't exist? We can use NSNumber and awkwardly box and unbox values everywhere. If we stick with our NSInteger type, we have no choice but to set aside a special value to represent the absence of value. No matter what value we choose, we won't be able to disambiguate between that value and the absence of value, so we'd better choose a value that isn't common or we'll have ambiguity problems everywhere. Zero is too common to use for this purpose. Cocoa chooses this to be NSIntegerMax and calls this special value NSNotFound. But now we have a problem where our nothings are not equal and thus must be checked for separately.

NSNotFound == nil; // NO

When allocating an object, Objective-C will set the contents of the object to nil so us software people don't have to fiddle with garbage data. This is an entirely sensible default, but if we have an object containing integers, all of those integers will be set to something that isn't NSNotFound. By default, our integers will exist as zero while everything else will be absent. As it turns out, this is rarely a problem in practice. However, Objective-C method calls will all return nil if sent to a nil pointer, so if we're returning integers and expecting NSNotFound when something doesn't exist we might get behavior that doesn't make sense. Here's some code from Realm's codebase.

+ (BOOL)isSwiftClassName:(NSString *)className {
    return [className rangeOfString:@"."].location != NSNotFound;
}

If className is nil, we have problems.

[RLMSwiftSupport isSwiftClassName:nil]; // returns YES

Even with nullability annotations the Objective-C compiler is unable to tell when a pointer can't be nil.

NSString *__nonnull className = NSStringFromClass([SFSafariViewController class]); // can still be nil

// Returns YES in iOS 8, NO in iOS 9.
[RLMSwiftSupport isSwiftClassName:className];

Objective-C struggles with this a lot.

NSNumber *empty;
[empty integerValue];       // 0
[empty doubleValue];        // 0
empty.doubleValue == 0      // YES
[empty isEqualToNumber:@0]; // NO

NSString *nothing;
[nothing integerValue];                       // 0
[nothing stringByAppendingString:@"not nil"]; // nil
[@"not nil" stringByAppendingString:nothing]; // crash
[nothing isEqualToString:@"not nil"];         // NO
[@"not nil" isEqualToString:nothing];         // crash

MassiveViewController *viewController;
[viewController performSideEffects]; // nothing happens

String addition and equality are crashy and no longer commutative, everything returns zero, side effects are not performed. Often code in which this happens will handle the ensuing results correctly on accident, which works great until all of a sudden something changes and the code suddenly doesn't work anymore.

Somehow, our pile of problems continues to grow. Because ObjC enums are just special names for integers, they don't have a good way of representing nil either. We can add special cases in our enums for when something doesn't exist and make those zero so that they'll equal nil, but then we're forced to handle the possibility of nothing even when we know that we have something. These special cases usually have names like MSCSomethingSomethingUnknown. Foundation enums do not have these special values, because it's really strange to have a handful of cases each representing one possibility and then a single case representative of all other possibilities. If anything, that special case should be handled separately outside of any switch statement that handles the rest. However, if we insist on having these special values, we can get away with only minor inconvenience by having an empty case statement in our switch, or we can commit great evil and give up on exhaustiveness either by lying to the compiler or by adding a default: statement. If we do the latter, the compiler will not warn us when we add a new case to our enum. There is no need to abandon the compiler. It is our friend, and it wants to help us.

Every once in a while we may encounter the solitary NSNull singleton. It is not extremely harmful, but it isn't nil, so it must be accounted for separately. Due to its singleton nature, we can simply check for equality with the == operator. It appears occasionally in serialized JSON dictionaries to cause unrecognized selector crashes.

In summary, we have problems because everything in C can be zero and nil is just another name for zero. Some types already have meaningful names and uses for zero, in which case we must use something else to represent nothingness. Foundation collections do not allow for the insertion of nil or non-object types, so we need yet another way to represent it. We can never separate nil from our types because our types are all just numbers and zero is a valid number.

Solutions

In Swift, the possibility of nothing is indicated by the Optional type (and its relative ImplicitlyUnwrappedOptional that I won't talk about), which is simply an enum with two cases. nil itself is a literal for Optional.None. And rather than being just another name for zero, Optional.None is a representation of the concept of nothing.

enum Optional<Wrapped> {
    case .None
    case .Some(Wrapped)
}

nil no longer has any strict physical representation. Instead, nil can be a literal declaration for any adopter of the NilLiteralConvertible protocol.

/// Conforming types can be initialized with `nil`.
public protocol NilLiteralConvertible {
    /// Create an instance initialized with `nil`.
    public init(nilLiteral: ())
}

The concept of nothing is sort of a black hole from which no meaning can be gleaned. Things can become nil, but nil cannot become things in a meaningful way. To do so would be literally making something out of nothing, which only works in movies. Consequently any initializer from nil should practically write itself.

extension Optional : NilLiteralConvertible {
  public init(nilLiteral: ()) {
      self = .None
  }
}

Optional is a generic type, which means we can create entirely new types by taking existing concrete types and applying the Optional type constructor. Optional by itself cannot be used, but we could apply it to an Int to get Optional<Int>, a concrete type representing either an integer or nothing. We no longer have to sacrifice individual values in our type to represent nothing. Optionals can be added to collections, negating the need for NSNull. Similar to the Optional<Int> example, we can create optional enums and have two distinct types for when an value definitely exists and when it might not. Optionals can be handled in a switch statement just like any other enum, allowing us to handle nil as the special edge case that it is.

Swift lets us declare optionals in a much less verbose way using a postfix ? so we don't have to type out Optional every time.

let maybe: Int? = /* ... */

// Xcode insists on this sort of indentation.
switch maybe {
case .Some(let unwrapped):
    switch unwrapped {
    case 0:
        print("zero is not nil")
    case NSIntegerMax:
        print("this isn't nil either")
    default:
        print("a non-zero number!")
    }
case .None:
    print("nil is a special snowflake")
}

Swift's guard and if let bindings make this even nicer.

func printValue(optionalValue: Value?) {
    guard let value = optionalValue else {
        print("(null)")
        return
    }

    print(value)
}

func printValue(optionalValue: Value?) {
    if let value = optionalValue {
        print(value)
    } else {
        print("(null)")
    }
}

Swift optionals solve all of our problems in an elegant way. When we create optional types, we create entirely new types that preserve the original types within. Wrapped? is a new type that is one space bigger than Wrapped, so we don't need to pick a special value to represent nil. Swift's standard library magic prevents optionals from breaking collections, which is a benefit we get for free. If nil breaks any other method out there, we can just change the method signature to take a non-optional type and Swift's type checker will error at compile time if we try to pass that method an optional. And because optionals are the only way to represent nil, this essentially eliminates all unexpected nil runtime problems.

With optionals in mind, there is one last way of expressing nothing that I want to mention. When working with collections, empty collections are usually good ways to represent an absence of value because most collection logic is already well-equipped to handle empty collections. If those collections were optionals, the collection logic consuming those optionals would have to handle emptiness and nil separately, even if they are representative of the same thing. On the other hand, things like strings should be left as optional values even though the empty string exists because people tend to not parse strings on a character-by-character basis.

The ability to represent nothing is really useful and important. Swift's single unified representation of nothing makes dealing with the absence of value much easier and much more deliberate--we can choose the possibility of runtime crashes, but we don't have to.

If you have any questions or comments, feel free to reach out to me on twitter.