Understanding the Slice type in Swift

This post has been updated for Swift 1.2.

When I first heard about Swift, I regarded its type inference capabilities as a convenience: type safety without all the typing. What I’ve come to realize, though, is that Swift’s type inference lets it hide a great deal of the subtle ways it makes things faster – things largely “just work,” even if we aren’t always using the types we think we are. Without an understanding of these subtle tricks, however, a programmer might undo the very things that make Swift so… swift.

For example, take the humble Array. We can create a new array, grab a chunk of it, check the length of the chunk, and loop through its contents, like this:

let jennysNumber = [8, 6, 7, 5, 3, 0, 9]    // ee-ai-een
let jennysExchange = jennysNumber[0..<3]
println(jennysExchange.count)
> 3
for digit in jennysExchange {
    println(digit)
}
> 8
> 6
> 7

Let’s check our types, too. Option-click on jennysNumber – yep, it’s an [Int], just like we expected. Do the same with jennysExchange and it’s – ArraySlice<Int>? Where did that come from?

The answer has to do with how Swift can make array operations run at C-like speeds while maintaining type safety and immutability. Read on to learn about slices, and what you need to know to work with them.

What is a slice? Why is it used?

You can think of an ArraySlice as a window into a portion of an existing Array instance. When we subscripted a range of jennysNumber, instead of allocating space in memory for a new array and copying the relevant elements over, Swift essentialy gave us a view of those three elements still inside the jennysNumber array.

let jennysNumber = [8, 6, 7, 5, 3, 0, 9]let jennysExchange = jennysNumber[0..<3]

Early on in the Swift beta, this behavior was completely transparent – the immutability of array elements wasn’t enforced, so you could modify the original array by changing values in a slice. Fortunately, the language was subsequently revised to ensure immutability.

Now, Swift knows whether our new slice is mutable or constant, and it knows when we’re calling any potentially mutating operations on either the slice or the original array. That means it can delay (or completely avoid) duplicating the doubly-referenced data, saving time and memory. The ArraySlice type has its own implementations of all the same methods and properties as Array, so from our perspective using the slice, we don’t really notice that it’s a different type; this optimization is handled transparently for us.

Transparently, that is, until we try to call a method that expects an Array instance:

func concatenateNumbers(numbers: [Int]) -> String {
    let result = numbers.reduce("") { $0 + "\($1)" }
    return result
}

println(concatenateNumbers(jennysNumber))
> "8675309"
println(concatenateNumbers(jennysExchange))
> error: 'ArraySlice<Int>' is not convertible to '[Int]'

Slicing and dicing

How can we work around this? We have three options: we can convert our slice back to an array, we can overload our function with a version that takes a slice, or we can rewrite the function as a generic. Let’s take a look at each option in turn.

.1: Converting back to an Array

Can’t we just cast the slice back to an array?

let jennysExchangeArray = Array(jennysExchange)
println(concatenateNumbers(jennysExchangeArray))
> "867"

Wait! That’s not a cast, that’s the initialization of a brand new array. Instead of getting a window, we’ve created an array and copied over the subscripted contents:

let jennysNumber = [8, 6, 7, 5, 3, 0, 9]let jennysExchange = jennysNumber[0..<3]let exchangeCopy = Array(jennysExchange)

It works, but it defeats the hard work of the good people behind Swift – converting a slice back to an array creates a new backing store filled with the subscripted data. Let’s try something else.

.2: Getting ArraySlice-specific

Here’s an overloaded function that takes a ArraySlice<Int> instead of an [Int]:

func concatenateNumbers(numbers: ArraySlice<Int>) -> String {
    let result = numbers.reduce("") { $0 + "\($1)" }
    return result
}

println(concatenateNumbers(jennysExchange))
> "867"

Okay, now we can let our slice stay a slice, and call concatenateNumbers properly. The only problem is that we’re repeating ourselves – having two versions of the same function is never a good idea. What was option 3?

.3: Generics are a cut above the rest

If we convert concatenateNumbers to a proper generic function, we’ll be able to call it with either the array or the slice. So how do we do that?

In our generic function, we won’t be able to use the reduce instance method, since it’s declared separately in Array and ArraySlice. There is, however, a global reduce function that accepts a parameter conforming to SequenceType. If we check the (sprawling) protocol hierarchy graph on SwiftDoc.org, we can confirm that ArraySlice and Array both conform, so that will be a suitable generic constraint. Here’s our new function:

func concatenateNumbers<S: SequenceType>(numbers: S) -> String {
    let result = reduce(numbers, "") { $0 + "\($1)" }
    return result
}

println(concatenateNumbers(jennysNumber))
> "8675309"
println(concatenateNumbers(jennysExchange))
> "867"

Perfect! No need to reallocate, no need to repeat ourselves.

Performance

If you’re doing a lot of range-based subscripting of arrays, all that allocation can really add up. I ran a test summing random subranges of an array of integers, and found that using a generic function was more than six times faster than converting the subrange to an array each time. Not too shabby!