I R Cubic

The personal blog of Daniel E. Bruce (@ircubic).

Want to contact me? You can tweet me or send an email.

Did I mess up? Want to suggest an improvement? File an issue!

Unpacking optionals with switch in Swift

13 Nov 2014

While I was working on a piece of code written in Swift I struggled with making complicated logic with optionals in a nice way. I eventually figured how to do it with the switch statement, complete with unpacking. I’ll first describe the background of the solution (in ways of examples), then the solution I came up with.

If you just want the TL;DR on how to unpack optionals with switch, feel free to skip ahead.

The optionals problem

If you’re working with multiple optionals (which, if you’re interfacing with Objective C, you’ll do a lot of) and you’re only using the if let syntax to deal with it, it can get real nasty and you’ll end up with code like this (or worse):

if let x = optx {
  if let y = opty {
    if let z = optz {
      // etc...
    }
  }
}

This becomes even more problematic if your logic is more straight forward than “check if everything is set”, and could lead to a fair bit of duplication. The Swift answer to situations like these (even without optionals) is the switch statement, which is more of a supercharged matching operator than the switch of C-likes.

However, when optionals are involved it isn’t straight forward how exactly to take advantage of the matching in a sensible matter while not having to deal with unpacking.

To illustrate the problem and a few non-working solutions I’m going to go through an example (similar in spirit to what I was doing), and the solutions you might try. Let’s say you want to write this function:

func checkLegality(name:String?, age:Int?) -> String {
  // to come
}

Inside this function you want to check if a person is legal to enter a bar (in Norway). If they aren’t (or age is unset), return "You're not allowed!", otherwise, return "Welcome to the bar!" or "Welcome to the bar, name!" depending on whether name is set or not.

A couple “obvious”, but non-working, solutions

First, for completeness sake, this is one solution with if let:

func checkLegality(name:String?, age:Int?) -> String {
  if let age = age {
    if age >= 18 {
      var nameString = ""
      if let name = name {
        nameString = ", \(name)"
      }
      return "Welcome to the bar\(nameString)!"
    }
  }

  return "You're not allowed!"
}

It’s not that bad, but definitely not pretty. We really want to do this kind of thing with a switch (especially as the amount of parameters expand). The first try might look something like this:

func checkLegality(name:String?, age:Int?) -> String {
  switch (name, age) {
  case let (n, a) where a >= 18:
    return "Welcome to the bar, " + n + "!"
  case let (nil, a) where a >= 18:
    return "Welcome to the bar!"
  default:
    return "You're not allowed!"
  }
}

Unfortunately, this (slightly contrived to force out the bug) example gives a compile error saying that n needs to be unpacked. So, even though you’re using let, it does not give you an already unpacked value like the if let construct does. This also means that n might be nil at this point, so if you had written "Welcome to the bar, \(n)!" for the first string, it would have compiled, but given "Welcome to the bar, Optional(nil)!" when name is unset. To compound the confusion, you do NOT need to worry about unwrapping optionals in the where test. (This behaviour contradicts some articles written during the XCode 6 beta which indicate it did use to implicitly nil- check in case let statements)

OK, you think to yourself and head back to the Swift book, and stumble upon the section that shows you how to do type checking in a switch. You then try with this, using your newfound knowledge:

func checkLegality(name:String?, age:Int?) -> String {
  switch (name, age) {
  case let (n, a) as (String, Int) where a >= 18:
    return "Welcome to the bar, \(n)!"
  case let (nil, a) where a >= 18:
    return "Welcome to the bar!"
  default:
    return "You're not allowed!"
  }
}

This compiles and seems like it should work, but quite surprisingly returns "You're not allowed!" when called with checkLegality("Name", 24). I can’t quite understand why that is, it seems like a bug, but it is currently the case with XCode 6.1. Luckily, in my case, I had tests to catch this. :)

The related solution of doing case let (n as String, a as Int) will give you the, seemingly incorrect, error message “is test is always true” implying that the downcast (unwrap) is always possible.

The real solution

If none of those solutions work, is it at all possible to use switch to both do logic and unwrap optionals in one fell swoop? The answer is (thankfully, or this article would be a lot more depressing) “Yes!”, but the Swift book doesn’t give the answer in any obvious way.

The crucial hint is that the Optional type acts like it is, under the hood, an enum like this:

enum Optional<T> {
 case Some(x:T)
 case None
}

Armed with this secret knowledge (which is implicitly hinted at in the Generics chapter of the Swift book), you can write the proper solution:

func checkLegality(name:String?, age:Int?) -> String {
  switch (name, age) {
  case let (.Some(n), a) where a >= 18:
    return "Welcome to the bar, \(n)!"
  case let (nil, a) where a >= 18:
    return "Welcome to the bar!"
  default:
    return "You're not allowed!"
  }
}

This works as expected, and gives you n and a as unwrapped values inside the scope of the case statement. The switch statement doesn’t seem to care whether you use nil or .None, but the .Some(x) is required. Also, as a sidenote: as I am only checking the value of a with the where statement and not using it in the case block, I don’t need to wrap it in .Some(), since a nil value implicitly cannot pass the test.

Hopefully that helps :)