SE-0269: Increase availability of implicit self in @escaping closures when reference cycles are unlikely to occur

This is true—removing the "explicit self" requirement for value types does introduce the possibility of having a reference cycle without any explicit spelling of self. However, I have only been able to come up with relatively pathological examples of how this might occur. If self is a value type, the simplest case of "self holds a reference to a closure that captures self" simply doesn't apply. Instead, you have to have an exterior reference which contains a value which somehow refers back to the exterior reference, then have the value implicitly capture self in an escaping closure, then have that closure make it's way back up to be retained by the exterior reference. It would be something like this:

protocol Config { ... }
class Foo: Config {
    var work: () -> Void
    func createSomeWork() {
        let creator = WorkCreator(config: self)
        self.work = creator.getWork()
    }
}

struct WorkCreator {
    var config: Config
    func getWork() -> () -> Void {
        return {
            print(config)
        }
    }
}

Under this proposal, the above code would be legal and result in a reference cycle. However, the resulting cycle would not, I believe, be made substantively more apparent if we had to write print(self.config). The crux of the issue in cases like this is that at the capture site, there is nothing obviously wrong with capturing self. Instead, reference cycles resulting from this sort of object graph depend on the precise relationship between the implementation of the value type and the actual eventual owner of the self that is captured, which will necessarily be divorced from the capture site. I'm not convinced that requiring self at the capture site really solves the issue in a reasonable way.

If I've missed a simpler way in which such a reference cycle might arise, please point it out!

3 Likes

I now want to state more strongly that explaining the nuance of the proposed behavior to newcomers seems like a real challenge. A challenge not obviously worth the gains.

3 Likes

I actually don't think that the example you give is so outlandish. I do agree, though, that the current solution of requiring explicit self.config does not make the reference cycle or even just the capturing of self very apparent.

I think the first part of your proposal, enabling the use of [self] in instead, would do something to move the needle in this regard. I think, then, that I agree with @anandabits that I would favor adopting the first half of the proposal alone.

Obviously, for backwards compatibility, we'd have to allow the existing syntax, but for better teachability, I would go so far as to suggest that the current fix-it for adding explicit self be removed in favor of the capture list. I think users will find it logical that, to capture self, one must use a capture list that includes self.

7 Likes

"Pathological" may have been a bit hyperbolic, and I agree that examples like the one I gave are simple enough that some version of them may (perhaps frequently) arise in practice. But I think even an explicit capture of self in the capture list in the above example would still do little to solve the problem. The programming error lies in the implementation of createSomeWork(), not in the implementation of WorkCreator.

If the example above were instead:

protocol Config { ... }
class Foo: Config {
    var work: () -> Void
    func createSomeWork() {
        self.work = WorkCreator.getWork(with: self)
    }
}

struct WorkCreator {
    static func getWork(with config: Config) -> () -> Void {
        return {
            print(config)
        }
    }
}

it would compile today without error. Comparing the two version of WorkCreator:

struct WorkCreator {
    var config: Config
    static func getWork() -> () -> Void {
        return {
            print(config)
        }
    }
}

struct WorkCreator {
    static func getWork(with config: Config) -> () -> Void {
        return {
            print(config)
        }
    }
}

I struggle to convince myself that there is a programming error at the capture site in the first version which would cause a programmer to introduce a reference cycle that could be avoided if we added [self] in or self., precisely because it depends entirely on the object graph set up by the client of WorkCreator.

Agreed, and I would likely support a follow-up proposal to that effect if this is accepted.

Maybe we could add that to future direction section.

1 Like

These examples may not make the most charitable case for requiring explicit self even for value types, actually. A better example would be something like:

struct WorkCreator {
    var config: Config
    var taskNumber: Int
    static func getWork() -> () -> Void {
        print(config)
        return {
            print(taskNumber) // all of 'self' is captured, including 'config'
        }
    }
}

This still relies on a further error by the client, but now even if they do scan the implementation of getWork to check what gets captured, it might not be immediately apparent that config is included.

This example caused me to consider a rule where the use of implicit self causes individual fields to be captured rather than the whole self:

class Foo {
    var x: Int

    func getClosure() -> () -> Void {
        return { // implicitly: [x = self.x] in
            print(x)
        }
    }
}

I haven't fully considered the feasibility of a rule like this, but it would (I believe) be fully source compatible with the first rule in this proposal. If the second rule is adopted as well, then this change would become potentially source breaking for anyone who relied on the "implicit use of self captures the whole self parameter" behavior.

It's covered under Alternatives considered for the moment, though maybe a callout in the section above would make sense.

1 Like

There's also methods capturing. It would still need to capture self.

1 Like

+1. Currently, explicit self serves the following two different purposes: (1) it enables us to access shadowed members, and (2) it denotes strong capture semantics. Personally, I would like (2) to be migrated to the new capture list syntax proposed here. Explicit self would then serve a single purpose that is straightforward to understand.

