Should weak be a type?

Spawning a new thread to answer this:

I don't see any of the above as a problem when that's what you want, and you can already accomplish this with a trivial wrapper struct. But what the current design of weak accomplishes, as a declaration modifier, is what I believe most people want most of the time with with a weak reference: store it weak in a given place, but convert it to a strong reference any time you want to use it.

I actually think the analogy to IUO is very sound. weak as we have now is both implicitly wrapping into a weak reference and unwrapping into a strong one as you assign and read to the variable. This wrapping/unwrapping is happening implicitly when accessing the variable, and we don't want this implicit behavior to propagate everywhere.

If you wanted to transform weak to be a type, I'd insist this type should work as a property wrapper. That'd allow it to work as a declaration modifier (what I believe most people want), but also as a standalone type you can put in containers (with no implicit conversion to strong). That change sounds a bit pointless given we can already accomplish both parts easily, but it might make the language a bit more elegant.

(This reasoning also applies to unowned and unowned(unsafe).)

3 Likes

IMHO, if you want weak to be a type, it should mirror Optional, and simply be a Weak<T> type. No need to have special syntax for this or to change to compiler to support it.

10 Likes

It'd also look more like WeakOptional and can be matched with if let.

1 Like

I've encountered a problem with this implicit unwrapping, when trying to conform my custom WeakRef<T> to Hashable. I want them to be compared based on object identity. And since object destruction does not mutate weak references, this identity should be preserved after object destruction.

From the first glance, side tables ideally suit for that. They have their own identity which maps 1:1 to the identity of the object, but they can outlive the object.

But currently, due to implicit unwrapping, when making a copy of the weak reference, it is first read as a strong one, and then a new weak reference is made out of strong one. But if object is already destructed, reading a strong reference returns nil and now identity of the side table is lost.

This results in counter-intuitive behaviour, which may break some algorithms:

let a: WeakRef<Foo> = Foo()
// instance of Foo dies here
let ha = a.hashValue // Based on address of side table
let b = a
let hb = b.hashValue // Based on nil
a == b // true
ha == hb // false

For my implementation, I've solved this by using a custom associated object instead of side table. But such solution is limited to Apple platforms. A pure Swift workaround would be storing both weak and unowned references to prevent reuse of the object identity.

To highlight the fact that conversion from weak T to T? is lossy, if weak T would be type, I think there should be an implicit conversion from T to weak T and an explicit conversion from weak T to T?. But no conversion from T? to weak T.

2 Likes

I think a Weak<T> type in the standard library would be very useful.

This is already a really common pattern throughout the community. For example, searching for struct Weak<T> on GitHub shows hundreds of similar examples. This is pretty similar to the status of Result before being added to the standard library.

I think a Weak<T> type is preferable over a @Weak property wrapper since the type can be used in many more places (e.g. collections ([Weak<T>], [String: Weak<T>]), which are the number one use-case for a Weak type in my experience).

6 Likes

Property wrapper is just a normal type (class/struct), so they can be both!

6 Likes

I'm not sure I understand what we'd gain from adding an @Weak property wrapper when we already have a weak keyword.

Why would one prefer @Weak var delegate: MyDelegate? over the existing spelling (weak var delegate: MyDelegate?)?

1 Like

Honestly... :woman_shrugging:. Some also want to retire the keyword-based variable modifiers, like lazy, weak, etc., so that's at least an option.

6 Likes

While I see how @Lazy could work (once property wrapper will support self reference), how do you plan to implement @Weak without weak ?

1 Like

I'm sure it'd need to be equally magical, and would need support from compiler.


One interesting thing about Weak is that we can use things like extension. OTOH, whether Weak should conform to protocols (which it definitely should be able to) is somewhat questionable.

I don't think having a stable hash makes that much sense for a weak reference. It'd make sense for a weak pointer. A pointer's equality is defined by the address it contains, whereas object reference equality is defined by the referenced object's content. (And what applies to equality also applies to the hash.)

I guess if we were to standardize a Weak struct in some form, it should look like a pointer and have a pointee property. Even then I'm not sure having a valid stable hash is so much in demand that you'd want to add the overhead in a standard weak pointer type, but if it can be done without overhead then perhaps it'd make sense.

Your reasoning is precisely why I designed weak and unowned the way I did. There was a design document for that, although it significantly precedes open-source evolution — not surprising, since the feature was in 1.0. I don’t know why it’s not in docs in the repository. I’m a bit swamped right now, but I’ll try to remember to pull it up in a few weeks and see if we can release it for the community.

IIRC, I did also call for a Weak type that could be used as array elements and dictionary values, even though it has unfortunate propagation properties.

18 Likes

https://github.com/apple/swift/blob/master/docs/weak.rst

9 Likes

The Library Directions section at the end of this document seems particularly relevant to this discussion:

Library Directions

The library should definitely provide the following types:

  • UnownedReference<T>: a fragile value type with a single public unowned field of type T. There should be an implicit conversion from T so that obvious initializations and assignments succeed. However, there should not be an implicit conversion to T ; this would be dangerous because it could hide bugs introduced by the way that e.g. naive copies into locals will preserve the weakness of the reference.In keeping with our design for unowned , I think this type should actually be an alias to either SafeUnownedReference<T> or UnsafeUnownedReference<T> depending on the current component's build settings. The choice would be exported in binary modules, but for cleanliness we would also require public APIs to visibly commit to one choice or the other.

  • WeakReference<T>: a fragile value type with a single public weak field of type T. As above, there should be an implicit conversion from T but no implicit conversion to T (or even Optional<T>). There is, however, no need for safe and unsafe variants.

The library should consider providing the following types:

  • A simple, memory-sensitive weak cache.

  • Finalizer : a value type which is constructed with a referent and a () -> () function and which causes the function to be run (on a well-known dispatch queue?) when the object is finalized.

Thanks! I don’t know how I missed it there.

I think it’s pretty unlikely we would add those implicit conversions now; this document was from a time when they were a major language feature.

3 Likes

Would Set<Weak<T>> automatically remove deallocated objects?

No; weak-keyed sets and dictionaries really have to be their own dedicated types. And as I cover in that design document, there are several different reasons you might want something like a weak-keyed or weak-valued dictionary, and they lead to quite different designs.

I’ve always thought of reference being a synonym for the pointer (with the exception of the C++, which has both, and distinguishes between them). And I would expect equality of references to be defined in terms of object identity - two references being equal if they refer to the same object.

I really dislike separation between == and ===. I don’t think any class should implement the == operator. And those who do now should be made structs with some heap-allocated storage encapsulated inside. I’ve already ranted about this here - Equality of functions - #32 by Nickolas_Pohilets

Anyway, regardless of terminology, for my use case I really need a type that represents a non-nil reference-counted pointer to a side table. And I need that type to not be nillified on copy. @John_McCall, do you remember what was the reasoning for making weak references nullified on copy?

1 Like

Weak references are nullified when the object is destroyed. What do you want to happen when the object is destroyed?

John, could weak and unowned becomes standard library types when we have support for move-only, non-movable types, rule-of-5, and other fancy things as part of ownership phasing in?

1 Like