Pitch #2: Extend Property Wrappers to Function and Closure Parameters

Looks brilliant, nice work folks

3 Likes

... not all "guys".

We strive to build an inclusive Swift community here, which means trying to avoid exclusionary terminology. While "you guys" may have historically been considered gender-neutral, it is not considered to be not gender-inclusive now and we should avoid it. There are numerous other ways to refer to more than one person without resorting to "guys".

[EDIT: Provided more context for my correction.]
[EDIT #2: Edit missed a ']'.]

Doug

26 Likes

I can't think of any particular ambiguity. It is a little bit interesting for trailing closures, because the { would no longer come immediately after the previous argument. I don't think it's a technical limitation---the parser can do arbitrarily lookahead past the attributes---but visually I think it's much harder to associate the trailing closure with the prior call if there were attributes between.

Doug

1 Like

I'm a bit confused by these 'attribute applied to the entire closure' examples. In this thread (unless I've overlooked something) we've discussed two kinds of attributes: 'type' attributes and 'declaration' attributes. I thought that @convention(c) was considered a type attribute, so the proper way to spell this would be:

let callback: @convention(c) () -> Void = { ... }

or

let callback = { ... } as @convention(c) () -> Void

Even broadening the discussion past the @convention(...) attribute, I'm not sure what the suggested placement of the 'entire closure' attribute is supposed to communicate. Whether the attribute is placed before or after the opening brace, it doesn't seem to fall into either of 'type' or 'declaration' buckets—it looks to me like a new kind of attribute, which labels a particular expression rather than a type or declaration.

Also, regarding trailing closure expressions, we can already write:

func foo(_: @convention(c) () -> Void) {}

foo {
    print("Passed as C function pointer")
}

What does the 'expression attribute' buy us?

2 Likes

+1 from me, it's a good idea.

While we're at it, here's another thing I would like to be able to do:

@propertyWrapper
struct Queued<A> {

let wrappedValue : (A) -> Void
    
    init(wrappedValue: @escaping (A) -> Void, queue: DispatchQueue) {
        self.wrappedValue = {a in queue.async{wrappedValue(a)}}
    }

}

struct Bar {

@Queued(queue: .main)
func foo(arg: Int){...} 
//Error: Property wrapper attribute
//'Queued' can only be applied to a property

@Queued(queue: .main)
var bar : (Int) -> Void = {print($0)} //works right now

}

4 Likes

Now that I think about it, in the absence of contextual information (outside function argument), they should look more like:

@declAttribute var foo: @typeAttribute = { ... }
2 Likes

One question that occurred to me: would this pitch allow for specifying a property wrapper on a function parameter in a protocol?

E.g.,

protocol Foo {
  func bar(@Wrapper arg: Int)
}

I assuming the answer is “no” since we don’t allow wrappers on protocol property requirements.

Also, with respect to the API resilience section, I was under the impression that that section was intended to answer questions like “is adding/removing a wrapper to/from a public function’s parameter an API breaking change?”—is that a misunderstanding on my part? If not, would the authors mind elaborating on that point a bit (or pointing out if I’ve missed something already mentioned in this thread)?

One question that occurred to me: would this pitch allow for specifying a property wrapper on a function parameter in a protocol?
I assuming the answer is “no” since we don’t allow wrappers on protocol property requirements.

Yes, your assumption is correct. I'll make this explicit in the proposal.

Also, with respect to the API resilience section, I was under the impression that that section was intended to answer questions like “is adding/removing a wrapper to/from a public function’s parameter an API breaking change?”

Yes, you're right about this, and adding/removing a wrapper to/from a public function parameter is not a resilient change. I changed this section in the PR but I forgot to update the text in this post- sorry about that! Here's what the section looks like:

Effect on API resilience

This proposal introduces the need for property wrapper custom attributes to become part of public API. A property wrapper applied to a function parameter changes the type of that parameter in the ABI, and it changes the way that function callers are compiled to pass an argument of that type. Thus, adding or removing a property wrapper on a public function parameter is an ABI-breaking change.

2 Likes

Ah, should have checked the linked document. Thank you for reiterating that!

Asked and answered up-thread!

Having thought on this a bit more, I'm curious if there's a compelling reason for this to be the case. The current proposal says that a function declared as:

func reportProgress(
  @Percentage of progress: Int
) { ... }

reportProgress(of: 50)

