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

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