SE-0293: Extend Property Wrappers to Function and Closure Parameters

I don’t have the time to dive deep and write a proper review for this, but from a quick read of the proposal and a skim of this thread, it seems to me that the main motivation behind the proposal is to enable this:

struct MyView: View {
  @State private var shoppingItems: [Item]

  var body: some View {
    ForEach($shoppingItems) { (@Binding item) in
      TextField(item.name, $item.name)
    }
  }
}

However, the complexity introduced around call site transformations and unapplied references doesn’t actually seem to be strictly related to that motivation. Can’t this be introduced in a way where the only transformation that happens is inside the function/closure, and where everything else is left as is? Essentially, make this less parameter wrappers and more argument wrappers, as @Jumhyn suggested. And if parameter wrappers with call site transformation are an independently important feature, they can be handled as a follow-up proposal with an independent motivation. (Apologies if this direction has already been explored somewhere – I haven’t had the time to read everything in detail.)

Ah, interesting—that's a super useful callout. IMO, this is the inverse (converse?) of the current story for property wrappers. That is, declaring a property as @Wrapper foo: Int is done when you want to expose an API that traffics in Int, but has behavior governed by Wrapper. Clearly projectedValue complicates that story a bit, but the core of the motivation for property wrappers is to replace the pattern of:

private var _fooStorage: Int
public var foo: Int {
  get { getMagic(_fooStorage) }
  set { _fooStorage = setMagic(newValue) }
}

From the perspective you're advancing, the motivation does feel a bit murky to me. In the Memberwise initialization section, issues with the status quo are presented as:

However, this can take flexibility away from the call-site if the property wrapper has other init overloads, because the call-site cannot choose a different initializer. Further, if the property wrapper is explicitly initialized via init(), then the memberwise initializer will choose the backing-wrapper type, even if the wrapper supports init(wrappedValue:). This results in unnecessary boilerplate at call-sites that do want to use init(wrappedValue:).

But these two separate issues pull us in different directions. The proposal acknowledges that some library authors do want to create APIs that traffic in the wrapped type, and presents argument/parameter wrappers as a solution. And, indeed, by most appearances, declaring a function with a wrapped parameter does create an API trafficking in the wrapped type. Yet you're saying here that this feature should not be used to create such APIs. I think this creates a confusing message for library authors that makes the feature liable for misuse.

If we don't want authors to use this feature to create API that traffics in the wrapped type, then IMO the spelling at the function declaration site needs to change. That is, the declaration site should simply have the signature they do now:

func foo(arg: Wrapper<Int>)

and we should focus on effort solely on improving the call site, perhaps with your idea from the pitch thread of an appropriate $ prefix:

foo($arg: 0) // gives the '(Int) -> Void' version of the function via `init(wrappedValue:)`

Then, @Lantua's suggestion from the pitch thread becomes the natural closure analog, as you pointed out above:

    ForEach($shoppingItems) { $item in
      TextField(item.name, $item.name)
    }

The advantages I see for this approach on the function side are numerous:

  • There's no confusion on the part of the library author about what sort values may be passed in: it's clear that any value of Wrapper<Int> may be supplied by clients, not just separate instances initialized via init(wrappedValue:).
  • It is not possible for library authors to misuse this feature to create APIs with traffic in the wrapped type rather than the wrapper type.
  • It's clear to the library author what the true type of the parameter/argument is, and there's no risk of confusion about the compatibility story when adding/removing a 'wrapper'.
  • At the call site, it's clear that there's something 'special' going on compared to a normal function call.

We could also add the ability to do something like:

func foo($arg: Wrapper<Int>) { ... }

or

func foo(arg $arg: Wrapper<Int>) { ... }