gets implicitly transformed to:

func reportProgress(of _progress: Percentage) {

  var progress: Int {
    get { _progress.wrappedValue }
  }
  ...
}

reportProgress(of: Percentage(wrappedValue: 50))

However, I don't right off the bat see why the caller-side transformation is necessary. I.e., why couldn't the transformation of the body of reportProgress take the form:

func reportProgress(of progress: Percentage) {
  let _progress = Progress(wrappedValue: progress)
  var progress: Int {
    get { _progress.wrappedValue }
  }
  ...
}

leaving the call reportProgress(of: 50) unchanged? AFAICT this would make the addition of a parameter wrapper ABI- (and API-) compatible, but I don't know if I'm overlooking potential wins from the current setup.

As a (potentially related) aside, would the injection of the wrapper type into the calling context have the ability to affect the inferred type of the calling expression? I can imagine a somewhat pathological example, again using the Percentage example from the post:

@propertyWrapper
struct Percentage {
  init(wrappedValue: Double) { ... } // Note: not `Int`!

  var wrappedValue: Int {
    get { ... }
    set { ... }
  }
}

func reportProgress(
  @Percentage of progress: Int
) { ... }

reportProgress(of: 50)

Again, under the proposed rules, the call to reportProgress becomes:

reportProgress(of: Percentage(wrappedValue: 50))

If the pre-transformation code compiles (does it?), a literal which by all appearances has type Int (an integer literal passed to an argument of type Int) is instead inferred to have type Double!

Pure pedantry

If the proposed semantics of this transformation as written are desired, would it be more apt to refer to this feature extension as "function argument wrappers" rather than "function parameter wrappers"?

1 Like

Your question was answered here:

But perhaps it could be added to the Alternatives Considered section.

2 Likes

Oop, thanks Ben. Checked alternatives considered but didn’t check through the thread this time :man_facepalming:. Still curious whether it’s desirable for the wrappedValue overloads to influence the type of the passed argument in potentially non-obvious ways.

Still curious whether it’s desirable for the wrappedValue overloads to influence the type of the passed argument in potentially non-obvious ways.

Overload resolution can't influence the type of the wrappedValue argument in an unexpected way, because it's an error to have a wrappedValue parameter type that isn't compatible with the type of var wrappedValue in the property wrapper. autoclosures of compatible types are allowed, though.

I don't think the need for this overload resolution behavior will be super common, but I do think it's reasonable to want. Personally, I think the most compelling argument for applying the property wrapper to the argument at the call-site is the ability to initialize the wrapper differently, e.g. through a projected value or by passing the backing wrapper directly.

If the proposed semantics of this transformation as written are desired, would it be more apt to refer to this feature extension as "function argument wrappers" rather than "function parameter wrappers"?

FWIW, here's the terminology I've been using: You attach a property wrapper custom attribute to a parameter declaration, and the property wrapper is applied to the argument at the call-site.

1 Like

But perhaps it could be added to the Alternatives Considered section.

Yes, I should (and will) add all of this to Alternatives Considered, thank you!

2 Likes

Ah, gotcha! That makes sense. In that case, if the argument expression is always supplied to a wrappedValue parameter with the same type as the un-transformed argument, I'm having a bit of trouble picturing what that this overload resolution behavior looks like. Whenever you add the item to Alternatives Considered, would you mind including an example to illustrate this behavior?

Yeah, I agree that's useful. Of course, if the author of a function with a wrapped parameter wants to provide that behavior, isn't it easy for them to supply a func reportProgress(of percentage: Percentage) overload? I'm slightly wary of whether it's worth forcing authors to introduce wrappers into their library's ABI when they could offer convenience overloads themselves, or, potentially, auto-generate such overloads with some future extension of this feature (such as, say, func reportProgress(public @Percentage of progress: Int)).

Initializing the wrapper differently is also necessary for the closure parameter design in this proposal. If (@Wrapper value: Int) -> Void means the function type is (Int) -> Void, then property wrapper parameters cannot be utilized to opt into property wrapper syntax for closures that take in wrapper types. The best you'd be able to do for the Binding example in the proposal/up-thread is declare a local property wrapper and initialize it out-of-line using the backing storage directly:

ForEach($shoppingItems) { binding in
  @Binding item: Item
  _item = binding
   
  TextField(item.name, $item.name)
}

