SE-0481: `weak let`

Hello, Swift community.

The review of SE-0481: weak let begins now and runs through May 13th, 2025.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email or Swift Forums DM. When contacting the review manager directly, please include "SE-0481" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available on the evolution website.

Thank you for contributing to Swift!

John McCall
Review Manager

24 Likes

I was totally in on the "it has to be var because it can change" angle, but I think the fact that you can have weak var inside a struct means that wasn't really the correct mental model. Add that to the struct workaround that the proposal mentions, and I'm convinced that this is needed not only to enable the "this is weak but I'm not going to change it explicitly" use case, but also to reinforce a more correct understanding of how weak works.

21 Likes

I reiterate my comment from the pitch thread that because a computed property that cannot formally mutate the struct (one that either doesn't have a set or has a nonmutating set) is declared with var, and because property wrappers that similarly cannot mutate the struct are also declared var, it is most consistent for weak to be declared with var instead of let. I do agree that there is value in declaring a weak property or property wrapper's backing storage to be immutable, but it should be done another way (if the complexity in added syntax is a worthwhile tradeoff). The existing precedent is that a computed property/property wrapper is always declared with var even if it cannot mutate the backing storage, and we indicate that the backing storage is immutable not through let, but through the computed property either not having a set or having a nonmutating set.

3 Likes

Whilst I'm sympathetic to the idea that "property-wrapper-esqe" is a better mental model, the implication of the idea that weak var declares a wrapper and a hidden let that points at the sidetable, is that we no longer have a way to spell "weak var that is truly mutable".

I guess you could deprecate language weak and provide @Weak and @MutableWeak macros in the stdlib that actually operate like a property wrapper:

// backed by `let _object: _SideTableRef<SomeClass>`; expands to { get }
@Weak var object: SomeClass?

// backed by `var _object: _SideTableRef<SomeClass>`; expands to { get set }
@MutableWeak var object: SomeClass? 

But honestly I think that's even harder to comprehend than the proposal as it stands — if that was the route, I'd suggest we allow macros to add accessors to let (similarly to how they can add accessors to initialized properties) so at least we can spell them as @Weak let and @Weak var, which leaves us exactly back to the proposal…

So I think the proposal as stands is fine — weak let means "can't be changed to point at another object" and weak var means "can be changed to point at another object", which is… if not perfectly consistent, at least understandable and teachable.

This is a feature that I need on a regular basis, and the existing workaround:

struct Weak<Wrapped: AnyObject> {
    weak var wrapped: Wrapped?
}
extension Weak: Sendable where Wrapped: Sendable {}

doesn't work for class-bound protocols:

protocol P: AnyObject {}
let p: Weak<P> // error: 'Weak' requires that 'any P' be a class type

So this does need a language-level fix (though I would also I accept allowing any P to be a class type when P: AnyObject).

+1 from me.

1 Like

Thanks for replying; I appreciate it. I wasn't intending to suggest that the meaning of weak var be changed, or that weak be replaced with a property wrapper. What I think is that because property wrappers and weak share the same issue, if we want the issue to be solved, we should come up with a syntax that works with both property wrappers and weak, and is consistent with the meaning of var/let implied by the existing computed property and property wrapper syntax. For example, the syntax could be:

  • nonmutating weak var x/nonmutating @State var x
  • weak(let) var x/@State(let) var x
  • let weak var x/let @State var x
  • weak var x: T { get }/weak var x: T { nonmutating }/@State var x { nonmutating }[1]

  1. With this one the idea is that after a property wrapper declaration, we can write braces to restrict the set of accessors we want from the original property wrapper declaration to apply to the synthesized computed property. { get } means "we only want the get from the original property wrapper declaration," and { nonmutating } means "only take the nonmutating accessors". ↩︎

1 Like

A Linux toolchain for testing this feature can be found here.

A macOS toolchain can be found here.

1 Like

This makes sense to me as it suggests the simple mental model that changing something and destroying it are two separate things, since one is about the value, and the other is about the availability. I feel the "destroying is mutating" idea to be a misleading technicality, equivalent to going to get the mail, discovering the mailbox is itself gone, and reporting back that there was no mail.

+1

1 Like

I don't know how much optimizations can the Swift compiler do around let, but wouldn't this potentially break some optimizations?


Also:

The existence of this simple workaround [struct wrapper] is itself an argument that the prohibition of weak let is not enforcing some fundamentally important rule.

This is not a very good argument because one could use it to eliminate let altogether: let doesn't make sense because you can always circumvent it by wrapping any variable in a struct.

But obviously let does make sense in terms of sendability but also more broadly as a message to yourself and other developers that you don't intend mutate the thing, i.e. "look no further, this is the only value it will ever get".

I'm a bit divided over this proposal. It does simplify certain things like the proposal lays out, but also fundamentally breaks the concept of immutability.

Wearing my compiler implementer hat: there are no optimization problems with this proposal. You are correct that there are a lot of optimizations the compiler can do with memory that's known to be immutable. However, all the type-generic optimizations of that sort are still valid for weak references, because the compiler internally models weak properties as just being ordinary properties of a builtin weak-reference type. When you copy a struct, it's a value of that weak-reference type that gets copied, and (as previously discussed) there's no semantic problem with thinking of a weak reference value as being immutably bound to its current referent. The promotion of a weak reference to a strong reference is a special operation that the compiler simply doesn't model the same way that it would model an ordinary load.

12 Likes
  • What is your evaluation of the proposal?

    • I think that this is a sensible solution.
  • Is the problem being addressed significant enough to warrant a change to Swift?

    • Yes. This is a significant problem.
  • Does this proposal fit well with the feel and direction of Swift?

    • Yes. As others have pointed out, this is a sensible refinement to the mental model for weak references. It is a useful backward-compatible addition to the language.
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

    • I am unaware of anything similar in other languages.
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

    • A reading.

This may simplify the solution to another significant problem: There is no way to create sendable graphs with cycles in Swift. Such graphs could represent data models or complex configurations for example. I have this issue with my own project.

FYI, I tried compiling the "workaround" in a playground and got this warning:

Stored property 'ref' of 'Sendable'-conforming class 'WeakStructUser' has non-sendable type 'WeakRef'; this is an error in the Swift 6 language mode

1 Like

+1, I’m very excited to have weak lets in my classes and structs! It’s always bothered me when I really wanted an immutable let that happened to be weak but it had to be a var that someone might accidentally mutate in the future.

1 Like

Similarly, closures with explicit weak captures cannot be @Sendable , because such captures are implicitly made mutable, and @Sendable closures cannot capture mutable variables. This is surprising to most programmers, because every other kind of explicit capture is immutable. It is extremely rare for Swift code to directly mutate a weak capture.

func makeClosure() -> @Sendable () -> Void {
    let c = C()
    return { [weak c] in
        c?.foo() // error: reference to captured var 'c' in concurrently-executing code

        c = nil // allowed, but surprising and very rare
    }
}

But, the example code above in the proposal compiles in Swift 6 without any errors!

1 Like

It’s a bug - see Let's debug: missing RBI data race diagnostics

3 Likes
  • What is your evaluation of the proposal?

+1.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes. It will be a big help in adopting strict concurrency.

  • Does this proposal fit well with the feel and direction of Swift?

Yes.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I haven’t seen it in any other languages.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I read the proposal and the pitch thread.

  • What is your evaluation of the proposal?
    strong +1
  • Is the problem being addressed significant enough to warrant a change to Swift?
    In my opinion, it is.
  • Does this proposal fit well with the feel and direction of Swift?
    Yes, one could argue that it fits even better, especially for new learners.
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    I was part in the first thread that addressed the issue and the pitch thread. In fact, I was skeptical at first, but ultimately was convinced that this would be a good change.

Since it came up a few times over the various discussions: Yes, this pivots weak let a bit away from other areas where we use the var keyword for things that are not just mutable stored properties. In other words, one could say "Well, if you allow weak let, then why not also allow let for a computed property without a setter?"
I don't think this is a valid concern to forbid a weak let, but more an indication to perhaps change those other places, too (in separate proposals, of course!).
Ultimately the most basic thing about what keywords are used for what is the intended meaning. I have been asked by newcomers to Swift why we use a var for a computed property that only provides a getter several times. Same for protocols: a required property is always defined by var, even if it then only specifies only a { get }.
There may be reasons for each of these cases, but one thing that's not a valid reason is intuition. One of the first things people learn is "let for things I don't need to change, var for things I need to change" (now with weak let we should probably emphasize the "I" in there). A bit later, they learn the exceptions, but from an educational POV, are these really necessary?

2 Likes

A question about Swift's current behavior: Is it possible that this closure is called and the action() function is called between 1 and 3 times (instead of 0 or 4)?

let closure = { [weak obj] in 
    obj?.action()
    obj?.action()
    obj?.action()
    obj?.action()
}

If the reference can't be lost mid-invocation, closures could treat all captures as let values (similar to function parameters). It's not surprising that obj isn't the same value between multiple calls, since it's already prefaced with a keyword.

Yes, it is possible.

For synchronous closures this is usually undesirable, so it is a common practice to strongify variable once in the beginning of the closure. But for asynchronous closures this behavior is often useful - having a strong reference in the async call stack frame is equivalent to strong capture in a synchronous callback.

5 Likes

You're right, I forgot about async closures.

I'm currently only looking at the capture list part of the proposal. I've copied this segment from the proposal into a Swift 6.1 project, and it seems to compile just fine.

final class C: Sendable {
    func foo() { }
}

func makeClosure() -> @Sendable () -> Void {
    let c = C()
    return { [weak c] in
        c?.foo() // error reported in the proposal, but no error appears
        c = nil // also okay
    }
}

It’s a bug - see Let's debug: missing RBI data race diagnostics

2 Likes

+1, I believe this closes a big gap with concurrency and sendability, and would love to see other constructs like computed and lazy vars be addressed next!