Why is it impossible to declare an immutable property wrapper property?

Swift does not allow you to define a fully immutable property wrapper:

struct SomeStruct {
    
    @APropertyWrapper let aProperty
    
}

Triggers the following compiler warning:

property wrapper can only be applied to a 'var'

Why does this limitation exist?

I feel like I'm missing something obvious here, but I'm not sure what. This decision also doesn't seem to be documented in the final proposal for property wrappers.

(Worth pointing out, just in case, that even defining the property wrapper's wrappedValue without a set doesn't achieve true immutability — I can still re-assign the property wrapper itself using _aProperty = APropertyWrapper() at any time.)

2 Likes

property wrapper makes a computed property - we use var for these in swift

that's because the value can change even if the storage is constant, and you never set a different value (for example a property wrapper that always returns a random value)

Now that makes sense to me, if I'm understanding you correctly: because property wrappers can be computed, it makes sense to restrict them to var declarations only, if only to sort of set users' expectations.

On the other hand, I'd argue that there's a case for let property declarations being allowed, but acting as a constraint on the type of property wrapper that can be used. In other words, trying to use let with a computed property wrapper would trigger a compiler error that could only be solved by redefining the property wrapper's wrappedValue so that it was backed by pure storage and not computed accessors.

(This picture gets more complicated when you throw reference types into the mix, but I think the same point generally applies.)

2 Likes

I can see something like you're suggesting being possible, but the picture gets even more complicated when thinking about API and ABI stability (currently you can always "upgrade" let on a nonfrozen thing to a var or a computed var without breaking anything)

3 Likes

With protocol, it's the opposite: { get set } property can be declared as get and compile just fine but it doesn't do what you expect: there is no animation interpolation:

public protocol Animatable {

    /// The type defining the data to animate.
    associatedtype AnimatableData : VectorArithmetic

    /// The data to animate.
    var animatableData: Self.AnimatableData { get set }
}
struct MyAnimatable: Animatable {
    let animatableData: Double
}

I wish the compile would not allow this and show error "it cannot be let, must be var.

That is in fact how things work. You must be thinking of a similar but different situation.


I think not being able to mark wrapped properties with let is preposterous. If you use the same type without the sugar, then you can remove access to mutating members when desired. But then you lose that capability with the sugar.

@propertyWrapper struct Wrapper {
  var wrappedValue: Void {
    get { () }
    set { }
  }
}

let wrapper = Wrapper()
wrapper.wrappedValue = () // cannot assign to property: 'wrapper' is a 'let' constant

let means "guaranteed not to change for the lifetime of the property"*, which is stronger than just "cannot be mutated". There's no way for the compiler to validate that for a property wrapper without exposing whether wrappedValue is stored or computed. As @cukr noted above, depending on that would break the ability of the property wrapper author to change their property wrapper.

Now, this is an explanation of the current behavior, not an argument that this is the best behavior. But a proposal to change the current behavior should address the current behavior. (In particular, people don't seem to design libraries with that as a guarantee, you can't check for let vs. get-only var within the language, and the compiler can probably infer it anyway in many cases.)

* the lifetime of the instance for an instance property, the lifetime of a local for a local, or the lifetime of the program for a static or module-level property

9 Likes

Could let declarations hypothetically be allowed for @frozen property wrappers with stored wrappedValues?

This is an interesting explanation to me, because if so Swift lets developers break this promise all the time. You can declare let properties that hold values types with computed values. Why are property wrappers deserving of special treatment?

Can you elaborate? Do you have an example of something you consider to be in breach of the promise?
I think the promise is compiler enforced and impossible to break. So probably I misunderstand what you mean.

I'm referring to instances like this one:

struct StructWithComputedValue {
    
    var value: Int {
        Int.random(in: 0..<10)
    }
    
}

struct SomeOtherStruct {
    
    let structWithComputedValue = StructWithComputedValue()
    
}

let instance = SomeOtherStruct()

instance.value // random

I don't think anything here changes during the lifetime of properties.
It is always possible to declare impure function and computed properties.

Elaborate?

instance is fixed and non-mutable. value is an impure computed property, basically a function without function invocation syntax. None of these change. But as a consequence of being impure, the computed property's return value is random. That's similar to assigning a closure to a let, non?

Yeah I agree with you. I guess I'm not sure if that aligns with this statement though:

let means "guaranteed not to change for the lifetime of the property"*, which is stronger than just "cannot be mutated". There's no way for the compiler to validate that for a property wrapper without exposing whether wrappedValue is stored or computed. As @cukr noted above, depending on that would break the ability of the property wrapper author to change their property wrapper.

If that's what let means, and the compiler can't validate that unless it checks whether wrappedValue is stored or computed, then shouldn't that apply to any let declaration, and not just property wrappers? You can declare any property as let — "guaranteed not to change for the lifetime of the property" — but the compiler currently doesn't perform any verification about whether they're stored or computed. I'm not sure why property wrappers are a special case.

It's possible I'm just not understanding the semantics here.

@PropertyWrapper var foo: Foo = ...

… gets de-sugared to something like

private var _foo: PropertyWrapper<Foo>(...)
var foo: Foo {
    get { return _foo.wrappedValue }
    set { _foo.wrappedValue = newValue }
}

I think it's only special in that the above code gets auto generated. The semantics are the same as if you wrote that yourself, methinks.

There's a difference between "the property won't change" and "the properties of the property won't change". Computed properties can do whatever the heck they want, as your value demonstrates, but that's still different from saying "the thing you stored here can't change". This is partly for optimization—compilers really like it when you promise that the contents of memory won't change, even while you execute arbitrary code around them—and partly because, well, types have semantics. You can write a struct whose computed property picks a random number, or that has reference semantics, but presumably the user of your struct would know that. And in fact, sometimes that's useful!

struct BoundedRandomNumberGenerator {
  var limit: Int
  var nextRandomNumber: Int {
    Int.random(in: 0..<limit)
  }
}

class ContrivedExample {
  let rng: BoundedRandomNumberGenerator
  init() {
    rng = .init(limit: 10)
  }
  func doSomething() { …}
  func demo() {
    self.doSomething()
    print(rng.nextRandomNumber) // guaranteed to never be greater than 10, because `rng` is declared with `let` and therefore it doesn't matter what `doSomething` does.
  }
}

EDIT: You could argue that a property wrapper ought to treat the wrapped value as "the property of a property", and that the let on a property with a property wrapper should apply to the underlying property wrapper (as shown in @sveinhal's example desugaring). I think that was sufficiently non-obvious / ambiguous that the designers of property wrappers made it off-limits. Again, though, that's an explanation of current behavior, not an argument that this is the best behavior.

3 Likes