Let’s imagine, you come into work one bright and sunny Monday morning to find a new requirement has landed in your inbox:
Please create a function that accepts two arrays of Floats and returns a new array of tuples containing those source array items side by side with a third property containing a Boolean indicating whether the items from the input arrays are the same value.
Easy, you think. My function simply needs to look at those two arrays, loop over all the values and, one by one, populate an output array with with a tuple containing the number from the first array, the number from the second and a Boolean indicating whether they are equal.
A five minute job:
func horizontalJoinCompare_v1(firstArray: [Float], secondArray: [Float]) -> [(Float, Float, Bool)]
{
var returnArray = [(Float, Float, Bool)]()
let n = min(firstArray.count, secondArray.count)
forvar i = 0; i < n; i++
{
returnArray.append((firstArray[i], secondArray[i], firstArray[i] == secondArray[i]))
}
return returnArray
}
Sure enough, plug in two arrays:
let sourceOne = [Float(2.5), Float(3.9), Float(6.25)]
let sourceTwo = [Float(1.5), Float(3.9), Float(6.05), Float(4.75)]
…and we get just what we expect:
let resultOne = horizontalJoinCompare_v1(sourceOne, sourceTwo) // [(.0 2.5, .1 1.5, .2 false), (.0 3.9, .1 3.9, .2 true), (.0 6.25, .1 6.05, .2 false)]
A cup of tea and a chocolate HobNob later, you remember that Swift 1.2 introduced the zip() function. zip() creates a sequence of pairs built from two input sequences which means you can ditch using subscripting to access each element and use the more elegant for in. Having to repeat the tuple definition, (Float, Float, Bool), is a bit clumsy too, so you decide to use a type alias to save on typing and have code that’s slightly more descriptive:
typealias JoinCompareResult = (Float, Float, Bool)
func horizontalJoinCompare_v2(firstArray: [Float], secondArray: [Float]) -> [JoinCompareResult]
{
var returnArray = [JoinCompareResult]()
let zippedArrays = zip(firstArray, secondArray)
for zippedItem in zippedArrays
{
returnArray.append((zippedItem.0, zippedItem.1, zippedItem.0 == zippedItem.1))
}
return returnArray
}
Neater, but could this be improved? Well, Swift is all about being functional, isn’t it. Why loop over that zipped sequence when map() allows us to execute code against the zipped arrays and populate the output array in a much more functional style. To use map(), we need to put the code in a closure which will execute against each item. While we’re at it, why not create another alias for the return array itself:
typealias JoinCompareResults = [(Float, Float, Bool)]
func horizontalJoinCompare_v3(firstArray: [Float], secondArray: [Float]) -> JoinCompareResults
{
returnArray(zip(firstArray, secondArray)).map
{
(zippedItemOne, zippedItemTwo) inreturn (zippedItemOne, zippedItemTwo, zippedItemOne == zippedItemTwo)
}
}
One thing to note is that zip() returns a SequenceType and map() is a method of Array, so we need to cast the result of zip() to Array.
There’s still some cruft in this function. Do we really need to name the two arguments to the map() function? Actually, no, Swift gives us $0, $1,… as references to the closure arguments, and it doesn’t require an explicit return either, so lets remove the superfluous:
func horizontalJoinCompare_v4(firstArray: [Float], secondArray: [Float]) -> JoinCompareResults
{
returnArray(zip(firstArray, secondArray)).map { ($0, $1, $0 == $1) }
}
Just as you’re about to treat yourself to a second cup of tea and maybe celebrate writing such a beautiful function by cracking open the Jaffa Cakes, another email arrives. Not only does the function need to compare arrays of floats, it needs to offer similar functionality upon arrays of strings too.
Maybe your immediate thought is to copy and paste your code and simply change the types - after all, Swift’s polymorphism allows for identically named functions with different signatures. Just as you press cmd-c, ping!, another email, “sorry, forgot, your function needs to support integers too”.
Here’s where generics come to the rescue. Rather than specifying a specialised type against the argument of a method, we precede the argument list of a function with type parameters, typically named T, U, etc, in angle brackets. In this next iteration of the horizontalJoinCompare() function, you define the function to accept two arrays which must contain identical types which conform to the Equatable protocol:
typealias GenericJoinCompareResults = [(Equatable, Equatable, Bool)]
func horizontalJoinCompare_v5<T:Equatable>(firstArray: [T], secondArray: [T]) -> GenericJoinCompareResults
{
returnArray(zip(firstArray, secondArray)).map { ($0, $1, $0 == $1) }
}
Now, not only does our function work as the first version did with arrays of floats:
let resultFive = horizontalJoinCompare_v5(sourceOne, sourceTwo)
…it will also happily accept arrays of strings;
let stringSourceOne = ["ABC", "DEF", "GHI", "JKL"]
let stringSourceTwo = ["AGC", "DEF", "KLZ"]
let resultSix = horizontalJoinCompare_v5(stringSourceOne, stringSourceTwo)
…and there we have it: what started as a slightly clunky and very specialised piece of code is now very generic and very elegant. Easy!
Addendum: Many thanks to Al Skipp for pointing out that Swift has a global map() function which accepts a sequence as an argument, This means there's no need to case the result of zip() to an array and allows the function to be tidied up even further:
func horizontalJoinCompare_v6<T:Equatable>(firstArray: [T], secondArray: [T]) -> GenericJoinCompareResults
{
return map(zip(firstArray, secondArray)){ ($0, $1, $0 == $1) }
}
Cheers Al!