SE-0293 (second review): Extend Property Wrappers to Function and Closure Parameters

I maintain that expanding property wrapper attributes to types, even if only in the parameter position, is out of scope for this proposal. What you're proposing is a bit of sugar on top of this proposal that comes with a lot of implementation complexity and downsides that haven't been fully considered yet. It also doesn't change anything about the base model of property wrappers on parameter declarations, so nothing is preventing someone from proposing this in the future.

The APIs you mention already exist today, because the only way to pass property wrappers around is to use their type directly. There are deprecation tools for APIs if a better solution comes along.

6 Likes

I can relate to your concerns.

1 Like

The four major changes in this version of the proposal together address essentially all of the major concerns voiced during the prior review, and they improve this proposal for the better. Overall, I think the design outlined here is about as intuitive as such a complex feature could get. It certainly now fits much better with the feel and direction of Swift.

I haven't thought up many use cases for it myself, but I can see the benefit for certain use cases justifying the addition of this feature; I haven't used other languages with a similar feature but I have certainly tried to pay close attention to this proposal from its earliest stages, and I put in a detailed study of this iteration of the proposal as well as all preceding feedback.

Again, fantastically well done and what a wonderful example of incorporating community feedback to strengthen a very useful and well thought-out proposal.

9 Likes

Would that proposal allow to use property wrappers in the constructors?

class Foo {
   let bar: Bar

   init(@Injected bar: Bar) {
      ...
   }
}

I'm thinking of making dependency injection solution using this feature.

Yes, the proposal enables applying property wrapper on parameters of functions –– aka global functions, initializers, instance methods, static functions, and subscripts (plus any function declarations I may have forgotten):

struct App {

  // Initializer
  init(@Asserted(.greaterOrEqual(1)) screens: Int) { ... }

  // Subscript
  subscript(@Asserted(.greaterOrEqual(1)) screen: Int) { ... }

}


struct LoginController {

  // Static function 
  static func checkAvailability(
    @Lowercased @Asserted(!.isEmpty) of username: String
  ) { ... }
 
}


// Global function
func loginController(@Clamped(0 ... 10) for index: Int) { ... }
2 Likes

I was made aware of this review in this post.

I love everything I see, except I am not comfortable lying or asking people to lie. We have to either

  1. Rename the feature to variable wrappers or similar.

or

  1. Redefine property to mean any variable or constant in any scope.

I don't think it's lying. @John_McCall points out in the thread you linked to that the property wrappers feature was "named after its most important use-case". Furthermore, as @hborla mentioned in that thread, property observers and computed properties also have "property" in their names besides not being exclusively meant to be used for properties.

I think a change in the documentation –– or even the compiler messages –– would be reasonable if it is agreed that action needs to be taken to address the property wrappers' name. Nevertheless, the proposed feature is not directly related to naming. Thus, if you think a change is required, it should probably be in a different proposal altogether.

3 Likes

I don't think the dishonesty is backed by malicious intent. I just think it doesn't matter to some of the people involved, enough to justify any work it would take to be correct. I'm not comfortable with that, but I can understand someone trying to rationalize around the falsity.

Too much of a time investment; I don't think it would be worthwhile. :slightly_frowning_face: There's two of you very intelligent people already who didn't read the little bit of documentation-quoting I did already on computed variables.

Please don't make assumptions like this about other participants on these forums. I did read your documentation quote. My point was that the feature is generally referred to with the term "property", as are other similar features that apply to all forms of named data, and the terminology is not new in Swift 5.4 or with this proposal. This phenomenon even exists in other languages. You can drop the term "property" or substitute it with "variable" when the feature is used in a different context, just like the documentation does.

Let's take any further discussion on this topic back to the other thread.

7 Likes

I am +1 for this proposal.

I think the rationale for having the wrapping done by the caller is well laid out and is the correct choice. The proposal also does not preclude a callee from accepting a passed-in value and wrapping it within the called method, so this proposal also allows for both approaches.

The concrete example in my own coding experience related to this proposal is the case of propagating bindings through a ForEach in SwiftUI. I believe the proposal addresses that case very well and will lead to more expressive code.

Considering that case as well as the other examples in the proposal, I think the problem addressed is signifiant enough to warrant the addition. I also believe the proposal does fit well with the feel and direction of Swift.

This is an intricate feature, but I think the proposal adds it to the language in a straightforward and consistent manner.

I think it will take a considerable amount of knowledge to use this feature well in libraries and frameworks, understanding property wrappers themselves and then the implications of using a property wrapper for a parameter. I think the cognitive load will be much less for clients of those APIs, where specific property wrappers are used and idiomatic use will likely be clear from example code or documentation, even without a deep knowledge of property wrappers or this feature.

I read the proposal and feedback for this review closely, but did not follow the previous review closely.

This proposal also seems to be a great example of the Swift Evolution process at work. I really appreciate the work done by the proposal authors and everyone who contributed to the reviews.

1 Like

