Allow implicit self in escaping closures when self is explicitly captured

Introduction

Discussion thread: Review capture semantics of self
Bug: SR-10218

Modify the rule that all uses of self in escaping closures must be explicit by allowing for implicit uses of self as long as self is explicitly declared in the capture list for the closure. This would allow the following to compile without error:

class Test {
    var x = 0
    func execute(_ work: @escaping () -> Void) {
        work()
    }
    func method() {
        execute { [self] in
            x += 1
        }
    }
}

Motivation

In order to prevent users from inadvertently creating retain cycles, the Swift compiler today requires all uses of self in escaping closures to be explicit. Attempting to reference a member x of self without the self keyword gives the error:

error: reference to property 'x' in closure requires explicit 'self.' to make capture semantics explicit

In codebases that choose to omit self where possible, this can result in a lot of unwanted noise, if many properties of self are used in a row:

execute {
    let foo = self.doFirstThing()
    performWork(with: self.bar)
    self.doSecondThing(with: foo)
    self.cleanup()
}

Proposed solution

Allow the use of implicit self when it appears in the closure's capture list. The above code could then be written as:

execute { [self] in
    let foo = doFirstThing()
    performWork(with: bar)
    doSecondThing(with: foo)
    cleanup()
}

This change still forces code which captures self to be explicit about its intentions, but reduces the cost of that explicitness to a single declaration.

EDIT: With this change explicit capture of self would be one of two ways to get rid of the error, with the current method of adding self. to each property/method access (without adding self to the capture list) remaining as a viable option. This change makes previously invalid code valid, and is entirely source compatible.

The compiler would also offer an additional fix-it when implicit self is used:

execute { // <- Fix it: insert '[self] in ' to capture `self` explicitly
    let foo = doFirstThing()
    performWork(with: bar)
    doSecondThing(with: foo)
    cleanup()
}

This will require different versions depending on whether there is a capture list already present ("insert 'self, '"), whether there are explicit parameters ("insert '[self]'"), and whether the user the user has already captured a variable with the name self (in which case the fix-it would not be offered). If anyone can think of other cases where the fix-it would need to be tweaked, please note that in the discussion below!

This new rule would only apply when self is captured directly, and with the name self. This includes captures of the form [self = self] but would still not permit implicit self if the capture were [y = self] or if you did something like:

func method() {
    let y = self
    execute { [self = y]
        x += 1 // error
    }
}

Alternatives considered

Always require self in the capture list

The rule requiring the use of explicit self is helpful when the code base does not already require self to be used on all instance accesses. However, many code bases have linters or style guides that require self to be used explicitly always, making the capture semantics opaque. Always requiring self to be captured in the capture list explicitly would ensure that there are no self captures that the programmer is unaware of, even if they naturally use self for instance accesses. This would be a more drastic, source breaking change (and is not ruled out by adopting this change), so it was not seriously pursued as part of this proposal.

Eliminate the former fix-it

A less extreme solution to the problem described above is to simply stop offering the current fix-it that suggests adding the explicit self. at the point of reference in favor of only recommending the explicit capture list fix-it, when possible.

28 Likes

I like this a lot -- it looks like an elegant solution to me. The bigger issue imo is getting a similar level of convenience when capturing self weakly, but this would still be a welcome change on its own.

+1

