Swift: join() and split() explained


Using a method like join() is simple if we look inside the Swift header file (which can be accessed by pressing Command+click on any Swift Type in your code). There we see a simple and easy to follow example demonstrating that it takes a joining String and a String Array, something like this:

let joined = join(", ",["Hello","Swift!"])

And if join() is so simple to understand, then it's counterpart split() should be equally simple to understand. Right? No, wrong. We are hit in the face with things like Seq.GeneratorType.Element->R and expected to decipher them when there's little explanation of what this actually means in the documentation. 

So let's start with the method's requirements:

func split<Seq : Sliceable, R : LogicValue>(seq: Seq, isSeparator: (Seq.GeneratorType.Element) -> R, maxSplit: Int = default, allowEmptySlices: Bool = default) -> [Seq.SliceType]

The first thing to note is that this is a generic function, meaning that it can accept more than one Type. The Types it can accept for its seq parameter must adopt the Sliceable protocol. And this includes String and Array.

The second thing we need to know is that R in the context of this method adopts the LogicValue protocol, which looks like this:

protocol LogicValue {
    func getLogicValue() -> Bool
}
And this tells us that R must be a Bool value - i.e. true or false.

So now we can interpret the rest. Seq as I've stated is an object that is Sliceable. So here we'll use a String. The isSeparator: parameter, as denoted by its syntax (see here), needs a closure that accepts a Seq.GeneratorType.Element and returns an R (a Bool value, as has been outlined). Now, if you already understand Sequence and Generator, what a Seq.GeneratorType.Element is might come easy, but for the rest of us, it can be understood as the parts that the Sliceable object is made up of, which in the case of a String is Characters. So in plain English (or should that be plain Swift?), the closure accepts a Character and returns a Bool.

Something like this:

let separated = split("Split Me!", {(c:Character)->Bool in return c==" "}, allowEmptySlices: false)

which can be abbreviated (once you get the hang of closures) to:

let separated = split("Split Me!", {$0==" "}, allowEmptySlices: false)

But we could also split an Int array (because it is also Sliceable) like this:

let separated = split([1,2,3,4], {$0==3}, allowEmptySlices: false)

In this instance, rather than accepting a Character, the closure accepts an Int. (And so we test it in different ways.)

Note: The maxSplit: and allowEmptySlices: parameters both have default values, so can be omitted or included, it's up to you (the first takes an Int and the second a Bool value).

In both instances (with the String and the Int array) what is returned is an Array, or what the method refers to as a [Seq.SliceType].

Why couldn't it just be an Array? What you need to remember when approaching generic methods is that they aren't using this obscure terminology for sadistic pleasure but in order to be generic.

If we look back at join(), we see that the method's requirements are also generic

fund join<C : ExtensibleCollection, S : Sequence where C == C>(separator: C, elements: S) -> C

and in the opening example, the object which adopted the ExtensibleCollection protocol was a String and the Type that adopted the Sequence protocol was an Array of Strings. It then returned the same Type as our ExtensibleCollection object (i.e. our String) and all worked as expected.

The difference was that we didn't need to think so hard, because we're already used to the kind of syntax expected of us and it could be explained succinctly within a code comment. It didn't need an entire blogpost to make sense of it!

Endorse on Coderwall

Comments

  1. Just a note: Apple changed the order of the parameters in recent builds of 1.2, so isSeparator is now at the end. That means if you use any of the other parameters, you have to add isSeparator or you'll get a really meaningless error.

    ReplyDelete
    Replies
    1. Thank you for the comment and for keeping things up to date.

      Delete
  2. This does not seem to work in xcode 7 beta 3 (7A121l)

    let separated = split("Split Me!", allowEmptySlices: false, {(c:Character)->Bool in return c==" "})

    gets the errors

    Playground execution failed: MyPlayground.playground:6:17: error: cannot invoke 'split' with an argument list of type '(String, allowEmptySlices: Bool, (Character) -> Bool)'
    let separated = split("Split Me!", allowEmptySlices: false, {(c:Character)->Bool in return c==" "})
    ^
    MyPlayground.playground:6:22: note: expected an argument list of type '(S, maxSplit: Int, allowEmptySlices: Bool, isSeparator: @noescape (S.Generator.Element) -> R)'
    let separated = split("Split Me!", allowEmptySlices: false, {(c:Character)->Bool in return c==" "})
    ^

    ReplyDelete

Post a Comment