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

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

9 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

In my example I meant to nest two property wrapper types and the wrapped property to be of type Value.

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

// the above forms allow us to avoid the following composition form.
@A<B, Int, String> 
@B
var property: Value

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

FWIW, I'm opposed to changing the grammar of attributes to introduce commas. It won't be at all clear when commas are needed or significant, and I don't think it clarifies this code at all.

I've put my thoughts into the proposal draft, so I won't repeat them at length here, but I feel like commutative composition is a non-goal for property wrappers.

  • Doug
7 Likes

Right, that'd work. We can even add $$foo, $$$foo, etc. in the future to access inner wrappedValue should the need arise, though quite frankly I don't even see the need myself.

What about 'half-way initialization'? Would you be allowed to do this?

@MyPropertyWrapper(name: "test") var property: Int

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

I imagine this being similar to declaring a constant value and assigning it later, which is something we can already do.

If you can construct MyPropertyWrapper(name: "test"), this will work. We can't call an initialValue: initializer with no initial value, though.

Doug

What about using a function similar to type(of:) to access the wrapper? Something like wrapper(of:keypath:)

1 Like

Putting it first is fine. If this can be done, this resolves the last issue I have with the proposal :)

1 Like

For anyone following along on this thread... property wrappers is up for its second review at SE-0258: Property Wrappers (second review) .

Can I still post a question in this thread?

Regarding synthesized Decodable, I am trying to find a way to make an omitted key acceptable.

The following type will decode the JSON "{}" successfully:

struct OmitTest: Decodable {
    var val: String?
}

let val = try! JSONDecoder().decode(OmitTest.self, from: #"{}"#.data(using: .utf8)!)

However, I cannot think of a way to retain that synthesized behavior while adding a wrapper to the property val.

Given the trivial Property Wrapper

@_propertyWrapper
struct Omittable<T: Decodable>: Decodable {
    var value: T?

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        value = try container.decode(T.self)
    }
}

The following code exhibits the behavior I am looking for (without using the property wrapper as a property wrapper)

struct OmitTest: Decodable {
    var val: Omittable<String?>?
}

However, I cannot find a way to retain that decoding behavior when using it as an attribute

struct OmitTest: Decodable {
    @Omittable var val: String?
}

Is the above the intended behavior of synthesized decoding?

2 Likes