Allow implicit self in escaping closures when self is explicitly captured

+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

I'd reiterate that explicit self. does not indicate captured self. If one writes self. out of habit, they never see this warning. Making it an error adds to confusion.

+1

This is probably ok, but I think we should carefully evaluate the tradeoff in the case of value types that store references.

I agree.

Big +1 here. This hole leads to cycles in practice. It would be great to see it closed.

3 Likes

My question is how do we play "strong-weak-dance" with this new feature.

Specifically, if in a class, we write code like:

var x

func someMethod() {
  let block = { [weak self] in
    guard let self = self else { return } // <------- (1)
    print(x)     // <------- (2)
  }
}

My question is:
Is line (2) valid?
If it is, is self referenced weakly or strongly in line (2)?
If it is referenced weakly, how can compiler know that self should be captured strongly, after line (1) is executed and before line (2) is executed?
Does it mean the existence of line (1) will affect the semantics of line (2)?

Strong +1, but I think we should consider the [weak self] and [unowned self] cases.

class Foo {
    var x: Int

    func bar() {
        let block =  { [weak self] in
            let y = x // Int? ????
            baz(using: self) // allowed? will it error as self is weak?
            guard let self = self else { return } // Is it allowed?
            let y2 = x // Is it allowed?
        }
    }
}

func baz(using foo: Foo) { ... }

In the current state of the proposal/implementation, only a strong, unowned, or unowned(unsafe) capture of self will enable implicit self (EDIT: which is to say, none of the implicit usages of self in the examples of @CrystDragon or @GLanza would be valid under this proposal as it stands). Allowing more general self rebinding is something that could be interesting to pursue, but there are enough additional questions (e.g. if we are willing to look through the weak storage, what else are we willing to look through? Does it work with if let? What about let self = self!) that I think it makes sense to deal with this idea in a separate proposal. Would be interested to hear others’ input, though!

2 Likes

For those interested in this pitch, I have an implementation which addresses the first two of @dabrahams's suggestions above, but does not incorporate this one:

After my initial investigation work I hit a wall and have been unable to dedicate the time and effort necessary to adequately address this issue. The question I faced is summarized here for anyone that feels strongly about ensuring that this feature be included in the proposal, but absent some additional assistance I will likely have to move forward with the proposal excluding this aspect.

Explicit self. is not an adequate indicator of a captured reference. It is good that we are removing it from the compiler. In my view it is not worth the effort to introduce it again in other places. Leave it to linters.

1 Like

To be clear, nothing is being removed here. This is a strictly additive change that will only introduce an additional way to specify a capture of self. Writing self. explicitly will continue to capture self.

I am fully aware of that. From my perspective, this is a step in direction of removing self. as an indicator of captured reference completely, whatever the team view is today. I will fight the proposal introducing self. back on other elements.

See how this simple statement needs a side note not to be misunderstood.

1 Like