SE-0258: Property Wrappers (second review)

It does seem to me like an orthogonal feature that can be added later, what we probably need right now is strong motivation.

Would you be more comfortable with "var wrapper"?

True, I agree with you. The point I was trying to make that we should explore and design a potential generalized feature for multiple projections first (even if we‘re not going to implement them now) before we can reduce it to a single anonymous projection. That said, having thought about this idea for a day now I don‘t want to go back to the prefix form as I think it would be a wrong decision that won‘t pay off and lock down for future extension.

I did say this earlier:

So perhaps we stand on different sides on how similar/different single and multiple projections should look, and by extension, whether or not it could be added independently later on.

Dear core team and proposal authors, does anyone of you considered extending the idea of property wrappers beyond computed properties?

Why shouldn‘t we have wrapped subscripts with synthetized accessors in the future? In that sense Property wrapper would be a wrong name.

@MyBehaviorWrapper subscript (index: Int) -> Element

How many of us have written subscripts with COW optimization? Why not allowing to wrap the behavior into a type as well?

1 Like

Hmm. I think it's accurate enough, since there is going to be a var at the heart of the implementation, regardless of whether it's a property or a variable, but it seems a bit semantically … empty.

Given that the @<whatever>Wrapper type is forced to have a wrappedValue member, why not call them value wrappers? A value wrapper then wraps its wrapped value, which seems like a good thing to have as a truth. :slightly_smiling_face:

1 Like

Hmm, wrapping functions seems rather tricky (oh my, now I see why subscript uses ->) when most of the time you need to access the parent variables, which current proposal doesn’t seem to support.

It would be a future direction as it would require a few alternative rules, but in general a subscript has similar accessors as a computed property, and the current proposal mentions that it wants to solve the issue with accessing self in the future. That‘s why I think we can go beyond describing behaviors for properties and create behaviors for subscripts as well (not methods).

PS: if would accept that as a future direction then the story of projections adds another view angle because as I mentioned above it would allow us to express new things in Swift (it‘s already almost like a small DSL).

3 Likes

And I also would like to suggest another name for the proposed feature.

  • Behavior Wrapper

We do indeed wrap a certain behavior into a type and it would still apply to subscripts if it was a future direction on this proposal.

I think that @varWrapper is the technically correct name for this. As others have mentioned, is possible to introduce a @subscriptWrapper (which would work differently, and therefore should be a different thing) and it isn't unreasonable to expect @funcWrapper (analogous to python decorators?) etc.

Value wrapper isn't specific enough because functions and subscripted things are also values.

-Chris

6 Likes

Could be:

@wrapper(var)
@wrapper(subscript)
@wrapper(func)

And then if you really want to go crazy:

@wrapper(for) // @Parallel for x in values { ... }
12 Likes

Given all of the churn on the name of this feature I'd like to bump my original post proposing that we name this @valueWrapper.

A handful of others have voiced similar feelings:


Of course, given what @Chris_Lattner3 has said...

... this would get shot down pretty quickly, but only if we're serious about expanding "wrappers" beyond var values to functions and subscripts. I'm bumping this because that idea has only just recently been introduced.

If this does get shot down for that reason, I'd be in favor of @varWrapper and mildly in favor of @michelf's proposed spelling: @wrapper(var) as a runner-up.

But what exactly is the advantage of being able to project a bunch of properties like that? The end user only saves one character, and logic at the use site ia very similar.

Sure it’s cool but it seems to provide very little beyond what projecting a wrapper value and/or the storage. Maybe I’m missing something?

Strong +1 to this syntax.

I would love to see function decorators on Swift!

2 Likes

Sorry to bring up access control again, but I feel allowing property wrappers to dictate a consuming type’s access level via wrapperType to be a huge misstep. I can’t think of any other language feature in any language that allows a library author (in this case of a property wrapper) to have such a big impact on the public API of consuming types.

In lieu of custom access control for property wrapper’s storage/wrapperType (e.g. @Wrapper public($foo) public var foo: Int, the only safe option, to me, is to make everything private to the type using it. If you feel that the wrapper itself is so intrinsic to its use as to make it accessible at the same level of the property then, to be blunt, this proposal isn’t for you (yet).

The access rules here are really simple: The property is as declared, everything that supports the property is private to the type the property is declared.

Are there any existing use cases of property wrappers with wrapperType that require the second revision's suggestion of internal access? I found that odd (but kind of understandable if there's some existing uses inside Apple that need it). I find the suggestion to make them the same access level as the property itself dangerous.


As a smallish example consider:

class Widget {
    @ResetableLazy public var foo: Int = …
    @ResetableLazy public var bar: Int = …
    
    public func reset() {
        $foo.reset()
        $bar.reset()
    }
}

@ResetableLazy is a property wrapper from a library the author of Widget has imported. It's implemented without wrapperType.

The type Widget has a nice, clear public API: there's access to two properties: foo, bar, and function reset which allows both the properties to be reset at once. Users of Widget do not know how foo and bar are implemented, just that they're Ints and instances of Widget can be "reset" using reset function.

Now, if the author of @ResetableLazy decides that exposing ResetableLazy is no longer desirable and they want to expose its functionality using wrapperType instead, the author of Widget has a problem.

