Value semantics of init with defaulted private let fields does not make sense

one popular way to manually insert padding into a Swift struct is by doing the following

struct S {
    let x: Int
    private let padding: Int = 0

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

now it’s not quite right - the struct isn’t @frozen and so the layout isn’t correctly specified, so this code doesn’t formally do what the library author probably thinks it does. nevertheless, it’s a very popular pattern, it is used, for example, by Google flatc.

if you’re handed an API like this, you run into some really inconsistent compiler policies surrounding definite initialization. you can’t, for example, do this:

extension S {
    init() {
        self = .init(x: 0)
        // Immutable value 'self.padding' may only be initialized once
        // Initial value already provided in 'let' declaration
        // Change 'let' to 'var' to make it mutable
    }
}

this is a rather useless guardrail, because self = new is something that should always be semantically valid for value types, regardless of field mutability, and it’s trivial (but awkward) to circumvent the restriction:

protocol P {
    static func create(x: Int) -> Self
}
extension P {
    init() {
        self = .create(x: 0)
    }
}

extension S: P {
    static func create(x: Int) -> Self {
        .init(x: 0)
    }
}

let s: S = .init()
6 Likes

Yeah, this looks like a bug to me.

7 Likes

Why not just write

extension S {
    init() {
        self.init(x: 0)
    }
}

...because it would not then demonstrate the bug...

5 Likes

good question

the most common reason why is because you get the data for the init from some API that vends a closure

extension S {
    init(xml: XML) {
        self = xml.parse { .init(x: $0.decode("x"), y: $0.decode("y")) } 
    }
}

classes Suck! you cannot do this with classes. that is why value semantics are exquisite

4 Likes
3 Likes

To those who think this is a bug... what would the fix be? My mental model of the outlined scenario was that it is consistent with how things work were you to try and write to a default-initialized let property directly:

extension S {
  init() {
    // self.padding implicitly set here b/c it has a default value
    self.x = 0
    self.padding = 0 // error: immutable value 'self.padding' may only be initialized once
  }
}

What is the right way to think about the difference between this case and the case in which self is assigned to directly (self = <new>)?

I think there are a couple of precedents that hold weight here:

extension S {
    init?(y: Int) {
        return nil
    }
    init(z: Int) {
        self.x = 5
        zero()
    }
    mutating func zero() {
        self = .init(x: 0)
    }
}

As shown by init?(y:), the language knows which properties have already been initialized and is able to correctly deinitialize only those properties when the initializer fails. And zero() replaces self even though there is a let property. So I’d expect your init() to first reinitialize the padding property, then reinitialize the whole struct. Of course, in a -O build, all of that would be optimized away if the default value of the let property was side-effect-free.

1 Like

Swift already has special rules around how inits work, including @j-f1 where it tracks the properties that have been explicitly initialized. So why wouldn't this just be an extension of that fact? Explicitly setting a property in an init would become optional if the value has a default defined. Of course, this is already how it appears to work for vars with defaults, the behavior would be extended to let. So while it currently works the way you describe, what's the difficulty in reimagining it? Is there some correctness issue here?

I do think it should not be allowed to reassign let properties in an initializer. Allowing self = ... would not break that constraint because the right-hand side must itself come from a valid initialization with the defaulted value of the property.

What your objection? If we simply don't define it as "reassignment", would that be enough? I think this has come up before and there was a specific reason why Swift didn't do it, but I don't recall it being some fundamental limitation.

I think that if it has always been that way we shouldn’t make a change without a clear rationale.

Sorry, I may be overlooking something, but in which example would you expect this to occur?

I'm not sure if there are correctness issues – you can read about some of the motivation in the commit that made the current logic work how it does today, but unfortunately it references internal issue ids, so the full rationale seems somewhat opaque to me. In general I think the "lets cannot be overwritten" rule is useful and straightforward to reason about, and I agree with Jed regarding the Chesterton's fence argument against changing it.


I think I've come around to this proposed change as it seems like it would make the language more consistent. It would be nice to have self within a struct initializer behave in the same way that a local var or self within a mutating method already do.

In init(z:) and the S.init() from the OP.

Here's a possible change that would alter the diagnostic behavior in the proposed way.

1 Like

Interestingly, this is legal:

struct S {
    let prop: Int
    init(prop: Int) {
        self.prop = prop
    }
    init(arg: Int) {
        prop = 1
        printSelf {
            self = .init(prop: arg)
        }
    }
    func printSelf(body: () -> Void) {
        print("Before", self)
        body()
        print("After", self)
    }
}

…although the version without the printSelf { ... } wrapper is not.

2 Likes

I feel like that's further evidence the existing error should be relaxed. Though I must say I find the behavior there rather confusing... the capture of self is effectively inout but the value of self in printSelf(...) is the "old" value. I guess that's how it's supposed to work?

I agree the error should be relaxed.

About the confusion, I do think that works as intended. The trick is semantically equal to:

struct S {}

func foo(_ copied: S, _ block: () -> Void) {}

func initialize() -> S {
  var `self` = S()
  foo(self, { self = S() })    // critical line
  return self`
}

the invocation of foo does not violate the rule of exclusivity, because the first argument is copied first.

Should foo be defined as foo(inout S, () -> Void), we then can not perform the same trick, because foo(&self, { self = S() } violates the exclusivity rule.

@CrystDragon While your answer is relevant (there are multiple copies of the value in play), it doesn't directly answer @jamieQ's question about @j-f1's code:

let s = S(arg: 0)

// Output:
// Before S(prop: 1)
// After S(prop: 1)  <- Why does self still have the old value?

The value eventually changes. It's just that it doesn't happen inside printSelf:

let s = S(arg: 0)
print(s)

// Output:
// Before S(prop: 1)
// After S(prop: 1)
// S(prop: 0)  <- the value is updated after `printSelf` call completes

I find this is a common behavior and it has nothing to do with let variable, init, or self assignment. Below is one example . Inside foo, one would expect that self and mutated variable should have the same value, but as they are two copies during the call of foo, they have different value. self is updated after the call of foo completes.

struct S {
    var value = 0

    func foo(_ mutated: inout S) {
        mutated.value = 1
        print("[foo] mutated: \(mutated.value)")
        print("[foo] self: \(self.value)")
    }
}

func test() {
    var s = S()
    s.foo(&s)
    print("[test] \(s.value)")
}

test()

// Output:
// [foo] mutated: 1
// [foo] self: 0
// [test] 1

What happens in @j-f1's code is similar. Immutable self and the captured mutable self are two copies inside printSelf call. The immutable self is updated after the call completes.

A possible interpretation of this behavior is that foo(_ mutated: inout S) method in my example is equivalent to a global function

func foo(_ self: S, _ mutated: inout S)

which we know should work. While it describes compiler current behavior accurately, I find the behavior counter-intuitive, because:

  • By definition, modification on inout parameter should persist.
  • That means, if a function takes both immutable and mutable copies of a value, the immutable copy should always be discarded.
  • This makes sense for a global function, because the immutable copy is considered a temporary copy.
  • This also makes sense for a mutating method taking an immutable copy of self for the same reason.
  • However, for a immutating method taking an inout copy of self, in my intuition the immutable self should be the copy that persists. I think the output of above examples also indicates the behavior is confusing. IMO Compiler should produce diagostic and suggest changing the method to a mutating method taking an immutable copy of self instead.

BTW, if the value is non-copyable, the above examples can't compile, because a non-copyable can't be borrowed and mutated at the same time.

1 Like

Jamie asked how to interpret "the capture of self is effectively inout but the value of self in printSelf(...) is the "old" value".

The simple answer is the captured self is not the same self in printSelf. Any nonmutating method of a copyable struct has an implicit self parameter, this parameter does not have reference semantics and is semantically just a copy.

I gave an example that just make the self explicit, the foo function is to demonstrate the behavior of printSelf in the original code, and initialize is just a rewritten version of the original init.