SE-0258: Property Wrappers (third review)

Hi @DevAndArtist,

Can you or other that share the same concern elaborate on why this is an issue?

See below

Can you provide a concrete example why or where such collision would ever occur?

See you question below.

Why would you have a wrapped property foo but also a totally different _foo property which is unrelated?

Why not?

If that is the only explanation for your (but maybe not others) concern then I don‘t understand how something like this can be justified as a dealbreaker.

I still would like to see anyone providing a concrete code example where it would be critical to have a wrapped property but also a totally different and unrelated property that shares the exact same identifier as proposed for private backing storage which then would collide.

5 Likes

Thank you for your hard work.

A.swift
class Test {
    @MyWrapper var t = ""
}

private func test() {
    let d = \Test.$t
}

B.swift
private func test() {
//    let t = Test()
    let d = \Test.$t // cannot access
}

C.swift
private func test() {
    let t = Test()
    let d = \Test.$t // can access
}

Is this a bug?

1 Like

Yes, definitely, we'll try to get it fixed.

2 Likes

Please file a bug report if you haven‘t already.

SR-11046

2 Likes

Is there any plan to use Property Wrapper to enable simpler Codable usage where one wants to specify a JSON key being different from the name of just one property?

That would really be awesome, because AFAIK currently Codable Dora not offer me to use the synthesized init/encode function and keys if I wanna change just one key. For bigger types, with e.g. 10 properties (JSON fields) this is very cumbersome. Property Wrapper might solve this?

You don‘t need real world examples to dislike names that could (theoretically) collide - imho that is a question that is more fundamental.

Objective-C has a history of „magic“ names (property setters, and maybe more), but imho it‘s no good fit for Swift when changing names has side effects on other properties of a type.
Take inherited methods as example:
How likely is it that you implement a totally different and unrelated method with an identical name?
Still, Swift demands that you override explicitly.

Memory layout is another example: It is actually a property of a given type - but afair, it isn‘t modeled that way because of possible interference with static properties.

If we would reject every SE proposal just because some people say "why not?" as an explanation for their concern we wouldn‘t have a language we have today.

If people cannot present any possible concrete examples (even hypothetical pseudocode), I don‘t buy these contra arguments. To be clear I only speak for myself here.

Any form of identifier name for the backing storage is a subject for a collision because "who says that in the future such identifier won‘t be allowed as valid user code"!?

8 Likes

Every variable may collide with an other variable. If you encounter such case, just do as you do right now, change the name of one of the two variables. That's not like if the compiler where imposing you a fixed name on all types (what it would do for memory layout).
We are talking about a name that can be changed easily as it is based on the wrapped variable name, and that will be generated only when a type contains a property wrapper.

1 Like

It's in the future direction (Delegating to an Existing Property) for you to opt-out of the default storage name, should you wish to.

2 Likes

So, if I make a MyWrapper type that goes around an Int, and use it for a property I call "value":

  • value accesses the Int.
  • _value would access the MyWrapper if it's used in an accessible context.
  • $value lets me configure the wrapper, its type is Self by default, but I can change it to any other type by specifying the projectedValue property.
  • If a projection is used, I must use it; I have no way to override the decision of MyWrapper's author to use a proxy type.
  • A previous version of the proposal used to have a $$value to override my previous point, but we can't do that now.
  • If stacked wrappers are used, we can only access the projection of the outermost one? Or can we get to the intermediate ones, whether or not they use Self?

Why is the substitute feature called a "projection"? Wouldn't "proxy" be better?

$value only will exist if your MyWrapper implements projectedValue but that is optional, nor is it a requirement that it returns Self. In the previous version the user cannot override what the wrapper projected if you are not its implementor. You also don‘t need to use $value if there is a projection, in private context you can still use _value to access the wrapper directly. That said you can also access or expose nested projectedValue of nested property wrappers.

So if your wrapper doesn't expose a projectedValue member, you can't do $value at all (leaving no access for reconfiguration)?

Can you show an example of this (if you know how)?

I apologize if this was already discussed. It’s getting hard to keep up with this :) if it was, feel free to just point me in the right direction. I figure it’s worth bringing it up just in case.

