Array copying and computed properties

This came up in a Stack Overflow question recently.

I have a struct with a 2D array in it and one of the arrays is called the "active array" and this is denoted by a variable holding the active array index.

struct Foo
{
    var activeArrayIndex: Int = 0
    var allArrays: [[Bar]] = [[...]] 
}

I can access elements of the active array with myFoo.allArrays[myFoo.activeArrayIndex][i]

If I have to write that a lot, I can write getter and setter functions e.g. myFoo.setActiveArray(index: i, myBar) but it seems a little ugly, so what if I naively create a computed property:

    var activeArray: [Bar]
    {
       get { allArrays[activeArrayIndex] }
       set { allArrays[activeArrayIndex] = newValue }
    }

Now I can do things like write myFoo.activeArray[i] = myBar

As far as I can see, this would mean lots of array copying. Is that assumption correct or is there some magical optimisation that elides the array copying?

On the assumption that there is lots of copying, is there a canonical way to fix this? I thought of creating pseudo collection that proxies the subscript via closures that capture allArrays and activeArrayIndex or equivalent and even subscripting Foo directly which seems kind of wrong.

Alternatively consider a subscript like this:

extension Foo {
    subscript(_ i: Int) -> Bar {
        get { allArrays[activeArrayIndex][i] }
        set { allArrays[activeArrayIndex][i] = newValue }
    }
}

myFoo.allArrays[myFoo.activeArrayIndex][1]
myFoo[1] // same

Don't have an answer for this, but it does feel lots of copying.

Historically, traditional answer to 2D arrays or arrays of higher dimensions - make them 1D:

private var elements: [Element] = [...]
subscript(x: Int, y: Int) -> Element {
    get {
        precondition(y >= 0 && y < rowCount && x >= 0 && x < columnCount)
        return elements[y * columnCount + x]
    }
    set {
        precondition(y >= 0 && y < rowCount && x >= 0 && x < columnCount)
        elements[y * columnCount + x] = newValue
    }
}
3 Likes

The reason why I do't want to subscript Foo directly is because Foo itself is not conceptually a collection. That said, there's no reason why there couldn't be a collection that abstracts the functionality and has a subscript of one argument for the "active" array and a subscript of two arguments for the full 2D array and Foo has one of those collections in it. Might be a bit confusing though.

Historically, traditional answer to 2D arrays or arrays of higher dimensions

Works nicely if you can guarantee that each row is a fixed width.

Hey, can you pls explain where exactly the array is getting copied? I'm not being able to understand what you mean.

this is the correct pattern. all you need is a _modify instead of a set to prevent copy-mutate-assign from taking place.

3 Likes

That's perfect. Is there any significance to the fact that _modify starts with an underscore?

Yes, it's a non-public language feature, which may stop compiling or working at any time in a future version of the compiler.

Yet the need it addresses will remain. You'll always find it in a form or another. If you are sure you can swiftly react to new compiler versions, the risk is pretty low IMHO.

Here is the original code:

struct Bar { var x: Int  }
struct Foo
{
    var activeArrayIndex: Int = 0
    var allArrays: [[Bar]] = [[Bar(x: 0)]] 

    var activeArray: [Bar]
    {
       get { allArrays[activeArrayIndex] }
       set { allArrays[activeArrayIndex] = newValue }
    }
}

Consider the following:

var myFoo = Foo()
var myBar = Bar(x: 1)
myFoo.activeArray[0] = myBar

The last line is semantically equivalent to

var tmp = myFoo.activeArray
tmp[0] = myBar
myFoo.activeArray = tmp

When you do the assignment, copy-on-write causes a copy of the backing story of the entire tmp array to be made and this is then used to replace the array within Foo when all you wanted to do was change one element.

1 Like