Could we allow implicit capture of let properties for escaping closures?

Hello,
This is my first real post on this forum and it's about something that I haven't found being discussed anywhere yet, but it's one of the main things I've always wondered about since I started using Swift a year ago.

I hope this is the right category, I guess it's half a question and half a pitch.

In this simple code example:

class Example {
    let foo: Int

    init(foo: Int) {
        self.foo = foo
    }

    func bar() {
        DispatchQueue.main.async {
            print(foo)
        }
    }
}

We get the error:

Reference to property 'foo' in closure requires explicit 'self.' to make capture semantics explicit

Because I generally don't want to unnecessarily capture self (although in this simple example it won't really matter of course) and foo is a let property that won't be able to change anyway, what I generally end up doing is this:

DispatchQueue.main.async { [foo] in
    print(foo)
}

But what I don't really understand is why I need to explicitly capture foo when it's a let property in the first place. Why can't my first example just work? Am I missing something obvious? Could it be allowed in a future version of Swift?

1 Like

I don't think being a let variable makes it any less likely to form reference cycle (an argument for SE-0269). It'd still capture self when accessing foo.

You may be thinking of a slightly different but also interesting idea of sugaring capture list for let stored properties—having the compiler implicitly capture let variable for you.

Yes, I was basically thinking what I do in my second code snippet could be done automatically/implicitly.

The current behaviour in combination with the wording of the error message might lead newcomers to unnecessarily and sometimes dangerously capture self. At least my experience as a newcomer was that it was pretty cumbersome to deal with self capture all the time and in a lot of cases it seemed unnecessary, since I use a lot of let properties in my code.

Also the [foo] capture of anything else but self seems to be a lesser known feature of Swift, probably also because right now you're guided to capture self in all cases by the error message. Changing the error message or offering a different fix-it could also be a solution, but personally I think that would be more confusing and automatic let capturing less confusing.

Is there a reason for it to capture self instead of only foo? The only difference that could cause as far as I can tell is releasing self earlier, which sounds like an optimization for me.

If the closure captures self, it will print the value of foo at the time it is executed. If the closure captures foo, it will print the value of foo at the time of capture. (Which doesn't matter for a let property, but would for a var, obviously.) The compiler doesn't really have an ability to guess what you need, so forcing you to state it is correct; maybe your program relies on the capture to keep the object alive.

Yeah, I meant let properties. The situation for var and methods is different.

But I’m specifically talking about let properties here. I struggle to find a scenario where you depend on your closure to keep your object alive, when all you need from that object is a constant. Even if you somehow do, that actually seems like a really implicit way to keep your object alive, instead of making things more explicit.

It's just that that's the current semantic. Referring to a property will also require access to its container regardless of whether it is let or var. Try this:

func mutate<T>(_ t: inout T, block: () -> ()) {
  block()
}

struct A {
  let c = 1
  var a = 1

  mutating func ff() {
    mutate(&self) { // <- Overlapping access to `self`
      c             // <- Overlapping access to `self`
    }
  }
}

That's why I said most likely in escaping closure you want this:

queue.async { foo }

to translates to this:

queue.async { [foo] in foo }

not this:

queue.async { self.foo }

Note that the latter of which is the current behaviour for non-escaping closure.

1 Like

You seem to be advocating a special-case behaviour for this particular situation. That sounds like a bad deal all around.

I'm not saying that the alternative is good. But the implicit self in this scenario doesn't avoid reference cycle to begin with. Given we're talking about the closure context, it's simply a no no.

You make a good point. I generally use the explicit [foo] capture when possible. I think it might be a good idea for an additional fix-it to suggest adding the [foo] capture (in addition to the current fix-it suggesting the use of self.foo) when foo is a let property.

4 Likes

This is something that I explored when working on the implementation for SE-0269. I ultimately de-scoped it for the proposal since it was not a semantics-preserving fix-it in the case of var properties, but if this was an additional fix-it for let properties only I think it's sound. The machinery for inserting a value into a capture list is already there, it would just need to be generalized to insert something other than self.

1 Like

I don't think there's really a reason why @Onne's change couldn't be done except that there could be programs (accidentally) relying on the current semantics. That said, it's always a little weird when let behaves differently from var in ways not related to setters (or optimization), so I think I'd agree that adding the fix-it because it's known-safe is a good way to go.

3 Likes

But there are no programs that rely on current semantics, aren't there?

For capturing, currently foo is a compiler error, you have to write self.foo

For overlapping access, we would be removing a runtime error, not adding one

2 Likes

Thanks everybody for the feedback. Seems to me there are differences in how people expect the compiler to behave. I personally didn't expect the compiler to capture self in my example when I started out with Swift and I was surprised that it did want to do that. Maybe it's just my code where I have a lot of let properties on classes like view controllers, but I run into this quite often.

I have another reason why I would prefer a change in compiler behaviour over a different fix-it:
If you later for some reason change the let into a var and you already put the property in the capture list, that might not be what you want, but will go by unnoticed.
With my proposal, changing the let into a var will cause a compiler error, forcing you to think about if you want to capture self for your closure.
This way you're only forced to think about and deal with self capture the moment it matters.

1 Like

Good points, @cukr and @Onne. I'm still a little concerned about having different behavior between var and let like this, though. In particular, if I replace a constant with a computed property, today that won't break anyone's code, but with this change it could make existing client code invalid. In other words, the behavior @Onne's talking about is good if you control all the code, but not if you're writing a library and trying not to break your clients.

(I suppose we could limit the "convenience capture" behavior to only within the current module, but now it's getting harder and harder to explain.)

4 Likes

Pesky forward source compatibility :sweat_smile: I don't think about it often enough because I don't write libraries

You convinced me that current behavior + a fixit is the best solution

And anyway, once Swift 5.3 ships it’s not longer the case that bare foo (without self.) inside an escaping closure is a compiler error. If self is a value type then bare foo Is perfectly acceptable (and results in all of self being captured).

In my original post I asked if I was missing something and I guess the answer is forward compatibility for library authors. Like @cukr it's not something I think about much :sweat_smile:.

Anyway, knowing the reason already makes it much less frustrating.

1 Like