+1 on this revision. Pre-concurrency use cases may not fully justify the effort, but this is changing. Now with actor concurrency on the horizon and its parameter requirements, it is going to be a very useful feature. Especially for library authors and API designers, particularly for transitory and adapter APIs.

How so? So far no additional property wrappers have been introduced as part of the concurrency effort, so where do you see this proposal helping in that regard?

Mostly just have a hunch about this. I was working on migrating some of my code and it seems l am going to be able to put this feature to good use. I am guessing Apple might use it in some future API, especially to ease transition of legacy code.

There's an example of how property wrappers on parameters may be useful for concurrency in the ConcurrentValue and @concurrent closures proposal draft.

3 Likes

This proposal is very well-written, and the future directions are very well-thought out. It's also a significant improvement from the previous revision.

Before I give my review, I would like to ask for some clarifications:

By default, unapplied references to functions that accept property-wrapped parameters use the wrapped-value type in the parameter list, and the compiler will generate a thunk to initialize the backing wrapper and call the function.

[...]

func log<Value>(@Traceable value: Value) { ... }

[...]

The compiler will generate a thunk when referencing log to take in the wrapped-value type and initialize the backing property wrapper. [...]

{ log(value: Traceable(wrappedValue: $0) }

If the user has already defined a function that takes the entire wrapper storage

func log<Value>(Traceable<Value>) { ... }

(e.g. an existing API like those in SwiftUI that take Binding arguments), would the compiler consider it as an erroneous redeclaration, or does the compiler have a way to work around it to help with migration from older APIs to newer ones that use property wrapper-annotated parameters?


In addition, I would like to add another reason for not supporting passing a property-wrapper storage instance directly: It's ambiguous when the wrapper and wrapped types are the same.

@propertyWrapper
struct Foo { /* ... */ }
func bar(@Foo foo: Foo) { /* ... */ }
let foo = Foo( /* ... */ )
bar(foo) // treat foo as a wrapped value or wrapper storage?

A very much delayed response to 2 posts in the latest pitch thread

I didn't read the proposal in full at the time, and misunderstood the type signature of functions that use property wrapper-annotated parameters. Now that I've finally read the proposal fully, I see that there is nothing dangerous with the function signature. Sorry for this much delayed response.

I thought about trying to make this kind of overloading work, but I decided against it. That said, API authors could exploit the fact that the function you wrote above has the same ABI as one that takes in an @Traceable wrapped parameter and do something like this:

  • Write a new function that takes in Traceable<Value> and an extra parameter with a defaulted argument. Deprecate this function with a message to use the function with the property wrapper.
  • Change the original function to use a property wrapper parameter.

This shouldn't break ABI, and clients will get the deprecation message to use the new function with property wrapper syntax instead.

The migration path for a function that takes in a wrapped value is easier - deprecate the function and declare a new one with a property wrapper on the parameter.

3 Likes

I'm interpreting this to mean that existing compiled clients would call into the wrapped-parameter version of the function, passing the storage directly. Is that accurate?

If so, is this an officially-supported promise of the proposal? I.e., does the proposal guarantee that a function func foo(@Wrapper arg: Int) will have an identical ABI to a function func foo(arg: Wrapper<Int>)? I think it would be helpful to spell this out explicitly in the proposal text (perhaps along with your deprecation guide) to inform users who will want to convert wrapper-typed APIs to use parameter wrappers in an ABI-compatible way. :slightly_smiling_face:

Yes, that's right. Of course, this should be done with caution because of all the reasons we've outlined in the proposal for why callers shouldn't pass the backing storage directly (e.g. don't do this and put arguments in the wrapper attribute) :slightly_smiling_face: but I think it's reasonable to want to do this in the case where the backing storage is the projection, or if the only way to initialize the backing wrapper is through an init(wrappedValue:) with no additional parameters and call-sites are just doing that manually.

Yes - the parameter (@Wrapper arg: Int) is replaced with one that looks like (arg _arg: Wrapper<Int>) before the function is emitted in SILGen (which is specified in the Function body semantics section).

Good idea - I can expand the API resilience section and it would probably also be good to put this information in the library evolution document (if accepted)

4 Likes

Could you provide the rationale for deciding against it? Is it in some way related to not allowing passing a property-wrapper storage instance directly?

This feels like a hack. I think it is only a minor pain, but would be better if the migration path is more straightforward.


I like this proposal very much. It's very well done, and opens a door for many other desirable features e.g. property-wrapper parameters in memberwise initializers. I also find it very impressive by how much it has improved from its previous revision.

Yes. Although the proposal itself is somewhat a sugar, it really makes it a lot easier to work with property wrappers in functions. And more importantly, it serves as a first step for many future improvements to the language.

Yes. I do find the migration path for existing unapplied function references somewhat of a hack, but it is only a minor problem in my opinion.

N/A

I read both revisions in full, and participated in both reviews.

Thank you to everyone for the feedback on this proposal. This is making great progress, but the core team has return it for further refinements and discussion.

-Chris

2 Likes