Using Equatable and NilLiteralConvertible to re-implement Optionals in Swift (Part 2)

 

This post updated December 6, 2014 to reflect changes in Xcode 6.2

A few weeks ago I did a little thought experiment on how we might re-implement Swift’s optional type by using the Swift enum. If you haven’t read that yet, you can read it here. In this post, we’re going to push a little further and see if we can get something that corresponds a little more closely to the Swift Optional type.

In the last post we ended up with this class, JOptional:

enum JOptional<T> {
    case None
    case Some(T)
    
    init(_ value: T) {
        self = .Some(value)
    }
    
    init() {
        self = .None
    }
    
    func unwrap() -> Any {
        switch self {
        case .Some(let x):
            return x
        default:
            fatalError("Unexpectedly found nil while unwrapping an JOptional value")
        }
    }
    
}

…and a custom operator…

postfix operator >! {}
postfix func >! <T>(value: JOptional<T> ) -> Any {
    return value.unwrap()
}

It’s fairly straightforward to use the class by using the generic constructor, passing in your value, and then using our overloaded >! operator to unwrap.

// Instantiate some optionals
var myOptionalString = JOptional("A String!")
var a = myOptionalString.unwrap() // "A String!"
var c = myOptionalString>!     // "A String!"

In Swift’s actual Optional implementation, as of Xcode 6 Beta 5, Optionals can also be compared to nil, without doing any unwrapping first. For example:

var myNilValue : String? = nil

if(myNilValue == nil) {
    println("This is nil!")
}
else {
    println("This isn't nil, carry on")
}
This is nil!

This simply creates your standard Swift optional set to a nil value, then we can compare directly to nil and act upon this information. If we try this same code with our version of optional, we run in to a problem:

var myNilJOptional = JOptional(myNilValue)

if(myNilJOptional == nil) {
    println("This is nil!")
}
else {
    println("This isn't nil, carry on")
}
This isn't nil, carry on

Or even worse, you might get an error about Optional<String?> not conforming to MirrorDisposition, this is basically Swift being unable to find comparison operators between Optional and nil… so it’s falling back to what it can find: MirrorDisposition, which is something Swift uses for some of the nifty IDE features.

So, what’s going on here?

Let’s dig a little deeper… If we comment out our comparisons and just put a breakpoint right after the setting of the myNilJOptional value, and check them out in the watch window, we see the following:

The JOptional is nil, but it has a ‘Some’ case instead of ‘None’, because we used the init(_ T) method to instantiate it, and the init method sets the Some case even if value is nil. As a result, JOptional is in fact *not* equal to nil!

We could modify this behavior by doing a check on that init method and setting None if the object is nil, but this introduces the issue of needing to update the case if the inner object is modified. This introduces a variety of issues, so what may be better is to simply implement two Swift interfaces that help solve this common problem: Equatable and NilLiteralConvertible.

Equatable just specifies that we are going to implement our own operator for ==, specific to our type.
NilLiteralConvertible gives an interface to convert between JOptionals and nil literals. In our case, a nil object with type JOptional should be converted to JOptional.None.

So first, we add the interfaces to our JOptional definition:

enum JOptional<T> : Equatable, NilLiteralConvertible {
  ...

Next, we need to implement the equality operator. Because this is an operator overload, it goes in global space (outside of the enum):

func == <T>(lhs: JOptional<T>, rhs: JOptional<T>) -> Bool {
    switch (lhs,rhs) {
    case (.Some(let lhsVal), .Some(let rhsVal)):
        // Both have a value, the *optionals* are equal, although the values might not be
        return true
    case (.None, .None):
        // Both are nil
        return true
    default:
        // One does not have a value, but the other does
        return false
    }
}

We’re using pattern matching of tuples here in order to handle three cases:

  1. Both values are not-nil
  2. Both values are nil
  3. Values nil-ness does not match

It’s important to note, comparing two optional (even in Swift’s implementation) only compares that the case is the same (Some vs None) It does not compare the inner values, you need to unwrap for that behavior.

If we get a nil lhs or rhs value in this == operator, we need Swift to know to convert that to the .None case. So we implement the init(nilLiteral: ()) method.

init(nilLiteral: ()) {
    self = None
}

So now, not only can we say things like this:

myNilJOptional = nil

And the result will be that myNilJOptional is set to JOptional.None

At the same time, we can do direct comparisons of JOptional values due to the equatable method, including to nil values. Like this:

if(myNilJOptional == nil) {
  // Do something
  println("This JOptional is nil!")
}

Checking out earlier code again, we get the expected result:

This is nil!
This is nil!

Full source code of Part 2 found here &raqou;

This topic is explored more in-depth in my upcoming book.

In the meantime, take a look at my other tutorials and posts about Swift.

Follow me on Twitter
Developing iOS 8 Apps in Swift
An upcoming ebook detailing everything you need to know to produce marketable apps for iOS 8 using swift.
Learn to produce real world applications through tutorials. Available for pre-order today at a 50% discount.

Early Access Available Now


Sign up now and get a set of FREE video tutorials on writing iOS apps coming soon.



Subscribe via RSS

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>