Swift: Missing Pieces & Surviving Change

Nicholas Maccharoli

February 22nd, 2016

Change

Swift is still a young language when compared to other languages like C, C++, Objective-C, Ruby, and Python. Therefore it is subject to major changes that will often result in code breaking for simple operations like calculating the length of a string.

Packaging functionality that is prone to change into operators, functions or computed properties may make dealing with these transitions easier. It will also reduce the number of lines of code that need to be repaired every time Swift undergoes an update.

Case study: String Length

A great example of something breaking between language updates is the task of getting a string’s character length. In versions of Swift prior to 1.2, the way to calculate the length of a native string was countElements(myString), but then in version 1.2 it became just count(myString). Later at WWDC 2015 Apple announced that many functions that were previously global –such as count - were now implemented as protocol extensions. This resulted in once again having to rewrite parts of existing code as myString.characters.count.

So how can one make these code repairs between updates more manageable? With a little help from our friends Computed Properties of course!

Say we were to write a line like this every time we wanted to get the length of a string:

let length = count(myString)

And then all of a sudden this method becomes invalid in the next major release and we have unfortunately calculated the length of our strings this way in, say, over fifty places. Fixing this would require a code change in all fifty places. But could this have been mitigated? Yes, we could have used a computed property on the string called length right from the start:

extension String {
   var length : Int {
       return self.characters.count
   }
}

Had our Swift code originally been written like this, all that would be required is a one line change. This is because the other fifty places would still be receiving a valid Int from the call myString.length.

Missing Pieces

Swift has some great shorthand and built in operators for things like combining strings - let fileName = fileName + ".txt" - and appending to arrays - waveForms += ["Triangle", "Sawtooth"]. So what about adding one dictionary to another?

//Won't work
let languageBirthdays = ["C" : 1972, "Objective-C": 1983] + ["python" : 1991, "ruby" : 1995]

But it works out of the box in Ruby:

compiled = { "C" => 1972, "Objective-C" => 1983 }
interpreted = { "Ruby" => 1995, "Python" => 1991 }
programming_languages = compiled.merge(interpreted)

And Python does not put up much of a fuss either:

compiled = {"C":1972, 'Objective-C': 1983}
interpreted = {"Ruby":1995, "Python": 1991}
programming_languages = compiled.update(interpreted)

So how can we make appending one dictionary to another go as smoothly as it does with other container types like arrays in Swift?

By overloading the + and += operators to work with dictionaries of course!

func + <Key, Value> (var lhs: Dictionary<Key, Value>, rhs: Dictionary<Key, Value>) -> Dictionary<Key, Value> {
   rhs.forEach { lhs[$0] = $1 }
  
   return lhs
}

func += <Key, Value> (inout lhs: Dictionary<Key, Value>, rhs: Dictionary<Key, Value>) -> Dictionary<Key, Value> {
   lhs = lhs + rhs
  
   return lhs
}

With a light application of generics and operator overloading we can make the syntax for dictionary addition the same as the syntax for array addition.

Operators FTW: Regex Shorthand

One thing that you may have encountered during your time with Swift is the lack of support for regular expressions. At the time of writing, Swift is currently at version 2.1.1 and there is no Regular Expression support in the Swift Standard Library. The next best thing to do is to rely on a third party library or Foundation Framework's NSRegularExpression.

The issue is that writing code to use NSRegularExpression to find a simple match is a bit long winded every time you wish to check for a match. Putting it into a function is not a bad idea either, but defining an operator may make our code a bit more compact.

Taking inspiration from Ruby's =~ regex operator, let’s make a simple version returning a bool representing if there was a match:

infix operator =~ { associativity left precedence 140 }

func =~ (lhs: String, rhs: String) -> Bool {

   if let regex = try? NSRegularExpression(pattern: rhs, options: NSRegularExpressionOptions.CaseInsensitive) {

   let matches = regex.matchesInString(lhs, options: NSMatchingOptions.ReportCompletion, range: NSMakeRange(0, lhs.length))
       return matches.count > 0

   } else {
       return false
   }
}

(Take note of our trusty length computed property springing to action.)

This time around there is no operator as of Swift 2.1 called =~. Therefore, we need to first define the symbol telling the Swift compiler that it is an operator that is infix taking objects on the left and right side, with a precedence of 140, and its associativity is left. Associativity and precedence only matter when there are multiple operators chained together, but I imagine most uses of this operator being something like:

guard testStatus =~ "TEST SUCCEEDED" else {
   reportFailure()
}

Have fun but be courteous

It would be wise to observe The Law of the Instrument and not treat everything as a nail just because you have a hammer in arm’s reach. When making the decision to wrap functionality into an operator or use a computed property in place of the canonical way of coding something explicitly, first ask yourself if this is really improving readability. It could be that you’re just reducing the amount of typing – think about how easily the next person reading your code could adapt.

If you want to create even better Swift apps then check out our article to make the most of the Flyweight pattern in Swift - perfect when you need a large number of similar objects!

About the author

Nick Maccharoli is an iOS / Backend developer and Open Source enthusiast working at a startup in Tokyo and enjoying the current development scene. You can see what he is up to at @din0sr or github.com/nirma