Review capture semantics of self

Motivation

Explicit self on member access is required to resolve symbol shadowing.

Overloading self with capture semantics leads to confusion:

Inconsistent

class Style1 {
    init() {
        some = {
            bar()
        }
    }
    var some: Any?
    func bar() {}
}

Call to method 'bar' in closure requires explicit 'self.' to make capture semantics explicit

class Style2 {
    init() {
        some = bar
    }
    var some: Any?
    func bar() {}
}

(no warning) SR-8536

class Style3 {
    init() {
        func niente() {
            bar()
        }
        some = niente
    }
    var some: Any?
    func bar() {}
}

(no warning)

Confusing

Explicit self. doesn't indicate that self will be captured:

class Style5 {
    init() {
        some = self.bar
        some = self.nada
    }
    var some: Any?
    var nada: Int = 42
    func bar() {}
}
class Style6 {
    init() {
        foo(self.nada)
        bar(self.nada)
    }
    var some: Any?
    var nada: Int = 42
    func foo(_ cb: @autoclosure () -> Int) {}
    func bar(_ cb: @escaping @autoclosure () -> Int) { some = cb }
}

Problems

A team adopting explicit self on every member access is left with no indicators which self is captured.

This makes explicit self a conditional requirement – it requests you not to write self on all other code.

Learners of the language are confused: explicit self or not, it looks just a member access to me.

Not every example is currently covered in the compiler.

Can we do better?

Explicit capture should have an explicit syntax.

Closures

Closures have capture lists, and this is the natural place for the explicit self:

class Style1b {
    init() {
        some = { [self] in
            bar()
        }
    }
    var some: Any?
    func bar() {}
}

Writing bar() or self.bar() is optional, and is left to teams' preference.

Method reference

One way to resolve this with existing syntax is to require wrapping method access in a closure, see above.

Otherwise, discuss an explicit syntax, like:

class Style2b {
    init() {
        some = [self] bar
    }
    var some: Any?
    func bar() {}
}

Nested function

Can compiler enforce the same escaping/non escaping rules as on closures?

Should we have an explicit capture for nested functions, or compiler should force you to use a closure?

One possible explicit syntax:

class Style3b {
    init() {
        func niente() [self] {
            bar()
        }
        some = niente
    }
    var some: Any?
    func bar() {}
}
class Style3c {
    init() {
        func niente() [self] {
            nada = 11
        }
        foo(niente)
        bar(niente)
    }
    var some: Any?
    var nada: Int = 42
    func foo(_ cb: () -> Void) {}
    func bar(_ cb: @escaping () -> Void) { some = cb }
}

Auto-closure

With existing syntax, compiler could require wrapping member access in a closure, see above.

Otherwise, discuss an explicit syntax, like:

class Style6b {
    init() {
        foo(nada)
        bar([self] nada)
    }
    var some: Any?
    var nada: Int = 42
    func foo(_ cb: @autoclosure () -> Int) {}
    func bar(_ cb: @escaping @autoclosure () -> Int) { some = cb }
}

Explicit capture all

A follow-up discussion: should the language require explicit capture of all objects?

class Style1x {
    init() {
        let x = X()
        some = { [self, x] in
            bar()
            x.foo()
        }
    }
    var some: Any?
    func bar() {}
}

Discussions

7 Likes

I love this idea! This issue has always bothered me, because writing out explicit self. doesn’t actually add any clarity (due to the fact that any non-capture member can also be spelled with self. with no indication of the fact that the member is captured). I’m not sure explicitly capturing everything is a great idea, but, on the other hand, why should self be any different from other captured variables in this context? Perhaps, it would be better to separate the capture semantics of value types from that of reference types. If we allow implicitly capturing value types, it can’t lead to unintended reference cycles unless the value type in question holds poorly managed references, in which case it’s a bug in the value type itself and has nothing to do with the capture semantics. For all reference types, however, it might be a good idea to require explicit capture syntax.
For example, this:

func foo(a: Int, b: NSNumber) -> () -> () {
    return {
        print(a) // okay: `a` is captured implicitly due to it being a value type.
        print(b) // error: reference types cannot be captured implicitly.
    }
}

Will have to be rewritten as this:

func foo(a: Int, b: NSNumber) -> () -> () {
    return { [number] in
        print(a) // okay: `a` is captured implicitly due to it being a value type.
        print(b) // okay: `b` is captured explicitly.
    }
}

So all instances of classes, class-bound protocols and closures will have to be captured explicitly (including self) and all instances of structs and enums will be captured implicitly (including self).

What do you think?