I realized that this might not be obvious, but not all property wrappers support init(wrappedValue:). Changing the transformation to be callee-side would mean that this feature can never be used for property wrappers that need to be initialized differently.

1 Like

This example also caused me lots of confusion. In the next revision, it should be replaced by something realistic that hangs together logically (and if you can't find such a case, that should tell you something!).

Another point: since it complicates the declaration, IMO exposing this sort of parameter adjustment to users in APIs may only be valuable to the extent that it allows the API's doc comment summary to be written more simply, because the adjustments are implicit in the property wrapper. (e.g. in terms of the API on the wrapper). So when looking at these examples, IMO we should examine what happens to the doc comment summary.

Edit On further reflection, In the vast majority of cases, this sort of argument normalization is an anti-pattern in an API, because it turns detectable bugs in the calling code into silent misbehavior. In general, it is much better to put preconditions on parameters, which offer the API author a choice about how/if to respond to violations. I have good reasons to want more flexibility from property wrappers, but IMO this case is a poor motivator.

1 Like

I've been thinking about this, and especially the case when the property wrapper takes arguments which seems to be excluded here. Since logically speaking this annotation doesn't create a new property wrapper, it is more about telling Swift that "we want to use this argument as e.g. a @Binding", wouldn't the natural thing be to accept e.g. @Clamped but without the ability or necessity to supply arguments?

I mean, the Clamped<Int> was initialised elsewhere, and that is where the range was supplied. In the closure we simply say we want property wrapper syntax.

Hm, this is a good point. It does seem to me that at that point, expressing the argument as a wrapper is purely a convenience to the API author, with the API consumer suffering. You'd have an API declared as:

func foo(@Wrapper someArg: Int)

but which is unable to be called as foo(someInt) since the transformation will generate an invalid Wrapper(wrappedValue: someInt) call.

IMO it would be much clearer for API consumers if that were declared instead as:

func foo(someArg: Wrapper<Int>)

(This is especially the case if the someArg label were chosen specifically for an Int-type argument, with a name that was poorly suited for a Wrapper-type argument).

It feels somewhat analogous to me to the justification for the removal of var in argument declarations, (a la func foo(var someArg: Int)). Just like it's trivial for function authors to write var someArg = someArg at the top of their implementation in exchange for a clearer interface for readers, it's easy for function authors to do the dance to create a local wrapped variable in exchange for a clearer interface.

On the topic of passing a wrapper value directly to a wrapped argument, I'm curious whether you've thought much about what that would look like at the call site. Were you thinking you could just be able to call the function as:

foo(someArg: Wrapper<Int>.makeWrapper())

or would you need to do something like:

foo(_someArg: Wrapper<Int>.makeWrapper())

If the latter, would that be realized in the transformation by generating multiple foo overloads, or by some other mechanism?

It seems a bit strange to me that wrappers on properties synthesize private storage by default, but wrappers on function arguments would implicitly appear at the level of visibility of the function.

1 Like

I don't think I understand this part:

When there are multiple property-wrapper custom attributes on a single parameter (I assume that's legal from the language introducing the rules above), presumably the wrappers nest and there is a wrappedValue at each level. Can you explain how this item plays out in that scenario?

Edit: Also, I'm completely lost here. I hope you can help me.

OK as far as it goes, but I can't imagine what alternative you may have in mind…

application of a property wrapper type is only available within a closure expression.

Again, as opposed to where? Here's a declaration using a closure expression that I presume would be legal based on your example. It uses the @Percentage property wrapper on its parameter:

let inverted = { (@Percentage _ x: Int) -> Percentage in
  .init(percent: 100 - x.percent)
}

It seems obvious to me that you can't use the wrapper outside the closure expression, except to declare a new (wrapped) property.

That is, the signature of a function that contains a closure cannot include the property wrapper attibute. Instead the application of the attrbute will be up to the caller of the function, which supplies the closure argument.

After spending a long time trying to figure this out… I suspect maybe by “signature of a function that contains a closure” you don't mean “the signature of a function whose body contains a closure” but “the signature of a function that has a parameter of function type” and by “closure argument” you don't mean an “argument to a closure,” but a function parameter of function type. If so, this whole thing should be rephrased to be less ambiguous. A closure is a literal expression, not a type. Function arguments may or may not be closures but function parameters are not closures.

And even if I assume my suspected intent as detailed above, I'm still not sure what this all is trying to say. Please help, thanks!