Temporary-Only Wrapper Types

There is a number of use cases where a type is necessary to encapsulate access to a specific part of a type without creating complex object graphs. The most representative example is the String type with its var unicodeScalars: UnicodeScalarView { get }, var utf16: UTF16View { get }, and var utf8: UTF8View { get } properties. Currently, these work because they don't allow mutating the string, so they can simply hold a shallow copy of the string internally and provide read-only access to the respective functionality. Even if these views are saved and the original string isn't, they still work because they hold a shallow copy of the string. However, trying to implement the same pattern with mutating methods becomes problematic.

Problem


struct Vector {
    var coordinates: [Double]

    var rgba: RgbaView {
        mutating get { Rgba(vector: &self) }
    }

    struct RgbaView {
        let vector: UnsafeMutablePointer<Vector>
        var r: Double {
            get { vector.pointee.coordinates[0] }
            set { vector.pointee.coordinates[0] = newValue }
        }
        var g: Double {
            get { vector.pointee.coordinates[1] }
            set { vector.pointee.coordinates[1] = newValue }
        }
        var b: Double {
            get { vector.pointee.coordinates[2] }
            set { vector.pointee.coordinates[2] = newValue }
        }
        var a: Double {
            get { vector.pointee.coordinates[3] }
            set { vector.pointee.coordinates[3] = newValue }
        } 
    }
}

Here are the problems with this implementation:

  • The struct Vector.RgbaView may be stored in a property, which can outlive the original Vector value, causing a dangling pointer (crash).
  • The accessor Vector.rgba has to have a mutating getter, meaning that the nonmutating members of Vector.RgbaView cannot be accessed through a read-only instance of Vector.
  • Implementing Vector.RgbaView is clunky and dangerous, because it involves working with unsafe pointers.

Solution

My idea is to add a new attribute (e.g. @temporary) for struct and enum declarations that would impose the following limitations and liberties on the declared type:

  • Values of the @temporary type may not be assigned to a property or passed to a function call, but only returned.
  • The @temporary type may provide custom infix operator = implementations.
  • inout parameters in initializers may be initialized with immutable values. The @temporary type is mutable if and only if all of its inout parameters are initialized with a mutable value.
  • Properties of @temporary types have their self value inherit the mutability of the enclosing object.

Example

struct Vector {
    var coordinates: [Double]
    
    var rgba: RgbaView {  .init(vector: &self) }
    
    @temporary struct RgbaView {
        var vector: inout Vector

        var r: Double {
            get { vector.coordinates[0] }
            set { vector.coordinates[0] = newValue }
        }
        var g: Double {
            get { vector.coordinates[1] }
            set { vector.coordinates[1] = newValue }
        }
        var b: Double {
            get { vector.coordinates[2] }
            set { vector.coordinates[2] = newValue }
        }
        var a: Double {
            get { vector.coordinates[3] }
            set { vector.coordinates[3] = newValue }
        } 
    }
}

I'd like to hear some opinions about this idea. Has this been proposed before? What fundamental problems this idea has?

Why would you do this? Instead, declare var vector: Vector, and give the rgba property a setter which does self = newValue.vector. Some of the view types in the standard library are mutable, and this is how they’re implemented.

Unfortunately I think you've hit an iceberg-tip.

This problem is the main advantage Rust has over Swift, and I feel like you should read up on the borrow checker to understand why this is a complex problem. (I had a hard time phrasing this sentence, just know I am NOT saying "we should just all go use Rust")

The main push to support this type of thing can be found in swift/OwnershipManifesto.md at main · apple/swift · GitHub

Have you benchmarked them in -O? I’m not sure the optimizer will notice the opportunities here, but it might.

Doesn't this cause copying of the entire value type every time a tiny change is made? Or is this guaranteed to be optimized away?

1 Like

That depends on how smart ARC is. There’s definitely an opportunity for an optimization; I’m just not sure that Swift takes it.

As @George says, this is almost exactly the place that Rust's (in)famous borrow checker is tackling, allowing it much greater flexibility than Swift for "first-class" in-place mutation.

One somewhat interesting point is that there's quite a bit of similarity to inout itself and closures and whether they escape: it's fine to pass inouts and (non-escaping) closures deeper down the stack, but not okay to do things that may allow them to be used in higher stack frames such as assigning to value properties or global variables. It would also be okay to pass "no-escape" @temporary values down the stack, in a similar manner, which is effectively what properties/methods on the type are doing. We could have a rule where a @temporary type behaves like a closure (non-escaping by default), but cannot be annotated with @escaping.

However, there's some extra dimensions here: as soon as you're able to pass around mutable inout references as first-class values, you have to start worrying about exclusivity: you can't duplicate one of those, because then one could be writing to both. In general, this then relies on move-only types. Fortunately, I suspect your rule of "you can only return @temporarys" avoids this problem for now.

1 Like

So far, it seems that this idea has merit, as far as its use case goes, but implementing it now would be a major hack, considering the ownership manifesto and the plans to improve Swift's object ownership model. When that happens some of the limitations that I mentioned could be lifted (e.g. @temporary objects could be stored in local variables and passed to function calls, but no escaped into global state or heap).