Upon updating to new version of @ResetableLazy, Widget now has a confusing public API: as well as the original API it now has $foo and $bar. Dangerously, it allows users of Widget to reset foo and bar separately, something the initial author didn't expect, and could lead to unexpected behaviour if foo and bar should be reset together.

Sure, you can "fix" this in the Widget type by making foo and bar private and exposing them publicly as properties that delegate to the private one (you could even write a @Private property wrapper!), but as a conscientious API designer this means you now have to do this for all properties being wrapped by property wrappers you don't control just in case this happens to them as well.


To address @tanner0101's example more directly

I would expect the public API for this type to have name and id (and whatever's on Model). Anything else is confusing. Why should I need to know the implementation of @Field to know that @Field is adding additional properties to my API? If I know it's a property wrapper, why should I care if it's implemented using wrapperType or not? (I appreciate you'd rather everything were the same access level as the property so the wrapperType vs. not distinction wouldn't matter).

This seems to be approached as the author of @Field, and seems to be suggesting that every user of a property backed by @Field must need to know that fact. To me this is not the spirit of property wrappers which is to provide a single property that hides an arbitrarily complex implementation to provide that property. Instead, here you seem to want a way to provide two properties for the price of one, which property wrappers kinda do, but only as an implementation detail.

I completely agree, and they absolutely can: on the type that uses them. If that type wishes to expose the underlying functionality of property wrapper they can add their own functions and/or properties that do so safely – as my example does. I don't see any examples on the proposal that require the wrapper to be as visible the property itself, and many that could leak privileged implementation details if they were.

I see the argument here, but as I said in my opening paragraph I can't think of any feature in any other language that allows the author of a type to have this much control over not just its consumers, but consumers of its consumers. Further, it's being conflated with a feature that a property author could quite reasonable want to use (wrapperType) without also wanting to opt in to the access change.

If you'll allow me to be somewhat melodramatic, allowing wrapperType to have this effect under the argument the API it provides is meaningful, or well-documented doesn't seem too far removed from allowing types to opt-out of access levels entirely. A type author could argue that their type should always be accessible externally to types that contain it for similar reasons being floated here, but this has never been their choice to make.

4 Likes

I have been staying out of this discussion because the design has been in constant flux and it did not seem ready for review. Now that the shape of the feature seems to be settling down a bit, there is a lot of material to read through.

I strongly agree with @jcrang (immediately above) that the author of a wrapper type should emphatically never be able to dictate the API of a type containing a property which uses that wrapper.

• • •

This proposal is called “Property Wrappers”, however it does not include the ability to, *ahem*, wrap a property. It does not do what it says on the tin. Instead, it requires that the wrapped property (aka. backing storage) be synthesized by the compiler.

Now, synthesis of the backing storage is great, and often quite convenient, however at other times it is undesirable.

Based on the vast body of pre-existing precedent, I find it abundantly obvious that the backing storage should be spelled with a leading underscore. Thus the backing for “var foo” is “var _foo” (ie. access to “foo” is redirected to “_foo.value”).

Furthermore, the access level for the synthesized backing storage should always be private.

On the other hand, if a programmer wants the backing storage to have a different access level than private, they should be able to declare the backing storage themself. To receive the synthesized access redirection, the backing storage must still be named “_foo”.

For example:

struct Bar {
  @MyWrapper
  public var foo: Int = 2
}

is equivalent to:

struct Bar {
  private var _foo: MyWrapper<Int>
  
  @MyWrapper
  public var foo: Int = 2
}

And if instead the access level of _foo were public, it would still work and the backing storage would be visible publicly.

• • •

The whole todo about dollar signs and projections strikes me as the wrong direction entirely.

So far, the only compelling use-case I have seen is in SwiftUI, for establishing a Binding. However, it seems to me that what a Binding really needs, is a mutable reference to a property.

Moreover, a mutable reference to a property is a generally useful thing in a programming language. So, if we need them for SwiftUI, they are also generally useful, then we should introduce a way to spell them.

Thus, if we want the syntax $foo to work for establishing a Binding, then I would greatly prefer that the way it works is by having $foo represent a mutable reference to foo, which is then wrapped in a Binding by whichever SwiftUI API it is passed off to.

4 Likes

That makes the whole story about wrapperValue even worse and more restrictive. If $foo means a mutable reference to foo then you have to expose the wrapper type for the backing storage that is in use or otherwise how would the compiler know what to do? Not only that, who says that wrapperValue or even wrappedValue should be mutable? It is maybe a thing for Bindable but it's not a general concept.

I am saying that “wrapperValue” should not exist as a concept. There should just be the property itself, and its backing storage.

If the programmer wants more views and projections, they can write them manually just like anywhere else.

You can already make mutable reference to a variable or property, you just have to jump through hoops capturing it in two closures, one the “getter” and one the “setter”.

If the main property (eg. foo) is an Int, then $foo is a mutable reference to an Int. No other type needs to be visible.

It would be a compile-time error to create a mutable reference to something immutable.

I strongly disagree. Mutable references are definitely a general concept, not specific to any one library.

1 Like

Are you suggesting that the user should write $foo himself and route it to the _foo.wrapperValue? I'm not sure I understood what you said. :thinking:

I am saying that “$foo” should only and always mean “a mutable reference to foo”.

1 Like