Problematic Swift Arrays

In the Apple developer forums, there is a lot of talk about how the semantics of arrays are messed up in the current beta builds of Xcode 6. Yes, they are messed up, but many are missing the actual problem and are trying to solve the symptoms.

Example issue with arrays:

let array = [5, 1, 0, 10]
array[1] = 0  // just changed a value in the "const" array, ok...
array += 5    // compile time error: good

var sortedArray = sort(array) // original array updated, oops!
sortedArray.append(4)

sortedArray   // prints: [0, 0, 5, 10, 4]
array         // prints: [0, 0, 5, 10]

Ok, we can hopefully see a series of errors here.

  1. Constant arrays can have their positional values changed, but not appended to 2. Arrays that were constant in one function scope are indeed not constant when passed to another function

So what's the real problem here? There are actually two problems that end up causing this issue:

  1. Arrays are structs and structs have value-type semantics. The entire metadata for the array, such as the startIndex, endIndex, and the array buffer pointer all get copied on assignment. This is ripe for bugs, as we have seen above. 2. Lack of true immutable types.

I'm going to assert that issue #2 is the true source of the problem. Let's look at the example above but change two assertions about the language:

  1. Classes have the same mutating attribute that can be applied to them as structs. 2. All parameters into functions are immutable unless the inout attribute is specified.

First, the basic definition of the array looks like this:

struct Array<T> {
  var startIndex : Int, endIndex : Int
  var buffer : ArrayBuffer<T>
}

It's important to note that the ArrayBuffer is essentially a class (technically it's a struct, but internally it uses a class to store the contents of the array).

When a copy of the array is made, all of the values of the array get copied; that is:

  1. The numerical value of startIndex 2. The numerical value of endIndex 3. The pointer to the array buffer, not the contents of the array

Let's take a look at a simple function to reverse an array:

func reverseArray<T>(array : T[]) -> T[] {
  var copy = array.copy()
  var left = 0, right = array.endIndex - 1
  while left < right {
    var swap = copy[left]
    copy[left] = copy[right]
    copy[right] = swap
    left++; right--
  }
  return copy
}

Now, this method works correctly with today's implementation Swift. It works because of the bolded line: it creates a deep-copy of the array and not just a copy of the pointer to the array contents.

If I wrote the function as such:

func reverseArray<T>(array : T[]) -> T[] {
  var copy = array
  var left = 0, right = array.endIndex - 1
  while left < right {
    var swap = copy[left]
    array[left] = copy[right]
    copy[right] = swap
    left++; right--
  }
  return copy
}

This method now suffers from the problem of updating the array that is being passed in (I also introduce another bug on line 6 to show the other failure case). This is the real problem!

Now, if the Swift language implemented the two items I mentioned above, the above function would not have compiled. Instead, the following would be true:

  1. Line 2 would have a compiler error for assigning an immutable type to a mutable type (remember, I claim that parameters should be immutable by default, only overridden by the inout attribute) 2. Line 6 would also have a compiler error because the subscript methods would have needed to be marked with mutating in order for assignment to take place.

The problem is not with arrays in Swift; the problem is in how Swift does not have a mechanism to stop classes from mutating its members.

I'll leave you with this one last example, put it in your playground and you'll see immediately that arrays are just a specific example of this limitation:

class CPerson {
  var name : String
  init(name : String) { self.name = name }
}

struct SPerson {
  var age : Int
  var person : CPerson
  mutating func haveBirthday() {
    println("Happy birthday!")
    age++
  }
}
let david = SPerson(age: 32, person: CPerson(name: "David"))
var frank = SPerson(age: 35, person: CPerson(name: "Frank"))

//david.age = 23            // compiler error, as it should
david.person.name = "Sally" // this SHOULD be a compiler error,
                            // but classes do not support
                            // 'mutating' or const in general
david.person.name           // prints "Sally"

frank.age = 23
frank.person.name = "David"
frank.person.name // prints "David"
Problematic Swift Arrays