Fantastic, this is a really great idea, I think this will be a really big step forward, particularly if you call it $
as you mention. I'd love to see you take this one teeny tiny further step forward to ensure future generality of this proposal. For background, fundamentally, I see this proposal as introducing two things at the same time:
-
it is introducing the ability to define a "property wrapper" (but as others have mentioned, a better name for this would be "variable wrapper") which allows overloading the getter and setter of a variable with additional behavior.
-
It introduces the ability to refer to a derived value that is linked to the variable with the
$foo
syntax. The first revision of this proposal implied that this name was tied to the storage of the variable wrapper, but the second revision of this proposal clarified that the variable wrapper can actually overload this name to be anything the wrapper desires.
I find this direction (even with your proposed refinements above) concerning for two reasons: First, it is conflating these two different features into one proposal, and second, it is a mistaken assumption that there is exactly one associated view on the variable that could be useful to sugar. I am particularly concerned about this because the spelling of this feature paints us into a corner that could prevent future evolution on this feature from being as elegant as we want it to be.
My view on this is based on the belief that variable wrappers are a critically important feature that will shape the future of a wide variety of APIs that go way beyond SwiftUI - this point was made in the Modern Swift API Design talk at WWDC, which touched on just a few examples of this critical feature - but there are far more that will be explored over the coming years of Swift's evolution, and I'd really love to see this feature service a wide range of these library applications over time.
I'd recommend considering this proposal to be the first two steps of a three step proposal. In theory each step should be independently considered, but given that SwiftUI is a critical use-case and needs the first two steps in conjunction, considering the first two steps at the same time makes sense. The three step proposal would look something like this:
First Step
Introduce the notion of a "variable wrapper attribute". The variable wrapper gets an attribute on it, and has a wrappedValue
property, here's an example:
@variableWrapper
struct UserDefault<T> {
...
var wrappedValue: T { get {...} set {...}}
}
If you use a variable wrapper like this with the @UserDefault var foo : T
syntax, then you'd get a foo
computed property that routes through the wrapper, and you'd get a foo$storage
property with private
access control. This achieves your first goal of providing the storage with a consistent name that can be found through reflection, explicit initialization, the Xcode debugger, and all the other things that stored properties appear in. I think that adding the name "storage" to this makes it much more clear what this is, eliminates surprise, and (given its infrequency of use) I think that adding a word to its name improves clarity above and beyond the $$foo
or _$foo
proposals.
Second Step
Some clients want to provide one privileged computed view of a variable - it could be the storage directly or could be a derived variable that projects the storage into a form intended to be API of a variable with this wrapper. It is reasonable for these to be really really short given the bindings use-case in SwiftUI and other clients are likely to want other similar default projections when there is only a single possible interpretation (e.g. a resettable property wants a reset method on its storage).
To support this, we allow defining a $
property on top of the base proposal which provides access to this in a variable-wrapper defined way:
@variableWrapper
enum Lazy<Value> {
...
var wrappedValue: Value { ... }
public var $ : Lazy<Value> { get { return self } set { self = newValue } }
}
We'd require that the $
member have the same access control level as the wrapper itself, and thus (in the cross-module case) it matches the access control level of any properties that use the wrapper type. This gives wrapper authors control over what they want the $
member to do, the specific API they want to expose, and whether they want to expose it. This also ensures that a $
member is not exposed unless the wrapper author explicitly opts into it.
If a wrapper author defines this member, then users would get a suffix $
member, e.g. a foo
variable would get a foo$
member of this name. This is a tiny delta over the proposal as you suggest it above, but this tiny difference is critical because there isn't necessarily a primary view and a single projection of a variable -- and most importantly, it allows a future proposal to extend this in the third step.
Third Step
While the proposal as written covers the first two steps, I think it is important to think beyond the immediate SwiftUI use case, and consider more general applications of variable wrappers. In particular, it is not clear that the fundamental model of a variable is one primary view and exactly one more projection of that view -- nor is it clear that all uses of variable wrappers will want a projection that has such a trivial name as $
. Swift is designed to allow domain experts to design APIs with evocative APIs and think about the tradeoff (e.g.) between anonymous arguments and keyword arguments, and it would be really unfortunate if this feature prevented API authors from being able to use descriptive names for projections in variable wrappers.
Beyond clarity of naming, the fundamental nature of variables allows there to be "N" projections of a variable, particularly in advanced cases -- for example, it is easily imaginable that you might want to have a data projection, a database handle projection, and have an exportable bindable projection. Other cases may want more than one projection for other reasons: e.g. because the underlying foo$storage
is complex or private, and there are several curated views that have different sorts of (protocol or semantic) requirements.
As such, the third step is to allow named computed variable suffixes, which can be easily extended onto the first two proposed steps, by allowing named properties, e.g.:
@variableWrapper
enum MyWrapper<Value : MyProtocol> {
var wrappedValue: Value { ... }
// "public var $" : is still allowed, just not shown in this example!
public var $dbhandle : DBHandle<Value> {... }
public var $view : MyView<Value> {... }
public var $binding : MyBinding<Value> {... }
}
When applied to a v : T
variable, such a variable wrapper would produce private v$storage
(of type MyWrapper<T>
), as well as several computed properties that match the underlying access control level of v
: v$dbhandle
, v$view
, v$binding
.
This extension allows significant added modeling power, more clarity of APIs, doesn't overload prefix $
(currently widely used by anonymous closure arguments), doesn't conflict with LLDB, and provides a more logical access control story -- since the declarations that are being defined align clearly with the declarations that clients can use.
I'd really like for the core team to consider a direction like this, even though it means moving the location of the dollar sign from the beginning of the derived variable name to the end of the name.
-Chris