Pitch: Weak/Unowned Structs/Enums/Tuples/Closures/Any

Hello, everyone!

Today I’d like to pitch a proposal that could radically improve the modeling power of Swift.
Any and all feedback is very welcome!

Rationale

The ergonomics of captured references has been brought up numerous times, so I’d like to begin a discussion about a more universal solution to this entire domain of problems with a pitch to allow delegating of of reference capture semantics to outside the scope where it is captured.

If implemented, this feature will, among many other use cases, solve the reoccurring problem of closure capture syntax, as well as improve the widely used delegate pattern, decoupling it from class semantics.

Details

Structures

A weak structure is a property marked with the keyword weak of type T? (where T is a declared as a struct):

struct MyStruct {
    let myClass: MyClass
    let myClosure: () -> Void
}

weak var myStruct: MyStruct?

Such a property actually holds a hidden compiler-generated structure, where all its fields are declared as weak:

struct __Weak_MyStruct {
    weak var myClass: MyClass?
    weak var myClosure: (() -> Void)?

    mutating func unwrapping<Result>(into body: (inout MyStruct) throws -> Result) rethrows -> Result? {
        guard let myClass = self.myClass, let myClosure = self.myClosure else {
            return nil
        }
        var myStruct unsafeBitCast((myClass, myClosure), to: MyStruct.self)
        defer {
            self = .init(myClass: myStruct.myClass, myClosure: myStruct.myClosure)
        }
        return try body($myStruct)
    }
}

var myStruct = __Weak_MyStruct()

