Sabotaging the _read and _modify accessors

as i’m heavily dependent on _modify across a lot of my code, i’ve been looking into just how robust of a tool this accessor (and _read) is for preserving algorithmic complexity.

just what are we allowed to do with _modify that won’t add a linear factor to an algorithm?

example 1: normal _modify use

public
struct Tables 
{
    var core:[Int]
    var colonies:[Int: [Int]]

    subscript(index:Int) -> [Int]
    {
        _read 
        {
            yield index == 0 ? self.core : self.colonies[index]!
        }
        _modify 
        {
            if index == 0 
            {
                yield &self.core 
            }
            else 
            {
                yield &self.colonies[index]!
            }
        }
    }

    public mutating 
    func insert(x:Int, y:Int)
    {
        self[x].append(y)
    }
}

example 2: _modify that wraps an inlinable get set

why are we allowed to do this?

public
struct Tables 
{
    var _core:[Int]
    var _colonies:[Int: [Int]]

    var core:[Int]
    {
        get 
        {
            self._core
        }
        set(value)
        {
            self._core = value
        }
    }
    var colonies:[Int: [Int]]
    {
        get 
        {
            self._colonies
        }
        set(value)
        {
            self._colonies = value
        }
    }

    subscript(index:Int) -> [Int]
    {
        _read 
        {
            yield index == 0 ? self.core : self.colonies[index]!
        }
        _modify 
        {
            if index == 0 
            {
                yield &self.core 
            }
            else 
            {
                yield &self.colonies[index]!
            }
        }
    }

    public mutating 
    func insert(x:Int, y:Int)
    {
        self[x].append(y)
    }
}

example 3: _modify that wraps a resilient get set

(godbolt for people who are good at reading assembly)

public
struct Tables 
{
    var _core:[Int]
    var _colonies:[Int: [Int]]

    @inline(never)
    var core:[Int]
    {
        get 
        {
            self._core
        }
        set(value)
        {
            self._core = value
        }
    }
    @inline(never)
    var colonies:[Int: [Int]]
    {
        get 
        {
            self._colonies
        }
        set(value)
        {
            self._colonies = value
        }
    }

    subscript(index:Int) -> [Int]
    {
        _read 
        {
            yield index == 0 ? self.core : self.colonies[index]!
        }
        _modify 
        {
            if index == 0 
            {
                yield &self.core 
            }
            else 
            {
                yield &self.colonies[index]!
            }
        }
    }

    public mutating 
    func insert(x:Int, y:Int)
    {
        self[x].append(y)
    }
}

_read and _modify provide a general mechanism that can be used to avoid copies. They are not and have never been a promise that the storage will produce a value with no copies, and in fact that would be very limiting precisely because you then wouldn't be able to use them to wrap anything except persistent storage. You'd end up with a Rust-like situation where only the exact legal access pattern is the one which perfectly avoids copies, and therefore it is impossible to make ABI-resilient changes to code if they would change ownership.

9 Likes