to enable the function body sugar for library authors. (Although, I'm overall less concerned about requiring library authors to do the dance internally to properly 'wrap' the parameter, just as we require authors to do the var arg = arg dance to get a mutable version of a parameter).

ETA: I happen to think that the feature-as-proposed is also useful, but only if creating APIs which traffic in the wrapped type is intended to be a supported use case. If that's considered an anti-pattern under this proposal, then IMO the proposed solution misses the mark.

4 Likes

I don't think so; let me elaborate. The proposal in the section you're referring to doesn't really talk about API authors. It mainly addresses 'regular' language users –– API consumers, not authors. What Holly is referring to, though, is API authors, who as she says should use the proposed feature only in specific cases ("If the API author only wants the programmer to pass the wrapped type, they should not put a property wrapper attribute on the parameter.").

The way I see it, there are two parts of this proposal: the API-consumer one and the API-author one. The API-consumer part is wrapper application on function parameters and the latter is wrapper application in closure parameters. The reason behind this is simple: authors tend to use closures in their APIs (see ForEach, for instance), whereas consumers don't declare functions with closure parameters. The other main distinction is that when used by consumers, functions don't require very strict checking, since they're used within the same codebase, and often in the same type declaration as well. However, API authors are expected to very carefully select which customization points are available to the user and which are not.

Sorry for all these abstract ideas, but I think they're important to understand the important distinction between use by authors and consumers. All in all, that Holly put it very well, when authors look at the proposal they should think:

  1. You should use them however you like, but we expect cases involving a closure that takes a value with non read-only semantics to be most common.

For API consumers, though, interaction with the proposed feature will probably be very different:

  1. Property wrappers on function parameters will be used more often; probably for passing a property with different semantics (such as Binding) or for applying a custom behavior to the value passed on function call (such as Lowercased) –– with the latter being more similar to current property-wrapper use.

  2. Property wrappers, however, will rarely be used on closure parameters.

1 Like

Could you elaborate on the "complexity" these transformations introduce? Personally, I find them very straightforward. As for unapplied references, I think it's discussed thoroughly throughout the thread; for feel free, though, to ask for any clarifications.

No worries at all, this discussion has been extremely helpful in my understanding of the feature.

If this feature relies on API authors reading all the minutiae of the proposal in order to use it correctly, then that's a flaw, IMO. We should strive for a design which naturally guides authors towards correct usage.

Under this proposal, when I declare a parameter as @Wrapper arg: Int, I've created a parameter which a) is declared to have type Int, and b) can be passed arguments of type Int. That a library author sees this and creates a wrapped-argument API intended to traffic only in Int (and not Wrapper) should not, IMO, be considered a failure of the author to properly understand the feature—it's a failure of the feature design to make misuse difficult.

This seems... a bit too black-and-white to me. I'm mostly on the "API consumer" side of things in my daily work, but I've defined plenty of functions with closure parameters. Also, even an API consumer often wears both hats—defining APIs which are used by other components of the same app/project, even though it's all owned by one team. I don't think it's possible to make the distinction between consumer/author as cleanly as you're attempting to.

Half-serious question: if the function-parameter-wrapper feature isn't meant for use by API authors, why not ban the use of wrapper attributes in public functions? Then there would be no concern of misuse by API authors, and they would be free to provide a 'convenience' entry point which accepts the wrapped parameter type and forwards it appropriately.

2 Likes

You don't need to apologize! Communication is a two-way street. We're accomplices :smiling_imp:.


I'll make use of the scenarios I laid out earlier.

Even with this notion, it's not a clean separation between function vs closure. It's been trivial to refactor closures into functions (scenario 3):

ForEach(...) { (input) in ... }

// refactored to
func itemContent(input: ...) { ... }
ForEach(..., content: itemContent)

It is especially common where refactoring is encouraged, like in a SwiftUI environment. So the consumer side consists of both functions and closures, not just closure. With that dichotomy, we need features that work with functions (author side) or ones that work with both functions and closures (consumer side).


That's why I want to change the problem statement to better reflect regular usage. We can likely agree that the overarching problem is, "property wrappers are common and we want to pass them around". Let's look into existing wrappers.

Looking into SwiftUI and Github, I categorize them into four categories:

  • Shared-storage wrappers use some mechanisms to share the wrapped values between instances, e.g., Binding, UnsafePointer.
  • Utility wrappers adjust the wrapped values to maintain certain invariances, e.g., Lowercased.
  • Entity-bounded wrappers are tied to class/struct they're declared in and are not meant to be passed around as is, e.g., Published, Environment.
  • Proxied wrappers are entity-bound wrappers that vent out proxies when they are passed around, e.g., State. For SwiftUI, the currency proxy is `Binding

It's easy to see that the shared-storage and proxied wrappers, as @Jumhyn put it, traffic in the wrapper type. OTOH, the utility wrappers traffic in the wrapped type (scenario 2) as the caller doesn't need to configure them and they could be regarded as an implementation details at an API level. What dictates the traffic type is the category that these wrappers are in, not where they're used.

From this, I conclude that the decision of whether a parameter traffics in the wrapper type or the wrapped type should depend on the wrapper author (scenario 4). The best way to achieve that is to have the decision depends on the type of the wrapper itself (e.g. Binding traffics in Self, Lowercased traffics in String) regardless of where they are. This is different from the current idea that the wrappers in functions and closures traffic in different types (scenarios 2, 3 and 5).

Wouldn't you agree with this assessment, @hborla, @filip-sakel?

PS

Categories of some of the wrappers (hidden since it's a little long).

* Namespace could technically be a proxy wrapper.
** Some wrappers are not proxied as is but could use Binding. They might as well stay as entity-bound wrappers, with proxy potential.


If you agree, I propose slightly different rules:

  • Wrappers with init(wrappedValue:) are passed-by-wrapped,
  • Wrappers without init(wrappedValue:) are passed-by-wrapper,
  • The ABI always use wrapper, and
  • Unapplied method reference is created with a boilerplate so that bar1 and bar2 below have the same type (scenario 5):
    func foo(@Wrapper a: ...) { ... }
    
    let bar1 = foo
    let bar2 = { foo($0) }
    


If you disagree, I still have some comments left on the old problem statement. In particular,

I that case, I think we should not use init(wrappedValue:) at all even if the wrapper has it. Instead, we could add init(argumentValue:) in the future (though that could easily increase boilerplate).


I don't think we need too much discussion on section Mutability of composed wrappedValue accessor. Property wrapper already has a notion of mutation (you can't mutate property wrapper on struct if the struct is not mutating). We could easily say that the argument wrappers are immutable (and the inout argument wrappers are mutable).


(scenario 1) is problematic for Property-wrapper parameters in synthesized memberwise initializers.


Sorry for a lotta noise. The implication of this feature can be far-reaching and will easily ripple throughout other wrapper-related features. I wanna make sure we get it to the best shape possible.

I'm pretty sure I've said all that I want to say. Imma go hibernate now :bear::zzz:.

6 Likes

I really want to underline this point. I have put in a nontrivial amount of study into the proposed feature as designed, and my impression of it basically boils down to this: To the extent that the feature as proposed happens to align with what @Jumhyn spells out above (and indeed, in the simplest use cases, they do align), the proposed behavior makes sense to me. To the extent that the feature as proposed does not align with the above, the proposed behavior is comprehensible to me only with significant effort. While I can largely see how the motivation discussed in this forum leads to the end result, since the proposed design sometimes runs counter to users' legitimate intuition as @Jumhyn spells out above, it undermines usability even in straightforward scenarios because one can't be confident when the feature behaves intuitively or not.

7 Likes

You're right.

I see the confusion that can be caused to library authors.

You're right it's not as clear-cut as I presented it.

I think that would create too much confusion around this feature. Not to mention that –– at least to my knowledge –– no other feature, except for member-wise initializers, uses access control as a way to prevent use by library authors.

1 Like

I like your categorization, it gives us something more tangible to work with!

Agreed.

Originally, it was our intention to allow this, but seeing the complexity it introduced, we decided to leave it out of the proposal. I do think, though, that some form of differentiation between the above, passed-by-wrapped and these passed-by-wrapper cases is required. This differentiation could be tackled by a future proposal that enables the latter case, with the following potential syntax:

extension Binding {
  
  init(projectedValue: Self) {
    self = projectedValue 
  }

}


func toggleLights(@Binding lightsAreOn: Bool) {
  //                       ^ ~~~~~~~~~
  // The name implies a boolean value, not a binding. 
  lightsAreOn.toggle()
}

toggleLights($lightsAreOn: $livingRoomLightsAreOn)
//           ^~~~~~~~~~~~
// The dollar sign ($) implies use of the projected value,
// and thus 'Binding'. 

I think this would be reasonable.

Do you find @Lantua's rules intuitive? If so, I think they'd be reasonable as a goal for this and future, related proposals.

Overall, I think the rules outlined do make sense. They will not, though, be what this proposal will leave us with. That is, I think that non-init(wrappedValue:) wrappers and their handling of unapplied function references deserve their own discussion. On the other hand, what will change if we chose to work towards this rule is the handling of unapplied references and probably the ABI of functions that utilize the proposed feature. Unapplied references may also have to be left out of this proposal. As for ABI, it will probably need to change.

1 Like

I certainly agree with your agreement with @Lantua’s first point! I also agree with you that @Lantua’s second point requires some syntax to indicate what’s going on at the point of use. I am not sure about the ABI, but in any case it isn’t generally a user-facing issue. I agree that unapplied references could be left out of this proposal for further discussion.

2 Likes

I agree that such a rule would be confusing. The rhetorical point I was making with the non-serious half of the question was that based on what’s been discussed in this thread, there is already an implicit policy of “using function argument wrappers on public functions is probably inappropriate.” The fact that this proposal doesn’t strictly enforce the policy doesn’t make the rule any less confusing, IMO.

This is a good point—it’s weird to have feature availability predicated on access control. The existing pattern we have is to control the exposure of such features (e.g. make the synthesized memberwise init internal, make the synthesized backing storage for wrapped properties private, etc.). So I guess a closer analogue would be to allow wrappers on parameters of public functions, but only expose the Wrapper version of the function across module boundaries.

Like I said, though, I don’t ultimately think this results in a super usable model—it would just make the model proposed here (if I’ve understood everything correctly :sweat_smile:) more explicit in the language rules rather than relying on ‘soft’ enforcement (i.e., endless explanations on StackOverflow/#swift-users to the effect of “you’re using the feature wrong”).

1 Like

The current version of the proposal doesn't give a lot of control to the API author in that regard and, yes, it can be misused by authors more easily.

I guess that would be a more accurate analogy.

1 Like

Let's rename it to pass-by-storage and pass-by-wrapped, so that we don't need to squint our eyes on every line ;)

Praise be. I don't expect the whole shebang from a single proposal anyhow. I just want to make sure that we end up in a relatively good position and direction, and that we don't paint ourselves into a corner.

That may be for the best. We could even add a fix-it for unapplied references:

// Error
let a = foo

// Suggested fix-it
let a = { foo($0, $1, $2, ...) }

That should be what most developers expect if not a little more verbose than necessary. AFAICT, it is unprecedented to have a function that's not a first-class citizen (we can't use unapplied reference), but it looks fixable in the fullness of time. I'd be more worried if we end up in a bad spot by choosing the wrong behaviour.

:thinking: You got me an interesting idea, since we can have the function author select the appropriate argument with that:

func foo(@Wrapper a: Int) // Pass by wrapped
func foo(@Wrapper $a: Int) // Passed by storage

We can even extend it to closures:

{ (@Binding $a) in } // Passed by storage
{ (@Lowercased a) in } // Passed by wrapped

That does give the author full control of traffic type at the call site.


Maybe this proposal can instead be more geared toward pass-by-wrapped, everywhere. So the ForEach example would need to change from Binding to Lowercased:

// pass-by-wrapped, uses `[String]` instead of `[Lowercased]`.
ForEach(...) { (@Lowercased a) in }

That would be easier to grasp with

Binding doesn't work yet, but Lowercased works anywhere.

instead of

Binding works here, does not work there*. The converse is true about Lowercased.

2 Likes

Yes. When you write a wrapper attribute on an instance property, the backing wrapper type stays private (unless the generated memberwise initializer uses the backing wrapper type, in which case the backing wrapper is exposed as internal). When you write a wrapper attribute on a parameter, the backing wrapper type is exposed through the type of the function. It's already been heavily noted that this is a point of concern.

My argument is that the generated memberwise initializer is a source of complexity for property wrapper initialization, and its behavior should change. The intention is to change the memberwise initializer to use this feature, but first we need a way to pass the backing wrapper directly. So, for scenario 1:

This is intentional. I think generated memberwise initializers should always take the backing wrapper type, but still allow the call-site to use the init(wrappedValue:) shorthand initialization.

This isn't the motivating use case. The use case is for ForEach to be able to vend a binding directly to a collection element in the original data source to use in the closure body. The property wrapper attribute in the closure body is so the user can opt into the nice property wrapper syntax instead of having to access .wrappedValue and .projectedValue manually.

If you want to create a new property wrapper from the closure parameter (which will be passed from ForEach), you can use a local property wrapper. e.g.:

// array is [String]
ForEach(array) { a in
  @Lowercased var v = a
}

Per the proposal:

This can be lifted when we allow a special calling syntax to pass the backing wrapper directly. That said, this does prevent a valuable use case that you mentioned, which is factoring a closure into a function to be reused.

Yes they do. If they want users to only pass the wrapped value, they can write:

func foo(a: String) { ... }

Like I mentioned before, the property wrapper attribute is a replacement for:

func foo(a: Lowercased) { ... }

in order to provide the call-site a convenient way to initialize Lowercased, or pass an existing one.

I see the single expression closure case as a way to reference a property wrapper parameter function and make it take in the wrapped value type. This is also the way that you preserve default arguments when you want to reference the unapplied function. I agree that this isn't great, and it would be better if there was a way to reference the function while preserving information that's attached to parameter declarations.

I've been thinking more about the use case where the library author wants to do some kind of argument normalization or assertions on the values that are passed (which is one of the only use cases I can think of where the library author would only want the call-site to pass a wrapped value), and I think this use case might be better served by a function wrapper feature rather than a property wrapper on the parameter. A function wrapper feature wouldn't change the way the function is called at all and it wouldn't change the type of the function, but rather wrap the function itself in some pre- and post-call code that's defined by the wrapper. For example, you might imagine something like:

@Precondition { start <= end }
func scan(start: Int, end: Int) {
  ... 
}

which would assert start <= end before the scan function is called.

Your "call-site only" suggestion is interesting because the spelling at the parameter declaration of @Wrapper arg: Int makes the most sense for users in understanding how to call the function. If the $ syntax is adopted for initializing a backing wrapper for a property-wrapped instance property, this extends naturally to using $ to pass the backing wrapper directly to a property-wrapped parameter. However, I do understand the possibility for API authors to misuse the feature.

1 Like

This would be neat! I agree that this behavior more closely captures the 'traffics-in-wrapped-type' APIs that are not meant to be enabled by this feature.

Given our discussion, though, I'm not sure that I really consider this a feature. If argument wrappers are only intended to be used when you would normally pass the storage type, why is it a 'win' to make the call and declaration sites look as though they are accepting the wrapped type? Passing the wrapped type is not really "how to call the function" if the function is actually meant to traffic in the storage type—it's just a sugar to make initializing the storage type more convenient.

The more I think about this, the more I wonder if it's dangerously close to allowing the expression of certain implicit conversions (from the wrapped type to the storage type) in function argument position. That's why I think there's a lot of value in having some sort of signifier at the call site that something weird is going on.

Right, the $ syntax can really only have one meaning in this position, and it's certainly not obvious that my proposed meaning is the 'correct' one. I'm also not 100% sold in general on overloading $identifier to mean "insert some sort of property-wrapper magic here," but it does seem better than any alternative syntaxes I've seen suggested or come up with myself... :thinking: It's certainly a benefit that many more novice users who have never defined a property wrapper themselves likely already think of $ as being some sort of magical property wrapper operator (and indeed, I've seen it described as such even in official documentation!).

1 Like

I consider this a 'win' because it fits into the existing property wrapper initialization model (which IMO has issues, and I would probably design it differently if that were a possibility, but this is what we have to work with). Assigning a wrapped property to a wrapped value at the declaration or in init is not really "how to initialize the property". It's really initializing the backing wrapper, and the wrapped property itself is computed. It's really, really important to understand how this works because it impacts which code from the wrapper is actually called between init(wrappedValue:) and the wrappedValue setter. With parameters, at least there aren't two different things that could be happening (at the call-site, at least), but you still do need to understand that you're initializing a property wrapper with that line of code.

I get that this is kind of a crappy argument ("this is how it's always been, so it's okay!" :neutral_face:). I do like the idea to have some kind of syntax to make it obvious that property wrapper initialization is happening, but I don't think it's a good idea to have such a syntax for parameters and a different syntax (the regular syntax) for properties. Consistency with the existing design is equally important, in my opinion.

I do wish there were more semantics attached to $ for property wrappers. I think this would allow us to enforce more for property wrappers on parameters, and it might address some of the concerns in this thread, like the access control problem. For example, if $ is used as a mechanism to publicly expose the backing wrapper (which is super common), we might want to only allow wrapper attributes on parameters if the wrapper exposes itself publicly via $. This would also make the most sense for the $ calling syntax in order to not conflate the backing wrapper with the projection, unless they really mean the same thing.

1 Like

That is the point of scenarios 2 (and 3). The function argument wrapper caters to the utility wrapper, while the closure parameter wrapper caters to the (shared-) storage wrapper. It feels a lot more like two halves of two different features trying to make a single whole.

I deem this undesirable (and ignited this whole adventure ;). If we look at the function-closure pairs:

// Matching call-site
func foo1(@Wrapper a: Value) { ... }
let foo2 = { (a: Value) in
  @Wrapper var a = a
}

// Matching unapplied function reference
func foo3(a aTmp: Wrapper<Value>) {
  @Wrapper a: Value
  _a = aTmp
}
let foo4 = { (@Wrapper a: Int) in ... }

These parings/transformations hardly make senses. This is not a mental gymnastic I want to do on a regular basis.

It'd be much better if we have a simpler pairing:

// Matching call-site & UFR, whatever they are
func foo5(@Wrapper a: Value) { ... }
let foo6 = { (@Wrapper a: Value) in }

This feature could meet that criterion with some adjustment.

They sure can, but this feature aims to make it convenient. Right now I don't think there's any wrapper that would benefit from the function argument syntax. Storage wrappers surely don't, and I don't think utility wrappers do either (with the unapplied reference shenanigan).

Why is that desirable? Shouldn't UFR simply be a way to stow away functions for later usage? Why should it allow for more expressivity?


Maybe we can add support for those that are pass-by-storage first (e.g. proxy wrappers). They bypass the initialization process anyhow, and it should be common due to the currency proxy Binding. If the rule is:

  • Always pass-by-storage, and don't generate init(wrappedValue:) calling.

It would make function and closure much more consistent, make this feature look more like a single whole, and leave us with enough room for iteration if we want to add support for pass-by-wrapped (in both functions and closures).


On an overarching story, I think it's best if the decision on argument passing is done in three steps:

  • The wrapper author decides the supported traffic types via init.
  • The function author decides the exact type of init allowed for a particular argument. This argument can only use init(wrappedValue:), for example. We can put this in the function name: foo(a:) vs foo($a:), or just use annotations.
  • The user chooses, among the allowed init, the initializer to be used via type checking.

I think this should work pretty well.

The second step is quite crucial since I don't want to discount the possibility that a wrapper type can well support multiple traffic types (though I couldn't find any). It also solves the open-world problem too; when someone added more supported traffic types, causing ambiguity at the call site. If we include step 2, it'd be great if we can have uniform convention across functions, closures, subscripts, etc.

I'm totally with you until we run up against what is still the major sticking point for me: public functions. Neither of the issues you identify here are relevant across module boundaries. Direct initialization of a stored property is particularly straightforward, IMO, since it can only happen within the type declaration itself, and is immediately associated with the @Wrapper attribute.

But even property initialization in init always has to happen in-module (since initializers defined in out-of-module extensions have to delegate to an in-module initializer before accessing self). So we simply don't have a case today where library clients have to worry about the init(wrappedValue:)/wrappedValue setter distinction with regards to library types which have wrapped properties. The backing storage can basically be considered an implementation detail (modulo any API vended via projectedValue of course...).

Is this meant to be an argument in favor of the as-proposed call behavior, or am I misunderstanding? In my view, the proposal falls short on this point (hence my suggested explicit syntax for pass-by-wrapped calls).

I think I'm less troubled by this than you. I've mentioned above why I think the situation with direct initialization is unconcerning to me, but I'm also not worried about differences with the current situation for init arguments. If a type with some @Wrapped var foo: Int property exposes an init which takes a parameter foo: Int, why is it not an implementation detail that this init happens to create a new instance of Wrapped within its body? I could easily modify my type to drop the @Wrapped attribute altogether and the init could remain the same.

To me, that's a very different situation than what this proposal gives us, where an entire initializer call is injected at the call site, and overload resolution, etc. can be influenced locally. This is already a significant departure from the status quo, IMO. We'd basically be introducing a limited version of C++'s converting constructors at function-argument boundaries.

This sounds nice, but is it achievable in practice? I think all we would really be able to tell is that the property wrapper type vends something with the same type as Self—whether we actually return self or some other instance would be an implementation detail. But maybe having the same type for projectedValue would be sufficient for most 'normal' cases? (As a not-too-far-fetched example, imagine a Bool wrapper which vends Wrapper(!self) via projectedValue.)

Nice to see this proposal progressing; thanks for all your hard work on it! I think I mostly understand it and it seems like a +1, but I have a few questions:

  1. The proposal sez:

    (comment mine). Where did that Int come from? I may not really understand property wrappers yet, but I'd have thought that should have been Reference<Lowercased>.

  2. The Mutability of composed wrappedValue accessors section is a little unclear.

    • The algorithm is described in terms of an iteration over the composed property wrappers, but doesn't say whether it's inner-to-outer or vice-versa. If the direction doesn't actually matter, I wager the whole thing could be more simply and understandably described as rules in terms of the words “all” and “any”. If I'm wrong about that, I fear for users' ability to reason about nested property wrappers. That said, if we can't come up with simpler rules, could we have a walkthrough of some examples (and put it in the proposal for posterity)?
    • The first bullet seems to be describing an interaction between three property wrappers: the “next” one, the “previous” one, and the “composed” (which is presumably the current?) one. That seems really unlikely to be the correct interpretation. Can you clear that up?
    • The first bullet looks like it's unintentionally used the word “getter” where maybe it meant “accessor?”
  3. The use case I'm really concerned with is making imported C++ references ergonomic to work with. Specifically,

    struct X { var a, b: Int }
    func returnsReference() -> UnsafeCXXReference<X> { ... }
    func takesReference1(@UnsafeCXXReference _ y: X) {
     print(y.a) // prints the “a” property of some X instance
     takesReference2($y)
    }
    func takesReference2(@UnsafeCXXReference _ y: X) {
     print(y.b) // prints the “b” property of some X instance
    }
    takesReference1(returnsReference())
    

    If I understand correctly, what's in this proposal could “in spirit” (modulo any syntax mistakes I've made) be used to make everything but the last line work. I realize it's out of scope for this proposal, but would you be willing to discuss the possibility of extending the proposed feature, sometime in the future, to cover that last line? I just want to make sure that something like it isn't ruled out.

Thanks again,
Dave

2 Likes
Terms of Service

Privacy Policy

Cookie Policy