- Proposal: SE-0293
- Authors: Holly Borla, Filip Sakel
- Review Manager: Chris Lattner
- Status: Active Review (Dec 1, 2020 ... Dec 13, 2020) implementation
- Implementation: apple/swift#34272
Property Wrappers were introduced in Swift 5.1, and have since become a popular mechanism for abstracting away common accessor patterns for properties. Currently, applying a property wrapper is solely permitted on local variables and type properties. However, with increasing adoption, demand for extending where property wrappers can be applied has emerged. This proposal aims to extend property wrappers to function and closure parameters.
Property wrappers have undoubtably been very successful. Applying a property wrapper to a property is enabled by an incredibly lightweight and expressive syntax. For instance, frameworks such as SwiftUI and Combine introduce property wrappers such as State
, Binding
and Published
to expose elaborate behavior through a succinct interface, helping craft expressive yet simple APIs. However, property wrappers are only applicable to local variables and type properties, shattering the illusion that they helped realize in the first place when working with parameters.
Currently, property-wrapper attributes on struct properties interact with function parameters through the struct's synthesized memberwise initializer. However, property-wrapper attributes are not supported on function parameters. This leads to complicated and nuanced rules for which type, between the wrapped-value type and the backing property-wrapper type, the memberwise initializer accepts.
The compiler will choose the wrapped-value type to offer a convenience to the call-site when the property wrapper has an initializer of the form init(wrappedValue:)
accepting the wrapped-value type, as seen here:
import SwiftUI
struct TextEditor {
@State var document: Optional<URL>
}
func openEditor(with swiftFile: URL) -> TextEditor {
TextEditor(document: swiftFile)
// The wrapped type is accepted here.
}
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:)
:
import SwiftUI
struct TextEditor {
@State() var document: Optional<URL>
}
func openEditor(with swiftFile: URL) -> TextEditor {
TextEditor(document: State(wrappedValue: swiftFile))
// The wrapped type isn't accepted here; instead we have
// to use the backing property-wrapper type: 'State'.
}
Note also that the argument label does not change when the memberwise initializer uses the backing wrapper type instead of the wrapped-value type.
If the generated memberwise initializer always accepted the backing wrapper type while still allowing the call-site the convenience of automatically initializing the backing wrapper via a wrapped-value type, the mental model for property wrapper initialization would be greatly simplified. Moreover, this would provide more control over the backing-wrapper initialization at the call-site.
Using property-wrapper types for function parameters also results in boilerplate code, both in the function body and at the call-site:
@propertyWrapper
struct Lowercased {
init(wrappedValue: String) { ... }
var wrappedValue: String {
get { ... }
set { ... }
}
}
func postUrl(urlString: Lowercased) {
guard let url = URL(string: urlString.wrappedValue) else { return }
// ^~~~~~~~~~~~~
// We must access 'wrappedValue' manually.
...
}
postUrl(urlString: Lowercased(wrappedValue: "mySite.xyz/myUnformattedUsErNAme"))
// ^~~~~~~~~~
// We must initialize `Lowercased` manually,
// instead of automatically initializing
// from its wrapped value type.
In the above example, the inability to apply property wrappers to function parameters prevents the programmer from removing unnecessary details from the code. The call-site of postUrl
is forced to initialize an instance of Lowercased
manually using init(wrappedValue:)
, even though this initailization is automatic when using @Lowercased
on a local variable or type property. Further, manually accessing wrappedValue
in the function body can be distracting when trying to understand the implementation. These limitations are emphasized by the fact that property wrappers were originally sought out to eliminate such boilerplate.
Consider the following SwiftUI code, which uses ForEach
over a collection:
struct MyView : View {
// A simple Shopping Item that includes
// a 'quantity' and a 'name' property.
@State
private var shoppingItems: [Item]
var body: some View {
ForEach(0 ..< shoppingItems.count) { index in
TextField(shoppingItems[index].name, $shoppingItems[index].name)
}
}
}
Working with shoppingItems
in the closure body is painful, because the code must manually index into the original wrapped property, rather than working with collection elements directly in the closure. The manual indexing would be alleviated if the closure accepted Binding
s to collection elements:
struct MyView : View {
// A simple Shopping Item that includes
// a 'quantity' and a 'name' property.
@State
private var shoppingItems: [Item]
var body: some View {
ForEach($shoppingItems) { itemBinding in
TextField(itemBinding.wrappedValue.name, itemBinding.name)
}
}
}
However, now we observe the same boilerplate code in the closure body because the property-wrapper syntax cannot be used with the closure parameter.
We propose to allow application of property wrappers on function and closure parameters.
Using property-wrapper parameters, the above postUrl
example becomes:
func postUrl(@Lowercased urlString: String) {
guard let url = URL(string: urlString) else { return }
...
}
postUrl(urlString: "mySite.xyz/myUnformattedUsErNAme")
In the above SwiftUI example, if collection elements could be accessed via Binding
s in the ForEach
closure, property-wrapper parameters could be used to enable property-wrapper syntax in the closure body:
struct MyView: View {
@State
private var shoppingItems: [Item]
var body: some View {
ForEach($shoppingItems) { (@Binding item) in
TextField(item.name, $item.name)
}
}
}
Property wrappers are essentially sugar wrapping a given property with compiler-synthesized code. This proposal retains this principle, employing the following transformation.
The transformation of function with a property-wrapper parameter will be performed as such:
- For regular functions, the argument label will remain unchanged.
- The parameter name will be prefixed with an underscore.
- The type of the parameter will be the backing property-wrapper type.
- A local computed property representing the
wrappedValue
of the innermost property wrapper will be synthesized with the same name as the original, unprefixed parameter name. If the innermostwrappedValue
defines a setter, a setter will be synthesized for the local property if the mutability of the composed setter isnonmutating
. The mutability computation is specified below. - If the outermost property wrapper defines a
projectedValue
property, a local computed property representing the outermostprojectedValue
will be synthesized and named per the original parameter name prefixed with a dollar sign ($
). If the outermostprojectedValue
defines a setter, a setter for the local computed property will be synthesized if theprojectedValue
setter isnonmutating
, or if the outermost wrapper is a reference type.
The computation for mutability of a wrapped parameter's composed wrappedValue
accessors will be the same as it is today for wrapped properties. The computation starts with the mutability of the outermost wrapper's wrappedValue
accessor, and then iterates over the chain of composed property wrappers, "composing" the mutability of each wrappedValue
accessor along the way using the following rules, which are the same for getters and setters:
- If the next
wrappedValue
accessor isnonmutating
, then the mutability of the composed accessor is the same as the previous composed getter. If the wrapper is a reference type, the accessor is considerednonmutating
. - If the next
wrappedValue
accessor ismutating
, then the composed accessor ismutating
if the previous composed getter or setter ismutating
, since both are needed to perform a writeback cycle.
If any of the property wrappers do not define a wrappedValue
setter, then the wrapped property/parameter does not have a setter.
When passing an argument to a function with a property-wrapper parameter using the original argument label with no prefix, the compiler will wrap the argument in a call to init(wrappedValue:)
. This transformation does not apply to closures, because closures are not called with argument labels.
Since the property wrapper is initialized at the call-site, this means that the argument type can impact overload resolution of init(wrappedValue:)
. For example, if a property wrapper defines overloads of init(wrappedValue:)
with different generic constraints and that wrapper is used on a function parameter, e.g.:
@propertyWrapper
struct Wrapper<Value> {
init(wrappedValue: Value) { ... }
init(wrappedValue: Value) where Value : Collection { ... }
}
func generic<T>(@Wrapper arg: T) { ... }
Then, overload resolution will choose which init(wrappedValue:)
to call based on the static type of the argument at the call-site:
generic(arg: 10) // calls the unconstrained init(wrappedValue:)
generic(arg: [1, 2, 3]) // calls the constrained init(wrappedValue:)
// because the argument conforms to Collection.
Functions that accept property-wrapper parameters are transformed to accept the backing wrapper type. Consider the postUrl
function from earlier:
func postUrl(@Lowercased urlString: String) { ... }
The type of postUrl
is (Lowercased) -> Void
. These semantics can be observed when working with an unapplied reference to postUrl
:
let fn: (Lowercased) -> Void = postUrl
fn(Lowercased(wrappedValue: "mySite.org/termsOfService"))
Consider the postUrl
example from earlier:
@propertyWrapper
struct Lowercased {
init(wrappedValue: String) { ... }
var wrappedValue: String {
get { ... }
set { ... }
}
}
func postUrl(@Lowercased urlString: String) { ... }
postUrl(urlString: "mySite.xyz/myUnformattedUsErNAme")
In the above code, the postUrl(urlString:)
function and its caller are equivalent to:
func postUrl(urlString _urlString: Lowercased) {
var urlString: String {
get { _ urlString.wrappedValue }
}
...
}
postUrl(urlString: Lowercased(wrappedValue: "mySite.xyz/myUnformattedUsErNAme"))
A setter for the local computed property urlString
is not synthesized because the setter of Lowercased.wrappedValue
is mutating
.
Now, consider the following Reference
property wrapper, which is composed with Lowercased
and used on a closure parameter:
@propertyWrapper
struct Reference<Value> {
var wrappedValue: Value {
get
nonmutating set
}
var projectedValue: Self {
self
}
}
let useReference = { (@Reference @Lowercased reference: String) in
...
}
In the above example, the closure useReference
is equivalent to:
let useReference = { (_reference: Reference<Lowercased>) in
var reference: String {
get {
_reference.wrappedValue.wrappedValue
}
set {
_reference.wrappedValue.wrappedValue = newValue
}
}
var $reference: Int {
get {
_reference.projectedValue
}
}
...
}
Since both the getter and setter of Reference.wrappedValue
are nonmutating
, a setter can be synthesized for var reference
, even though Lowercased.wrappedValue
has a mutating
setter. Reference
also defines a projectedValue
property, so a local computed property called $reference
is synthesized in the closure, but it does not have a setter, because Reference.projectedValue
only defines a getter.
The composed mutability of the innermost wrappedValue
getter must be nonmutating
.
Rationale: If the composed
wrappedValue
getter ismutating
, then the local computed property for a property-wrapper parameter must mutate the backing wrapper, which is immutable.
Property-wrapper parameters cannot have an @autoclosure
type.
Rationale:
@autoclosure
is unnecessary for the wrapped value, because the wrapped-value argument at the call-site will always be wrapped in a call toinit(wrappedValue:)
, which can already support@autoclosure
arguments.
Property wrappers on function parameters must support init(wrappedValue:)
.
Rationale: This is an artificial limitation to prevent programmers from writing functions with an argument label that cannot be used to call the function.
Property-wrapper parameters cannot have additional arguments in the wrapper attribute.
Rationale: Arguments on the wrapper attribute are expected to never be changed by the caller. However, it is not possible to enforce this today; thus, property-wrapper parameters cannot support additional arguments in the attribute until there is a mechanism for per-declaration shared state for property wrappers.
Non-instance methods cannot use property wrappers that require the enclosing self
subscript.
Rationale: Non-instance methods do not have an enclosing
self
instance, which is required for the local computed property that representswrappedValue
.
This is an additive change with no impact on source compatibility.
This is an additive change with no impact on the existing ABI.
This proposal introduces the need for property-wrapper custom attributes to become part of public API. This is because 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.
Instead of initializing the backing property wrapper using the argument at the call-site of a function that accepts a wrapped parameter, another approach is to initialize the backing property wrapper using the parameter in the function body. One benefit of this approach is that annotating a parameter with a property-wrapper attribute would not change the type of the function, and therefore adding or removing a wrapper attribute would be a resilient change.
Under these semantics, using a property-wrapper parameter is effectively the same as using a local property wrapper that is initialized from a parameter. This implies that:
- A property-wrapper parameter cannot be used to opt into property-wrapper syntax in the body of a closure that has a parameter with a property-wrapper type.
- The type of the argument provided at the call-site cannot affect the overload resolution of
init(wrappedValue:)
. - This feature cannot be extended to allow the call-site to initialize the backing wrapper using a mechanism other than
init(wrappedValue:)
, which is later discussed as a future direction. This further implies that property-wrapper parameters can only be used with property wrappers that supportinit(wrappedValue:)
.
One of the main use-cases for property-wrapper parameters is opting into property-wrapper syntax in the body of a closure, which makes this approach unviable.
In this proposal, a property-wrapper argument is wrapped in a call to init(wrappedValue:)
when the call-site uses the original, unprefixed argument label. We could extend this model to support a different initialization mechanism for the backing wrapper, or to pass the backing wrapper directly, by using the original argument label prefixed with $
, e.g.:
func postUrl(@Lowercased urlString: String) { ... }
postUrl($urlString: Lowercased(...))
Many closures have a contextual type, which means the backing property-wrapper type can be inferred from context. So, spelling out the backing property-wrapper type in the custom attribute is repeating unnecessary type information. Instead, we could allow the $
syntax on a closure parameter to infer the wrapper attribute:
struct MyView: View {
@State
private var shoppingItems: [Item]
var body: some View {
ForEach($shoppingItems) { $item in
TextField(item.name, $item.name)
}
}
}
This syntax could also potentially be used on parameter declarations that want to use property wrappers that do not support init(wrappedValue:)
. This would allow such property wrappers to be used on function parameters without declaring the function with an argument label that cannot be used to call the function:
func createItemRowView($item: Binding<Item>) -> some View {
TextField(item.name, $item.name)
}
createItemRowView($item: binding)
It’s important to note, however, that the above syntax has quite a few shortcomings. Namely, one notable drawback is that this syntax is reminiscent of property wrappers' projected value and, thus, may be confusing. Furthermore, auto-complete could be a better solution here, as it would aid the user in writing the property-wrapper attribute, while still retaining expressiveness. Nevertheless, this idea could be expanded upon in the future.
Synthesized memberwise initializers could use property-wrapper parameters for stored properties with attached property wrappers:
struct MyView {
@State() var document: Optional<URL>
// Synthesized memberwise init
init(@State document: Optional<URL>) { ... }
}
func openEditor(with swiftFile: URL) -> TextEditor {
TextEditor(document: swiftFile)
}
This is left as a future direction because it is a source breaking change.
This proposal doesn't currently support marking property-wrapped function parameters inout
. We deemed that this functionality would be better tackled by another proposal, due to its implementation complexity. Nonetheless, such a feature would be useful for mutating a wrappedValue
argument when the wrapper has a mutating
setter.
Adding wrapper types to the standard library has been discussed for types such as @Atomic
and @Weak
, which would facilitate certain APIs. Another interesting standard library wrapper type could be @UnsafePointer
, which would be quite useful, as access of the pointee
property is quite common:
let myPointer: UnsafePointer<UInt8> = ...
myPointer.pointee
// ^~~~~~~
// This is the accessor pattern property
// wrappers were devised to tackle.
Instead of writing the above, in the future one might be able to write the following:
let myInt = 0
withUnsafePointer(to: ...) { (@UnsafePointer value) in
print(value) // 0
$value.withMemoryRebound(to: UInt64.self) {
...
}
}
As a result, unsafe code is not dominated by visually displeasing accesses to pointee
members; rather, more natural and clear code is enabled.