[Pitch #3] Property wrappers (formerly known as Property Delegates)

Yea, that should be the case.

I'm thinking that since now the author can omit the wrapperValue, access to it is meaningful, and if we have multiple of them, we can't access the inner wrapperValue at all. Furthermore, if we have wrapper without wrapperValue in the mix, it could be confusing if $foo doesn't always mean the outermost wrapperValue.

@NoWrapper @HasWrapper var foo: Foo
$foo
// Should this be error, because NoWrapper has no wrapperValue?
// or should this use _base_storage.value.wrapperValue (HasWrapper's implementation)?

One thing we could do is that we can differentiate them by type, that is

@propertyWrapper struct Wrapper1 {
  var wrapperValue: Foo1 { ... }
}
@propertyWrapper struct Wrapper2 {
  var wrapperValue: Foo2 { ... }
}

@Wrapper1 @Wrapper2 var foo: ...
let bar1: Foo1 = $foo // Uses Wrapper1 declaration; _base.wrapperValue
let bar2: Foo2 = $foo // Uses Wrapper2 declaration; _base.value.wrapperValue

With this, foo will have full access to all wrapperValue as overloads, and so collision shouldn't be much of a problem.

I have hard time convincing myself that self is a sensible default, especially since the most basic of wrappers won't need it (Atomic, Lazy, etc). While that's definitely debatable, I feel that it's at least as important that the author can opt out of it, and I think this would be the best way to allow that.

2 Likes

It does seem reasonable. However, I don't see how having access to the implicitly-created wrapper variable is actively harmful, even when it has no useful API.

Agree with much of the feedback here so far.

The one thing I'd like to chime in about is that I'm not enamored of using a notation that's currently order-insensitive for non-commutative composition.

I'd hope to see some sort of indication of non-commutative nesting, and I don't think it has to be onerous:

// Notation in proposal
@DelayedMutable @Copying var path1: UIBezierPath
// $path1 has type DelayedMutable<Copying<UIBezierPath>>

// Bikeshed notation
@DelayedMutable(@Copying) var path1: UIBezierPath
// $path1 has type DelayedMutable<Copying<UIBezierPath>>
1 Like

This bikeshed color doesn't interact well with the ability to invoke initializers on the wrapper. Maybe we could require users to write the generics explicitly, but allow inference of the value argument so it would be:

@DelayedMutable<Copying> var path1: UIBezierPath

How about requiring a comma between property wrapper attributes? It would require that wrappers be listed with intervening attributes, but I don't think that would be too much of a burden.

@DelayedMutable, @Copying var path1: UIBezierPath

The comma gives it a list-like implication, which implies ordering.

I'm leaning toward switching to wrappedValue. I'm fretting over whether that means init(initialValue:) should become init(initialWrappedValue:), because... I really like init(initialValue:).

Doug

1 Like

I think we can keep init(initialValue:) as it does not hurt anyone nor it's wrong to require the compiler or the user to write wrappedValue = initialValue. As a bonus we don't need an explicit self. here. I mean it even reads well as "wrapped value is assigned with initial value".

1 Like

Xcode 11 beta 1 roughly matches up with the first reviewed proposal.

Doug

Is nesting implemented in any available toolchain?

I would favorite this style where the inner generic type parameter lists can be omitted in some cases. This plays well with property wrappers that have more generic type parameters and this would avoid repetition of property attributes.

@propertyWrapper struct A<Value, X, Y> { ... }
@propertyWrapper struct B<Value>

@A<B, Int, String> var property: Value

I think wrapperValue is a misnomer (even in its current form) since it's not necessarily the value of the wrapper per se.

How about wrappedValue and alternateValue?

This is a little confusing, it implies that B == Value which isn't exactly the case, how about

@A<@B, Int, String> var property Value

I'm inclined to make this change, although I'd always put the initialValue: argument first (since it won't be defaulted or variadic, whereas others might be).

Doug

10 Likes

My inclination here is to say that $foo is only there if the outermost property wrapper has a wrapperValue, making your example an error.

I'm strongly opposed to overloading $foo in this way. The compiler shouldn't be introducing type-based overloads.

Doug

1 Like

I think you misunderstand what I meant to say here. In this particular example <Value> from B is omitted, while Value for A is indeed B, but B alone isn't the full type, however B<Value> is.

// this makes the following 
@A<B, Int, String> var property: Value

// as a short form for
@A<B<Value>, Int, String> var property: Value
1 Like

Not yet.

Doug

Strongly agree here.

I understand what you meant, but in its full form, property itself will be of type B<Value> instead of Value if we're to omit the type since B<Value> is still a valid type.

@A<Value, Int, String> var property // : Value
@A<B<Value>, Int, String> var property // : B<Value>

Edit:

nvm, it seems not to be a unique problem to this syntax.

It's implying that B<some inferred generic arguments> == Value, which is exactly what it's doing. Moreover, the type inference rules in the latest draft imply that this will work. I went ahead and changed @DevAndArtist's example to not use composition:

@_propertyWrapper struct A<Value, X, Y> {
  var value: Value
}

struct Z<T> { }

struct Test {
  @A<Z, Int, String> var property: Z<Float>  // okay! Infers <Float> for the `Z` in `A<Z, Int, String>`
}

The "master" implementation of property wrappers, which implements the new type inference rules, makes this work. It's completely reasonable for this to work in the composition case (once I get around to implementing composition).

This would change the type grammar. We shouldn't do that.

Doug

1 Like

That is correct, but in my case I did not omit : Value so no ambiguity applies to that example.

1 Like