3 Likes

Good point. Methods could still be compatible with this rule in a relatively subtle way, where the use of method in a closure body implicitly captures [method = self.method], thereby capturing the whole self, but there are further issues with setters and property observers that may make such a rule unattractive... in any case, I don't want to drag the review thread too off topic. If the Core Team deems the value type rule as unacceptable, further discussion could be had at that point.

@griotspeak is absolutely correct.

  • What is your evaluation of the proposal?
    -1
  • Is the problem being addressed significant enough to warrant a change to Swift?
    No
  • Does this proposal fit well with the feel and direction of Swift?
    Somewhat

How this is any more complex than the current behavior? Randomly needing to use self when it's not otherwise required is already confusing with a property explanation requiring an understanding of @escaping, Reference v.s. Value Types, and ARC. Once you're already that deep in the weeds...might as well get comfortable with the mold and worms :slight_smile:

1 Like

Since the second part of the proposal seems to be motivated in part by SwiftUI, let's look at an example of SwiftUI code somebody might conceivable try to write.

struct Event {}
class EventStream {
    private var observers: [UUID: (Event) -> Void] = [:]
    func observe(observer: @escaping (Event) -> Void) -> UUID {
        let id = UUID()
        observers[id] = observer
        return id
    }
    func remove(_ id: UUID) {
        observers[id] = nil
    }
}

struct MyView: View {
    let eventStream = EventStream()
    @State var lastEvent: String = "<none>"
    @State var observerID: UUID?
    var body: some View {
        Text(lastEvent)
            .onAppear {
                self.observerID = self.eventStream.observe {
                    self.lastEvent = "\($0)"
                }
            }
            .onDisappear {
                self.eventStream.remove(self.observerID!)
            }
    }
}

Obviously, there are problems with this code beyond the reference cycle. The reason I post this as an example is that I can imagine somebody writing it as they are learning SwiftUI and not yet familiar with idiomatic techniques in SwiftUI. Not only is it possible to create a reference cycle by capturing self in value types, I think it's pretty easy for programmers who are learning SwiftUI and try to apply imperative patterns in a declarative context to accidentally do so. An error if self is omitted in such code may cause a developer to think harder about what they're doing.

We should think very carefully before removing the need to make self capture explicit in code like this. I don't think the proposal makes a compelling case that it is the right decision.

3 Likes

    "are you capturing self in a closure"
    /                                     \
  Yes                                      No
   |                                       |
Use `self`                              carry on

I'm being reductive, I know. It's more like "might you be capturing self in a closure." still

  • The first part of the proposal changes "Use self for the rest of this code block" to "capture self with [self]" (which is in some ways simpler)

  • The second part adds another case of "Is it a struct, carry on".

I suppose it's technically more nuanced, but the amount seems trivial and non-invasive. Is there some complexity I'm missing? In what circumstances will this be an issue?

Regardless, that's not how I code. Maybe I'm an outlier, but even after a few years I often forget to use self in simple closures because, well, it's simply not necessary in any similar context. Simply the ability to add 6 characters rather than going through and clicking a bunch of fixits or adding self. to everything that's relevant would be a welcome improvement. On top of what IMO is an increase in clarity for captures in general (the advantages of which I'll avoid reiterating).

2 Likes

The status quo is already a bit more nuanced than this, since it only applies to @escaping closures and may result in diagnostics on @autoclosure parameters as well.

This is why you can’t write the following today: (EDIT: this is actually a similar but unrelated issue to the use of implicit self, got my wires crossed :slightly_smiling_face:)

struct S {
    var flag1: Bool
    var flag2: Bool
    var flag3: Bool
    init() {
        flag1 = true
        flag2 = false
        flag3 = flag1 && flag2
    }
}

I'm not sure what the difference between 'technically more nuanced' and 'more nuanced' is but …

My point isn't that this is a bad change on the face of it. My point is that, right now, we have a sometimes inconvenient diagnostic with a simple response to a complex problem. The underlying issue of capturing self is itself nuanced without much help from us. Adding any amount of complexity to what we tell people to do in response to the already nuanced issue is questionable if we're not really improving how the issue is handled.

So would you be in favor of change #1, but not change #2?

yeah… I was definitely speaking 'in the context of a closure' … The notion of 'capturing self' is difficult to apply elsewhere. Not impossible, but difficult.

Yeah. It seems easy enough to justify why self in a capture list should mean that you don't have to explicitly capture in scope.

1 Like

It's very hard to give a vote on this. Wouldn't it be ideal to separate the two described features into two separate proposals? The first half (self in capture lists) is additive, while the second half (implicit self for value types) is destructive and will need a shift in developer mindset when writing and reviewing code.

2 Likes