mutexre
(Alexander Obuschenko)
1
Hello everyone,
I've just written a proposal (a very simple one):
Motivation
Perhaps one of the common patterns in Swift is a simple direct forwarding of a property's getter/setter to reflect some property of another object
class MyView
{
private let label = UILabel()
var textColor {
get {
return label.textColor
}
set {
label.textColor = newValue
}
}
}
Proposed solution
The proposed solution is to introduce a syntax
alias var textColor = label.textColor
There are also alternative syntaxes:
forward var textColor = label.textColor
and
var textColor = forward label.textColor
These expressions are converted by a compiler into
var textColor: UIColor
{
get {
return label.textColor
}
set {
label.textColor = newValue
}
}
which is processed as usual.
The effect of this proposal on code size (a number of lines) is 1:8.
Source compatibility
'alias var' syntax doesn't break source compatibility with the old code.
It seems possible to write a tool that migrates old syntax to the new one.
Effect on ABI stability
As the implementation of this proposal is based on a simple macro it doesn't affect ABI.
Could you please review the proposal and tell if it has already been previously discussed?
Thanks.
10 Likes
cukr
2
2 Likes
tkrajacic
(Thomas Krajacic)
3
I like the idea and it seems elegant, but I do also think that this should probably happen considering the bigger context that has been named property behaviors.
Thank you for bringing up this use case!
mutexre
(Alexander Obuschenko)
4
Property behaviors are great for more complex use cases as the language itself obviously cannot support the special syntax for every possible property behaviour. Properties forwarding/aliasing is such a ubiquitous, simple and plain pattern that probably deserves its own syntax. What is the use of multiple property behaviour implementations which will virtually be the same? The same stands for the lazy keyword, which I suppose is the motivation for its special language syntax.
I personally do not like the proposed solution since it's just sugar, which has to meet a high bar in order for its inclusion. I think this would be better solved with property behaviors, which still would require a huge re-design.
1 Like
Tino
(Tino)
6
There‘s also forwarding of whole protocols, which could save even more boilerplate...
But imho it‘s better to wait with such additions until there is a roadmap for Swift metaprogramming.
1 Like
I proposed the same here. The argument back then was that it could alleviate bikeshedding, which in of in itself is of course no argument to add such a drastic change to the language.
In addition to your use case, another one would be as a mechanism for dealing with deprecation without code duplication (or wrapping). The recently introduced MigrationSupport.swift file is full of them.
I disagree that property forwarding is even good practice so i’m not a fan of sugaring this
6 Likes
mutexre
(Alexander Obuschenko)
9
@taylorswift, could you please describe why do you think property forwarding is a bad practice?
The pattern, for example, emerges when composing a complex object out of basic ones, when the internal objects are encapsulated in order to prevent direct access.
class C
{
private var a: A
private var b: B
forward var aName = a.name
forward var bName = b.name
}
In the future, you may want to change the internal implementation of class C that will break the user's code (change classes/protocols A and B; merge A and B into C, etc.). Forwarding allows concealing internal implementation thus reducing the chance of user's code being affected.
One use case to consider specifically in the pitch is as follows:
alias var varName = optionalProperty?.property
The getter should return an optional, while the setter should only accept the unwrapped value. This behaviour isn't currently possible for properties, so for now it would probably be best to disallow this alias.
As for property behaviours potentially making this feature redundant, I would expect the existing lazy to be impacted similarly. It's hard to say in advance whether these modifiers will be replaced or subsumed, so in the hope that property behaviours wont be held back, I'm tentatively in support.
mutexre
(Alexander Obuschenko)
12
Thanks for bringing up this case. When the target property is optional the forwarding still may be implemented in the following way:
class A {
var title: String?
}
class B
{
private var a: A?
var title: String? {
get {
return a?.title
}
set {
guard let a = a else {
return // Do nothing since there is no object to change
}
a.title = newValue
}
}
}
So, it seems there are two options in situations when the target object is an optional (or is a part of a chain of optionals):
- Disallow forwarding completely
- Disallow forwarding only for non-optional properties
That's true, but I think it would be preferable to leave the ideal solution open for later. People will rely on that ignore-nil behaviour, leading to breakage of valid code if we ever want to change to non-optional setters. For optional properties accessed through chaining, the situation is even worse, with behaviour subtly changing, without even any breakage to alert the user.
The confusing behaviour for chained optional properties shows getting locked into that solution isn't ideal, and a predictable behaviour mirroring direct access seems more in the spirit of an alias feature.
I can think of philosophical arguments why you might oppose forwarding properties, but imho they are trounced, in practice, by improved performance and code legibility.
In practice, we have to deal with other people's code, much of it written in C or ObjC. The only way I can cope with AudioToolbox, for example, is via wrappers. Without echoing members, I have to either (a) trade decent abstraction (i.e: not having to pass array sizes and pointers everywhere) or (b) use.unweildy.chains.like.this()
Beyond that, forwarding would let us work around a lot of issues that pop up due to Swift being a young language, without a performance hit. A workaround is by definition worse practice than a fix.. but in practice, we can't always wait years for functionality we need now.
1 Like
Jon889
(Jonathan Bailey)
15
How exactly could this be implemented with property behaviours?
if you had something like:
alias var title: String = label.text
How could you capture the RHS in the property behaviour declaration?
You need to reference it in the setter and getter to both set and get it.
1 Like
I really liked this proposal. Was something in the Property Wrappers proposal (or the proposal that is now named as such) supposed to address this? I like both proposals, but the latter doesn’t actually seem to implement the former.
I hope we get something like this. I am daydreaming about several files I have whose code could be made at least twice as concise.
beccadax
(Becca Royal-Gordon)
17
Note that if you want to forward all properties, key path member lookup is your friend here:
@dynamicMemberLookup
class MyView {
private let label = UILabel()
subscript<T>(dynamicMember keyPath: KeyPath<UILabel, T>) -> T {
label[keyPath: keyPath]
}
subscript<T>(dynamicMember keyPath: ReferenceWritableKeyPath<UILabel, T>) -> T {
get { label[keyPath: keyPath] }
set { label[keyPath: keyPath] = newValue }
}
}
4 Likes
spadafiva
(Joe Spadafora)
18
Here's an implementation that works for items that have default values:
protocol DefaultValued {
init()
}
extension UILabel: DefaultValued {}
@propertyDelegate
final class Forwarder<T, Parent: DefaultValued> {
private var parent: Parent
private var keyPath: ReferenceWritableKeyPath<Parent, T>
var value: T {
get { return parent[keyPath: keyPath] }
set { parent[keyPath: keyPath] = newValue }
}
init(keyPath: ReferenceWritableKeyPath<Parent, T>) {
self.parent = Parent()
self.keyPath = keyPath
}
}
class StringHolder: DefaultValued {
var heldString: String
required init() {
heldString = "Initial held string"
}
}
class Foo {
@Forwarder(keyPath: \StringHolder.heldString) var string: String
@Forwarder(keyPath: \UILabel.text) var labelString: String?
}
func test() {
let foo = Foo()
print(foo.string)
print(foo.labelString)
foo.labelString = "Label value changed"
print(foo.labelString)
}
Sorry about the weird naming on everything.... 
Also, this is using the propertyDelegate attribute that is in Xcode 11.2, so I'm not sure exactly what is different in the newest pitch.
3 Likes
Thanks. That’s useful, but not for my particular ends. I do want to see the properties explicitly in my code, but without devoting so much text for a getter and setter. Also, I think it becomes confusing, and I’m guessing slower performing, if I’m manually specifying which keys go where like that (eg: if I’m trying to compose a study that includes properties, possibly renaming them, from more than one helper struct)
Lantua
21
It should perform about the same. Compiler are pretty aggressive at substituting these code at use site.
1 Like
I guess I’m envisioning a list of properties I need to loop through for every access of a property, ie: to control which child struct gets or sets the property. I don’t know how much optimization could speed that up, and I might want a property that is called frequently.