Properties forwarding (aliasing)

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

It would be a great use for swift-evolution/0030-property-behavior-decls.md at master · apple/swift-evolution · GitHub

2 Likes

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!

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

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

@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.

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):

  1. Disallow forwarding completely
  2. 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

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.

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

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.... :slight_smile:

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)

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.