[Pitch] Property delegate composability, backing storage, and $

If property delegate composition was commutative then it would be by far too restricted. You simply can't compose @A and @IntDelegate to bake a property of type Int then. And as soon as both property delegate type have more than one generic type parameter requiring them to be commutatively composable brings even more issues. OTOH if we simply say that property delegate are composed from right to left or bottom to top, where the most nested delegate refers to the property type is the most right or most bottom delegate, it significantly simplifies the design.

Yes of course it does. That said, I think the potential for confusion is significant, especially if the feature uses attribute syntax. I think this is an issue that the proposal text should address, regardless of what the design is.

Ah, okay then I misunderstood your post a little. I agree that the proposal should be more specific about this design.

I also hope that we can use explicit nested composition with a single attribute like showed above.

@C<B<IntDelegate>, String> var foo: Int

What about my proposal to use underscores to make type variables explicit? To repeat our conversation from SE-0258, instead of @A @B var x = y, one would write: var x : A<B<_>> = y and sidestep the unordered nature of attributes (and not "burn through" the attribute namespace). Here is a link to our earlier conversation:

The nested composition syntax doesn't work as is because it's not obvious how many levels to look through. In the following code, does the variable's public type become Int or B<IntDelegate>?

@C<B<IntDelegate>, String> var foo = 0

Why is it not obvious if the type of the property was explicit anyway?

Your example is a little tricky as the inference resolution is not fully clear. We could go by init(initialValue:) of C if it was present, or check if B<IntDelegate> conforms to integer literal protocol or otherwise traverse down one level deeper and so on.

If the type of the property is explicit, it's a little better, but still requires the compiler to keep searching until it finds a value property of the right type. I don't love this kind of arbitrary-depth rule (like operator-> in C++), but it would be well-defined. It rules out types that can be their own delegate, though, and the syntax gets in the way of allowing inline initialization for the delegate (unless you want to require that the type of the delegate match the type of the initialization expression).

Why have we started caring about allowing the type of the property to be implied by the delegate type? Independent of any implementation concerns or subtleties of the design, is it even a good idea to do that?

1 Like

That has always been in the proposal; the very first example is @Lazy var foo = 1738.

(I'm personally against any property types ever being inferred except maybe plain old literals, but I haven't put together my case for that yet.)

That's just ordinary type inference. Somehow I thought you were suggesting that the type of the property could be implied from the delegate type, so that e.g. @AlwaysAnIntDelegate var x would in fact always be an Int.

1 Like

I thought the inference came from the implicit Delegate.init(initialvalue:), which can be unrelated to the type of the value property.

Yes, that's correct.

I suppose we could do that, checking whether the type of each value returned is itself a property delegate type. It feels a lot less intentional that writing out the two types separately.


That is true in some cases, but in that particular case merging all property delegates into one attribute results in way less boilerplate than otherwise would be required.

@C<B<IntDelegate>, String> // requires explicit types because of second generic parameter
@B // `<IntDelegate>` is inferred from next property delegate attribute
var foo: Int


@C<B<IntDelegate>, String> var foo: Int

We can just omit the two extra property delegate attributes as the very first one already describes the composition in a more compact way. Keep in mind that's just one example. In other cases you likely still want @A @B var property: Int.

At some point, you write a new property delegate type to capture something complicated like @C<B<IntDelegate>, String>. There are going to be limits of what can be arbitrarily composed.

In this case, I'm thinking mostly about the programmer model: the "use separate attributes to compose and the synthesized accessors will have a .value for each attribute" model is straightforward. If we're looking at the type of each value, then a property delegate type cannot be used as a "normal" type within another property delegate--it's always magic, even when the user didn't ask for it. That feels wrong.


I understand that the whole composition topic will eventually hit a wall where it simply cannot compose something that the user might want. Yet I think we could have a soft rule that can check if value is itself a type marked with @propertyDelegate attribute and then compare the type of the main property with the first value's type or if it does not match with the nested property delegate's value type until it matches the types or results in a type mismatch because it couldn't find value with the same type as the main property.

Such rule would allow @C<B<IntDelegate>, String> var property: Int. However that's just a random example which might not even be that common, but who knows.

I'm fine if this works at first:

@C<B<IntDelegate>, String>
var foo: Int

And if this happen to be more common than originally thought we could teach the compiler the above rule. That way it does not have to be implemented with the first version, if it's too complex to implement.

We want type information to flow the opposite direction, i.e. we want to infer generic arguments for the delegate types from the value type of the property. I don't know how that works in the face of ambiguous composition.

Sure, but if you have a property delegate with two generic type parameters you can't infer the second generic type anymore and the user would need to provide it explicitly, but that also means that the first generic type must be written out explicitly.

@C<B<IntDelegate>, String>
var foo: Int

In this example, the property is of type Int, that matches with value in IntDelegate, B is inferred as B<IntDelegate>, and last but not least C would be inferred as C<B<IntDelegate>, _>, but the second parameter is not known, that's why it's written out explicitly.

In case of just:

@C<B<IntDelegate>, String> var property: Int

The compiler would NOT infer the type, but rather just CHECK the types if they align. That however would only work in the reverse order by further traversing value's type on type mismatch and iff value's type happen to be another property delegate until it finds a value with the same type as the main property or fails with a type mismatch. It should only traverse further the nesting hierarchy if it knows that value is another property delegate, otherwise it will fail if the type from main property does not match value's type.

It may be obvious but I can't really imagine how composition could ever be commutative. True, I don't really see many cases where composition makes any sense at all, it seems like in all but the trivial cases the composed property will behave in very unpredictable ways. What does @Lazy State UserDefault do?

But getting back to my question, how could @A B ever be the same as @B A, even on the type level? For A<B<X>> to be the same as B<A<X>> would require A == B? Which in turn, unless I'm mistaken, would require both types to have delegateValues since the name of a propertyWrapper is otherwise determined by it's type, right?

But even when the storage type is the same, it seems that the behaviour of @A B would very rarely be the same as @B A.

But I may have completely misunderstood the point, the whole idea of composing property wrappers is still hazy to me.

This is an old thread, please consider moving the discussion to the newest corner.

Also you may need to catch up the latest discussions and design changes.

OK thanks, I thought this was a separate discussion but I see now that it hasn't been discussed for a while!