When accessing the property, all properties of the hidden structure are unwrapped. If all of them unwrapped successfully, then they’re arranged into the original structure and returned as such, otherwise, `nil is returned:

myStruct?.myClosure()
// translates to
myStruct.unwrapping { $0.myClosure() }

Enums

Since an enum case is essentially a structure containing an opaque discriminator and a memory region used for storing a tuple (where cases without associated objects correspond to ()), the rules of weak/unowned enums are identical to the rules of weak/unowned structures (see above).

Tuples

Since tuples are essentially anonymous structures, the rules of weak/unowned tuples are identical to the rules of weak/unowned structures (see above).

Closures

Since closures are essentially structures with stored properties holding captured variables and a single method implementing the closure body, the rules of weak/unowned closures are identical to the rules of weak/unowned structures (see above).

Any

Since an existential container is essentially a structure with an opaque payload, metadata and a witness table, the aforementioned unwrapping(into:) method (or a type-erased version thereof) can be included into all witness tables).

Would this allow us to write this? (today this is a compile error)

protocol MyViewDelegate {
...
}

class MyView: UIView {
  weak var delegate: MyViewDelegate // Error: 'weak' must not be applied to non-class-bound 'MyViewDelegate'; consider adding a protocol conformance that has a class bound
}

I would really like to solve this problem. It might be worthwhile to include an example like the above in the pitch/proposal.

Yes, absolutely! That’s the point! The delegate’s existential will wrap/unwrap the underlying non-class if necessary.

I'm not sure I like it as my gut feeling tells me there will be too many surprises with this.

What happens if you struct has optional values already? Your wrapper type would do double optional and when unwrapping it will use the non-optional type during unsafe bit cast to the struct that expects an optional. Wouldn't this already crash?

I don’t see how this can lead to a crash. The optional is either already nil (which will be propagated without change, or it’s non-nil, which will be unwrapped recursively.

The overall theme here is “for any instance, all reference types in the storage hierarchy are made weak and the entire storage hierarchy collapses to nil If any of those references is expired. This also means, that you can declare a weak closure and capture self inside it, which will cause self to be captured weakly for this reference to the closure.

Ah, sorry I think my brain faded here a little. No issue with optionals here.

How would you handle private stored properties for public types across module boundary?

Note that this will not do what you want with collections, and in general it exposes a lot of the implementation details of a type.

Also the closure thing really does not just fall out the way you seem to think it does.

3 Likes

Perhaps a more natural and encapsulation-preserving variant would be to autobox the whole struct in a class type, e.g. like this:

class ReferenceBox<T> {
  var value: T
  // insert appropriate init method here
}

…and then:

weak var foo: StructType?

foo?.bar

…is sugar for:

weak var foo: ReferenceBox<StructType>?

foo?.value.bar

I don’t have a good ending to this story about who precisely holds the strong reference to the ReferenceBox … but it feels like any workable proposal has to reduce to this in some form.

To elaborate: A weak Array, Dictionary, Set, or String would not end up weakly referencing its elements. Instead, it would weakly reference its copy-on-write backing storage object. That means the storage behind the collection would only last as long as there was at least one other instance using the exact same object for its storage.

(I think it might actually be worse than that because isKnownUniquelyReferenced(_:) ignores non-strong references. If there was only one strong reference left, each remaining instance with a reference would think it had unique ownership of the object and could mutate it; the other instances wouldn't know about the mutation and their other properties would be out of sync with the storage, which could cause them to break memory safety. But that would depend on the collection's implementation, which is of course the point.)

In any case, this badly breaks the encapsulation of the type you're weakening, exposing implementation details it may not want to share. I don't think it's a good idea.

5 Likes

The collection backing store is indeed a problem, because, in the proposed model, it will always be nil when the collection’s copy-on-write method executes. The closures I know very little about in the context of implementation, so details would be very appreciated!

On another note, I seem to have mixed up two distinct concepts, both of which I’ve called “weak”. The first one is a storage qualifier for a reference type and the other is an annotation that says hold this reference of mine and if it happens to be expired, consider me expired as well. For the sake of disambiguation, I’ll call the latter “cascading weak reference”.

What if the author of the type had the ability to manually mark appropriate references as cascading weak? Things like backing stores for arrays would definitely not be marked cascading weak, but, for example, generic wrapper structures would, allowing one to use the wrapper structure for its purpose without forcing the use of a strong reference.

Another very useful feature would be to have weak arrays (arrays that store its elements weakly). Since there is no guarantee as to how the backing store is implemented (it might as well be an opaque pointer with no inherent type safety guarantees), there will be nothing to annotate as cascading weak, since the elements are “extracted” on demand and not stored directly.

One radically different approach to this problem would be to treat weak and unowned on type-system level. If we imagine instead of

weak var myClass: MyClass?

We’d have:

var myClass: Weak<MyClass>

This would lift the restriction of the “holder” being exclusively responsible for choosing the reference semantics and allow (especially in generic contexts) the supplier of the value to be responsible (as, in my opinion, should be the case for delegates). This would also enable abstracting away the weakness of a reference in generic contexts and existentials.

Ideally, i’d like to think of a general direction in which swift could move to solve this domain of reoccurring issues.

This is merely a pitch and my limited knowledge if the inner workings of the compiler limit my ability to accurately assess the viability of solutions. I’d love to hear any and all opinions and propositions regarding this topic.

I think a compiler-generated wrap/unwrap hidden method would abstract away packing and unpacking the weak wrapper. However, as @John_McCall and @beccadax mentioned, this approach has serious problems when it comes to copy-on-write types (and similar types with special private reference members).

Maybe if we introduce a new semi-magical standard library protocol (I'll call it CustomWeakReferenceable for now), and have the conforming types provide the wrap/unwrap methods manually, this would give them control over which references to keep strong (e.g. Array's backing store) and which ones to make weak (e.g. Optional's case .some(Wrapped)). As with Codable, the compiler could also generate a default implementation that wraps and unwrapps all members.

@John_McCall @beccadax Please, take a look at my attempt at moving the weak to the type system. I was trying to solve the weak delegate problem, which forced me to make protocols class-bound even when it wasn't the right thing to do and deal with the fact that the compiler won't help me (or the author of the client code) avoid the problem if implementing the delegate property non-weak by misitake (since protocol requirements cannot be made weak). The wrapper does something very different from what I was pitching initially, but it was good enough at a time: It wraps a single value. If the value is a class instance (even if its static type is a non-class-bound protocol), then it stores a weak reference to it, otherwise it stores a copy of it. This allows me to not care about the implementation details of the delegate:

protocol MyType {
    associatedtype Delegate: MyTypeDelegate

    var delegate: Delegate { get set }
}

protocol MyTypeDelegate {
    func doSomething()
}

extension Weak: MyTypeDelegate where Unexpired == MyTypeDelegate  {
    func doSomething() {
        guard let unexpired = self.unexpired else {
            return
        }
        unexpired.doSomething()
    }
}

class MyTypeImpl: MyType {
    typealias Delegate: Weak<MyTypeDelegate>
    var delegate = Delegate()
}

struct MyStructDelegate: MyTypeDelegate {
    func doSomething() { }
}

class MyClassDelegate: MyTypeDelegate {
    func doSomething() { }
}

let myType = MyTypeImpl()
myType.delegate.unexpired = MyStructDelegate() // OK: Holds a copy of the structure instance.
let myTypeDelegate = MyClassDelegate()
myType.delegate.unexpired = myTypeDelegate // OK: Holds a weak reference to the class instance.

I'll try to rewrite this wrapper to make use fo a CustomWeakReferenceable protocol to allow the wrapped types to wrap/unwrap themselves in a custom way.

Unfortunately, this doesn't solve the problem postulated in the pitch. It only allows promoting a value type to a reference type and holding a weak reference to the entire instance (which is now a reference type). The goal was to make all relevant references inside the value type weak and have the value type's existence depend on them.