[pitch] Allow weak with Any

Swift uses ARC (automatic reference counting), an automatic memory management scheme in which cycles must be broken by the programmer. This is usually accomplished with unowned or more commonly with weak. These modifiers can be applied to closure captures or property storage.

Swift protocols are a powerful abstraction mechanism; generics and large chunks of the standard library are built on them. Swift also treats value types as first-class citizens, allowing enums and structs to conform to protocols. The choice of value type vs reference type can be made on the basis of the problem you are modeling, mutability, and sharing.

These two worlds collide when it comes to weak. It is not currently possible to declare a weak-any property, nor weakly capture a non-class type in a closure. This is illegal:

class Fizz {
    // error: 'weak' must not be applied to non-class-bound 'Any'
    weak var delegate: Any?
}

The same applies to protocols and generic types:

class Fuzz<T> {
    // error: 'weak' must not be applied to non-class-bound 'T'
    weak var fuzzyWuzzle: T?
}

This presents a problem for a library or API designer: How can I make it easy to avoid reference cycles for clients using a reference type without prohibiting the use of value types for all clients? It can also be problematic to use escaping closures in an entirely private implementation, which is an unwelcome leaky abstraction.

My pitch is very simple: Allow declaration of weak Any values. If the underlying value is a value type the weak declaration has no effect. If it is a reference type the capture is weak. This allows certain code patterns to be fully generic with respect to value vs reference types.

Swift must already deal with reference types held in an Any value, dynamically determining whether ARC operations are required. It is my hope that handling weak would not impose any significant additional costs but that may not be the case. In any event only properties or captures declared as weak Any would pay the price.

The generic case is more interesting. For internal implementations the compiler knows the specializations and could presumably discard the weak qualifier for value types at compile time. Across module boundaries or for non-specialized code I'm less sure of the fallout.

Are there fatal flaws I haven't considered? Does this seem like a reasonable thing to do? Have you ever run across cases where this would be useful in your own code? If so how did you work around it?

6 Likes

Any is not reference bound, the error is correct and this won't ever change.

The following does not work for you?

class Fizz {
    weak var delegate: AnyObject?
}

class Fuzz<T : AnyObject> {
  weak var fuzzyWuzzle: T?
}

Furthermore Any is the supertype of any possible type in Swift and Never will be the bottom type of any type at some point. If Any were reference bound then it would mean that every type would be reference bound. Trivial, no?

Yes, the error is correct in the current implementation. I'm pitching relaxing the requirements specifically for Any, protocol types, and generic types.

I'll quote my original message here:

Constraining weak references to AnyObject prohibits any clients of my API from using value types. As a good protocol-oriented engineer I use protocols where I can to be agnostic about the underlying implementation, but reference cycles can force me to make design decisions on behalf of my clients that I'd rather not make.

I don't think you understood my pitch or perhaps I did a bad job of explaining it.

Any can already contain reference types. It has nothing to do with "being reference bound"; there are no constraints on Any. Nevertheless if I stuff a reference value in an Any Swift dutifully determines that and performs the appropriate retain/release operations at runtime. If I put a value type in then no retain/releases are performed (or perhaps they are performed against reference types contained within the value type). Swift already abstracts over reference vs value types in several ways.

My pitch is that in certain limited cases we should be able to abstract over weak references as well.

I am certainly open to the possibility that this problem should be solved in a different way, for example the standard library could provide a WeakBox<Wrapped> with implicit conversion to Wrapped through compiler magic, allowing a client to substitute a weak reference to a reference type so long as the expected type is optional. That has other implications (such as TOCTOU failures in the library consuming such a reference).

I'm also open to the possibility that this problem can't be reasonably abstracted over and so a library or API author has no choice but to prohibit value types or ignore reference cycles.

1 Like

To see if I’m understanding you correctly, you want weak to act as a “copy” on value types as any standard reference would, but as a weak reference on reference types?

If that is the case, how should this work with value types that contain an internal reference to an object?

I would suspect if reference counting and back references are a risk for you, then this would be papering over the issue. It would probably be better to make it a weak and constrain the type to be a class. I realise this makes an API more constrained, but at least it would keep consistency from a memory management perspective...

I swear we've talked about this before but I can't find it. I just can't see this making sense. Most people don't actually want Any to be weak; they want a particular protocol they can use for their delegates. But then they don't want their value-type implementations to be held weakly; as you say, they expect those to be copied in the normal way.

Except…that's fine if the value type is stateless, or holds on to its own independent object graph. But there's no way to enforce that. And if you get it wrong, you've made a strong reference cycle through a property declared as weak.

(Another alternative would be to have a weakCopy function in the runtime metadata for all structs, just like there's a copy function. But I don't think people are actually designing their structs such that a piece of them could go to nil unexpectedly, and neither do I think it's a good idea to say the entire struct goes to nil when one property does, potentially recursively, potentially including non-public properties.)

Not all delegates are stateful or rely on their identity, and it's kind of annoying that we have to provide a distinct owner for those that are. But I don't think this is the solution.

3 Likes

Siesta had exactly this problem (with observers, not delegates, but nary a difference here), and solved it exactly as Jordan describes: every observer needs a distinct owner, and the owner must be an object even if the observer itself is not. Extensive description here.

This approach has played out nicely in practice, at least in my situation.

1 Like