Although I view having more than a couple selfs in a closure as being a code-smell, (as at that point it should probably be it's own function), the reason I'm aware of for constantly requiring self in a closure is to make it clear it's being retained. If anything, self being explicitly captured makes this more clear IMO since it at least hints at some more complicated reason for the requirement.

I'm also intrigued by the idea of always requiring it to be captured. Unfortunately that's probably an unwanted complication for beginners, and would be a breaking change so wouldn't happen at this point without a pretty good reason.

1 Like

I just had a quick glance at the pitch, I‘ll read it later in detail. I feels like you want 'self rebinding' here. This was previously discussed but went nowhere, it might be a good idea to dig up this and concatinate it with your ideas so that we don‘t have to completely rehash the topic.

-1, too much magic and too many exceptions. Just treat self like every other variable.

1 Like

From what I can tell from a quick search on that term, the discussions around "self rebinding" are usually about how to "re-strengthen" a weakly captured self in order to recover implicit self (EDIT: and I don't believe that anything in this proposal would interfere with a change like this in the future). This is a smaller change that enables implicit self in strong capture situations by moving the explicit self to the capture list. Let me know if I'm overlooking something in the history here.

Could you elaborate on what you mean by "like every other variable"? Being unable to use implicit self in closures is already an exception so I'm not really seeing how this introduces more exceptions. This actually feels less "magic" to me and more natural—the rule for implicit self is now "implicit self can be used everywhere the self parameter is (strongly) in scope" and there is a separate rule (EDIT) "if self is captured it must be spelled explicitly somewhere".

1 Like

Strong +1.

It appears from the comments on SR-10218 that this might actually happen without a full review. (Which I support; this seems like a really obvious and not-breaking-anything improvement. It's really more of a bug fix.)

1 Like

The pitch isn't 100% clear to me, but I think you're suggesting that the existing rule allowing explicit references to self would be preserved, but you would also be able to explicitly capture self and could then use implicit references. If that's what you're proposing, I'm in favor.

(If you're proposing abolishing the old check and always requiring a capture list, I'd have to oppose it—source compatibility is important.)

1 Like

Yes, that's exactly what I'm proposing. I tried to make clear in the "Alternatives considered" section that the explicit self in the capture list would not be made mandatory as part of this proposal—sorry that that didn't come through. This proposal makes previously invalid code valid (and potentially changes the suggested fix-its for already invalid code) and is entirely source compatible. I'll edit the original post to make that more apparent.

7 Likes

+1 This is a great pitch that would make escaping, self-capturing closures much more readable.

2 Likes

and there is a separate rule "any capture of self must be explicit".

If that was the case, then it wouldn't be backwards compatible?

By "like every other variable", I mean that in what I would consider ideal, this:

extension T {
    func x(y: Int) {
        print(self, y)
    }
}

Would behave the same as something like this:

func T_x(self: T, y: Int) {
    print(self, y)
}

Meaning self isn't even a keyword, just an implicitly named parameter, like newValue in a variable setter. Now of course, we're already quite far away from this ideal world of simplicity, but I see no reason to go even further in that direction.

Ah, yeah, that was poor phrasing. I've edited to reflect what I was trying to say better.

Is this to say you wouldn't want implicit self to be allowed anywhere a la SE-0009? I'm sympathetic to that mindset but the ship has surely sailed, and I don't think it's a very good argument against allowing the nearly universal implicit self in the one place that it isn't allowed (provided the capture intent is still made explicit).

I was just having a conversation about this a couple days ago, in which I swore that this was how it already worked. I'd love to have the ability to cut down on the self. noise inside closures like this.

1 Like

Yes

I agree :(

It introduces a lot of additional things to keep in mind, especially when reading code. Currently,

{ [x] in x.y() }

Is equivalent with

{ x.y() }

Because strongly capturing something that is actually used in the closure is the default.

But suddenly, strongly capturing something in a capture list that is used anyways is meaningful, just because that thing happens to be named self. Thats not very straightforward and it will confuse people.

Also, what about static functions? They already allow implicit self in closures, without that special capture! It just makes everything very awkward.

1 Like

It is not.

6 Likes

self is already treated differently from other captured variables for clarity around possible retain cycles; this proposal doesn't change that. I don't see this as adding new exceptions or special cases around self, but rather adding a convenience syntax for an existing special-case requirement.

With proper fix-its, it doesn't seem like this should be any harder to understand than the current requirement of explicit self. in escaping closures.

3 Likes

I'd argue that these examples (which all compile without warnings in today's Swift) are parallel and consistent:

{ [x] in x.y() }
{ x.y() }

{ [self] in self.z() }
{ self.z() }

There is a special case today to disallow the following when the closure is not guaranteed to be non-escaping:

{ z() }

Frederick's proposal allows this syntax to silence that diagnostic:

{ [self] z() }

So you can look at it as the capture list doing something new, or you can look at it as the diagnostic logic doing something new.

1 Like

You are of course correct, only if x is a constant. But that matters.

Well, I look at it as the diagnostic logic having a new special case concerning capture lists.

Resurrecting this thread, because it's a great idea. I'd like to expand on the pitch, because we need a comprehensive answer to the problem of “self.” clutter and the overall ineffectiveness of the requirement to write it. I propose the following:

  • Start with Frederick's proposal.

  • Eliminate the need to even write “[self] in” when self is known to be a non-class type. The only purpose of the “self.” rule is to expose unintended reference cycles, which are extremely rare when self is not a class type. One place (chosen at random of course!) where this would make a big difference: even seeing “[self] in” is onerous for many real SwiftUI examples and is worse than a single “self.”.

  • Close an existing hole in the reference cycle protection provided by this rule (pointed out by John Harper), by adding the rule that an escaping bound method or property of a class requires “self.” qualification:

    class Y {
        func inc(x: Int) -> Int { return x + 1 }
    
        func escape1() -> (Int) -> Int { 
            return inc // error: escaping bound method 'inc' requires 'self.' to make capture semantics explicit.
        }
    
        func escape2() -> (Int) -> Int { 
            return self.inc // OK
        }
    }
    

What do you think?

23 Likes

I think it is a great step forward. Make it optional where it does not make sense, require it where it helps readability :).

1 Like