Probably this ship has already sailed, and there won't be a nice solution for the itching problem of accidental captures anymore :frowning:
I think it might have been a good idea if all captures were weak by default (self?. wouldn't be that much more typing), but I guess there have been compelling reasons for the current design.

Could you please explain why?

I personally would overthrow compatibility without hesitation ;-) - but nowadays, it's very unlikely that any breaking change would be accepted.
Afaics, at least the core part of the proposal is purely additive, so a special case for self in closures might be more realistic… but special cases have their own downsides, so unless the concept receives some support from influential members of the community, it will be very hard to get this into review (but imho that's actually true for everything ;-).

Would the proposed syntax of allowing [self] in a closure's capture list really be a special case? It fits with the existing capture list syntax and wouldn't require breaking changes. To me, it seems that the current requirement of using self.whatever within closures is more of a special case than this.

1 Like

I strongly wish this was the case already. I use explicit self in my projects, and so 99% of my memory leaks come from using self within closures without thinking about it and causing reference cycles :(

That being said, I don’t know if there would be a way to do this while preserving source compatibility (and this would certainly be a very big source breaking change), so :confused:

I would think that simply adding explicit [self] capture as an alternative to self.member without removing the current syntax would be purely additive. So, I think that part's doable without breaking anything.

Removing self.member as a way to explicitly capture self would be source breaking, but it could possibly be done with a long deprecation cycle.

We already have [self] as valid capture syntax, so the change you're interested in is saying that it should be required whenever you reference self inside an escaping closure (presumably via warning rather than error so as not to break source compatibility). I think that's a reasonable thing to propose (without having decided yet if I agree with you).

2 Likes

At least the outcome would be slightly different — but you could say that is just because self itself is somewhat special, and not because of the feature in question.

I don't think the outcome is any different. You already can't reassign self in a class instance function, so capturing by value and capturing the variable aren't distinguishable. And elsewhere, you can't capture self mutably either unless the closure is non-escaping.

Somewhat off-topic, but I wonder why not, given that we can conform a class to this protocol:

protocol P {}

extension P {
  mutating func setSelf(_ x: Self) { self = x }
}

What I meant is that [self] would allow you to write myMethod() where you have to write self.myMethod() now — and afaik, that is and would not be possible for arbitrary variables (for those, [foo] in wouldn't have any effect at all, would it?).
Basically, you could say the idea "restores" a capability that self has in other contexts.
But anyways, I don't want to argue against this pitch (although I'd still prefer capturing weak by default ;-)

4 Likes

I'd like this as well. It's just as intentional as requiring self.(...) is currently, and would allow a lot of clutter to be removed in closures where there are many accesses to self.

1 Like

I'm pretty sure a bug has been filed for this but I can't find it now. That's definitely a reasonable change that could happen independently of the warning policy. I suspect the core team would be okay with adding that capability ([self] disables the self. check) without going through a full review, but you'd have to get one of them to sign off on it.

4 Likes

I believe that reason is that a lot (most?) of the uses of closures are in eager/non-escaping situations (e.g. functional methods like map, etc.) and making the captures weak would not only negatively impact the syntax of these use cases, but would also have more serious negative consequences. e.g. remember that a local variable can be deallocated in a function as soon as there are no further “strong uses” of it, so a map at the end of a function would be likely to find all its captured local variables already deallocated.

1 Like

I have filed an issue here SR-10218 for the escaping closure case.

Would love to see this happen. It would remove a lot of friction from working with completion handlers, etc (especially when moving code in and out of closures).

1 Like

Posted a pitch over here: Allow implicit self in escaping closures when self is explicitly captured

2 Likes

I agree completely with the general observation of inconsistency, and also with the idea of making explicit [self] in closures silence the compiler's "requires explicit self to make capture semantics explicit" errors.

As far as the other cases mentioned in the OP, the cure seems worse than the disease. The various proposed syntaxes are novel and only marginally consistent with the rest of the language. And it's a lot of boilerplate to have to add to your code, for not much actual benefit. Require wrapping things in extra closures? That's crazy talk. :slight_smile:

I'm actually not all that bothered by the inconsistency here. I'd restate the problem as "There are multiple ways to inadvertently capture self for which the compiler emits no warning."

What about simply requiring explicit self outside the context of closures when self will in fact be captured? This would be consistent with the compiler's behavior for closures. For example:

class Example {
    init() {
        bar(nada) // Error: "...requires explicit self..."
        bar(self.nada)  // OK
    }
    var nada: Int = 42
    func bar(_ cb: @escaping @autoclosure () -> Int) { }
}