My question is, how do property wrapper affect reflection on an object? If not, should they? Maybe a library author might want to take advantage of that.

1 Like

If your not the creator of the wrapper type then no, module clients are not allowed to retroactively extend wrapper types they don‘t own with projections like projectedValue, because they don‘t have access to the private backing storage the compiler creates for the module wrapped properties.

One could argue that it should be possible on your own properties that you wrap, but that can be left for future discussion. (See my next post.)

I must apologize for some confusion, I will edit my previous post. The nesting wrapper does not need to expose the access because the nested wrapper type will be accessible through its wrappedValue. Therefore you only need to traverse through outermost wrapper and all the nested wrappedValue up to the wrapper type of your interest and explicitly access its projectedValue.

I omit initaliters for the simplicity of the example:

@propertyWrapper
public struct W<Value> {
  public var wrappedValue: Value

  // this property is optional to implement
  public var projectedValue: AnyTypeYouWant
}

@propertyWrapper
public struct IntWrapperWithStringProjection {
  public var wrappedValue: Int
  public var projectedValue: String { "\(wrappedValue)" }
}

public struct Example {
  @W @IntWrapperWithStringProjection
  public var value: Int = 42

  // results into
  private var _value = W<IntWrapperWithStringProjection>(initialVakue: .init(initialValue: 42))
  public var value: Int { ... }

  // likely wrong type, see my post bellow
  public var $value: AnyTypeYouWant { ... }

  public func getInnerStringProjection() -> String {
    return _value.wrappedValue.projectedValue
  }
}

Edit: I will edit this example when I get a clarification on the projection rule on composed wrappers (see my post bellow).

@Douglas_Gregor just to clarify, since now the design of projections has shifted should we allow retroactive extensions of projectedValue?

The compiler won‘t generate any projections on types the user don‘t owns, but it safely can do so for user properties.

If the wrapper creator adds that property later it will break users code, but that‘s true for all extensions on types the user don‘t own.

The previous restriction can be theoretically lifted with current design.

1 Like

@CTMacUser actually now that i wrote the above example, I‘m not sure which type $value will have, either String or AnyTypeYoutWant.

@Douglas_Gregor the proposal needs to clarify which projectedValue will be projected in case of wrapper composition. It is weird that value is from the inner most wrapper but the projection from the outer most wrapper in my example. I tend to say it should come from the inner most wrapper.

The implication would be that if the inner most wrapper has nothing to project, but the outer most or the nearest ancestor wrapper has something to project, that the compiler won‘t do anything as the rule will create only a projection for the wrapper that also matches the wrapper properties type.

That said, I think I made a mistake in the desugaring, as $value should be of type String instead.

// correct desugaring
public var $value: String {
  get { _value.wrappedValue.projectedValue }
  set { _value.wrappedValue.projectedValue = newValue }
}

// wrong desugaring
public var $value: AnyTypeYouWant {
  get { _value.projectedValue }
  set { _value.projectedValue = newValue }
}

Maybe answering myself:

Given a concrete type with a prop-wrapper

class ConcreteType {
  @MyWrapper var myVar: MyClass
}

// use-site of projection:
let obj: ConcreteType = ...
let pro: MyProjection = obj.$myVar

Some time later I might refactor and introduce a protocol which should give access to both the property and the projection - I could do this:

protocol MyProtocol {
  var myVar: MyClass { get }
  var myVarProjection: MyProjection { get }
}

extension MyClass : MyProtocol {
  var myVarProjection: MyProjection { 
    return $myVar
  }
}

But this will require to change all call-sites where $myVar was used before to myVarProjection:

let obj: MyProtocol = ...
let pro: MyProjection = obj.myVarProjection

What about a way to make $ available for protocol oriented programming styles:

protocol MyProtocol {
  @projected(MyProtocol) var myVar: MyClass { get }
}

// or alternatively:
protocol MyProtocol {
  var myVar: MyClass { get $(MyProtocol) }
}
3 Likes

Why would you impose the type of wrapper in the protocol, while the only thing you want is the type of the projection ?

What about:

protocol MyProtocol {
  var $myVar: MyProjectionType { get }
}

For instance, you may want a protocol that expose a binding, but you don't care if the binding come from a @State or a @ObjectBinding property.

2 Likes