Swift won't allow a closure, stored in a property, to reference other properties

This might be a silly question, but I'm having trouble wrapping my head around exactly why Swift can't (or at least doesn't) allow this:

struct Demo {
    var enabled = false
    var enabler: () -> Void = { enabled = true }
}

error: cannot use instance member 'enabled' within property initializer; property initializers run before 'self' is available

I suspect this error message is misleading…? The closure is not executed during initialisation, merely defined. So there's no issue with initialisation & mutation racing. The compiler does know where the struct instance is going to live, by the time it's initialising it, so it can obtain the address of the instance (self) for the closure.

A workaround is something like the following:

struct Demo {
    var enabled = false
    var enabler: (inout Demo) -> Void = { $0.enabled = true }
}

But this doesn't seem to essentially do anything other than make using enabler more verbose.

This should be read as a syntax sugar for

    var enabler: () -> Void = { self.enabled = true }

which is just as invalid as

   var enabled = false
   var disabled = !self.enabled

Until all properties are initialized, self is unavailable, which is a fundamental rule of initialization in Swift. By assigning a default value that captures self to the enabler closure, you're violating that rule. What you're attempting to write probably should've been written as

struct Demo {
    var enabled = false
    mutating func enabler() { self.enabled = true }
}
2 Likes

In other circumstances, sure, but within the context of what I'm trying to do, that doesn't work. In short I have an array of conditions and actions, which changes over time (thus a variable and not e.g. a static switch statement). I've not been able to find any syntax which allows use of functions, any more than closures as in my original example.

Even the workaround I gave earlier doesn't happen to work in my specific circumstances because although I can mutate self directly in a method of self (without even marking the method mutating, weirdly), I can't pass a mutable reference to self to a closure. Maybe it's because this "method" happens to be the willSet attachment to a @State variable. :man_shrugging:

The workaround I'm actually using, for now, is an annoyingly verbose level of indirection whereby I have an Action enum that hard-codes all possible actions, and I essentially move the body of the closure(s) to the struct member method that's executing the actions. Which happens to be viable for me because there's only one place where I execute the actions.

Right, but my question is why? The compiler does have all the information it needs, at least in principle - the address of the struct (self) etc. Is there something intrinsic about why this cannot actually work - some devil in the details? - or is it just that Swift doesn't happen to support it [yet]?

Note that your second example there is not equivalent since it's directly, at initialisation time, reading another property. That could be made defined behaviour in principle, but that's a separate discussion in any case.

Yes. This is one of the crucial differences[1] between member func declarations and closure declarations: self has different meaning in those. In a member func declaration self is always an implicit parameter. In closure declarations any identifier not declared as a parameter is captured from the environment outside of that closure. That's just how closures work in Swift, unlike for example JavaScript, in which implicit this in closures is bound dynamically and is not captured from the environment.

In a way, one could say that in Swift

    mutating func enabler() { self.enabled = true }

can be desugared as

    var enabler: (inout Demo) -> Void = { $0.enabled = true }

and then all self.enabler() calls are desugared as enabler(self).

The compiler makes your implicit self parameter an explicit one under the hood. In fact, when you observe generated LLVM IR and assembly, you'll see that self is passed as an argument to member functions, one way or another.


  1. The other one is that func declarations can be generic, while closures can't. ↩︎

2 Likes

I think it's also worth noting that the problem here isn't really limited to the fact that we're trying to do this at initialization time. Swift will reject this pattern even if you use a dummy value and require a later call to properly set up the type:

struct Demo {
    var enabled = false
    var enabler: () -> Void = {}

    mutating func installEnabler() {
        // error: escaping closure captures mutating 'self' parameter
        enabler = { enabled = true }
    }
}

It seems like you're trying to express "when enabler gets executed it should set enabled to true on the value which is associated with the currently-executing enabler function,” but to maintain that association you either need to:

  • Pass around the specific value you're talking about that you want modified (which will just boil down to having a self param) or
  • Have some sort of stable identity for self between the point where you form the closure and where it's executed (i.e., Demo should actually be a class, not a struct)

If we allowed the code to compile without either of the above, consider the following snippet:

var d1 = Demo()
d1.installEnabler() // this will capture 'self' in an escaping manner, whatever that means
var d2 = d1
d2.enabler() // just a function value, not a method, so what 'self' are we going to mutate?
3 Likes

That seems to be the critical insight. I was forgetting that structs don't have stable addresses, so [escaping] closures can't capture them. It's starting to make more sense now.

The compiler error in this case is very misleading, in my defence. :stuck_out_tongue_closed_eyes:

Thanks @Jumhyn & @Max_Desiatov!

I don't think it's misleading. It's pointing precisely to the reason the code won't compile: you were trying to capture self in a closure before it's initialized. And it's always considered uninitialized at the time when default values of properties are evaluated.

Changing Demo from struct to class in the sample code you've shared won't make the error go away, for the reason I described above. That fundamental initialization rule holds for both classes and structs.

1 Like

It's an interesting brain exercise to think about how it could be achieved.
An idea that came into my mind is to add a flavour to a function type to indicate that it's not invokable.

class Foo {
  var foo: Int
  ...
  init() {
    // here we didn't initialize `foo`
    // so we have reference to `self`, but we can't allow it to be used
    let f = { print(self) }
    // therefore we can call the type of `f` as `@noninvoking () -> ()`
    f() // an attempt to call it should produce a compile-time error
    foo = 0
    // but here we have `self` initialized
    // so we can upgrade the type of `f` to just `() -> ()`
    f() // ok
  }
}

But this is an easy case, as the control flow analysis can "see" where the actual invocation is happening. It's getting much harder if the function escapes the frame.

1 Like

Let’s compromise and say the error wasn’t as clear as it could have been, or else we wouldn’t have had this thread!

6 Likes

… or a lazy stored property:

struct Demo {
  var enabled = false
  lazy var enabler: () -> Void = { enabled = true }
  // error: escaping closure captures mutating 'self' parameter
}
2 Likes