Pitch: Property Delegates

Love this idea, and I’m excited to see it return! This revised version’s power and simplicity is admirable. The Lazy<Value> example is beautiful.

I’d have to sleep on the by and $ syntax, but if they’re not perfect, they feel close.

Though I concede its use cases are few, I’d be sorry to lose this:

  • Delegates used for properties declared within a type cannot refer to the self of their enclosing type. This eliminates some use cases (e.g., implementing a Synchronized property delegate type that uses a lock defined on the enclosing type), but simplifies the design.

Could delegates take self as an explicit argument?

class C {
    var foo: Synchronized(on: self)
}

I’m guessing the answer is “no” because self isn’t fully initialized until the property already exists. Is there a future direction for this?

Can a generic type follow by?

Given that the proposed syntax doesn’t support nested delegates, would it support writing a generic delegate-wrapping delegate? e.g.

var foo: Int by NestedDelegate(Copying(), inside: DelayedMutable())

10 Likes

This looks great, and is a nice simplification of the earlier pitch that still hits the major use cases. My immediate thought is that I don't like the syntax, primarily because it is in the wrong place. The current similar features (e.g. lazy and @NSCopying) must appear before the var keyword, where this appears either at the end or in the middle of the property declaration. This creates inconsistency and limits the ability to clean up the property declaration by moving the delegate part to a different line. To expand an example from this thread:

var x: Data by StatefulDelegate(url: URL(string: "http://swift.org")!) = 
  try! Data(contentsOf: URL(string: "http://example.org")!)

it definitely starts to become confusing when you have property delegates with their own initialisers right in the middle of the property declaration. I'm sure you've considered various ideas here, so what were your thoughts about something like:

@delegate(StatefulDelegate(url: URL(string: "http://swift.org")!))
var x: Data = try! Data(contentsOf: URL(string: "http://example.org")!)

with your choice of spelling in place of @delegate? This puts it in a place that is consistent with lazy and @NSCopying, and allows for more flexibility in formatting code because it's no longer stuck in the middle.

2 Likes

It should be noted that, compared to today’s builtin lazy implementation, and SE-30, the version of Lazy proposed here comes at a storage efficiency cost, since the lazy initializer has to be stored inline for every instance and every lazy property.

2 Likes

That's not actually permitted syntax under this proposal; you can either explicitly initialize the delegate or you can provide an initial value of the "public" type with =, which gets turned into the call to init(initialValue:). So the "arbitrary code" part is always at the end of the declaration.

Ah, thanks, I didn't appreciate that when reading the proposal, and I thought it would initialise using the delegate's initialiser and then assign to value. Well, less potential for confusion but the delegate is still in the middle or at the end of the property declaration, which isn't consistent with the current similar features.

I really like the simplicity of this being just syntactic sugar for a wrapper type. It’s a very straightforward approach and echoes the design for user-defined attributes that @hartbit recently suggested. Like some of the others, I’m not sure of the usage syntax but discussion of that can wait.

This is just my immediate intuition without deep thought, but it seems like it might be a good idea to restrict @propertyDelegate to value types. Does anyone know of good use cases that would require @propertyDelegate class? Everything I can think of would use value types, even if a class was used in the implementation, for example:

@propertyDelegate
struct Indirect<Value> {
    private class Box { 
        var value: Value
        init(value: Value) { 
           self.value = value
        }

        func copy() -> Box {
             return box(value: value)
        }
    }

    let box: Box

    init(initialValue value: Value) {
        box = Box(value: value)
    }

    var value: Value {
        get { 
              return box.value 
        }

        set { 
           if !isKnownUniquelyReferenced(&box) { 
               box = box.copy() 
           }
           box.value = newValue
        }
    }
}

The above example makes me wonder if it would be possible to have general syntax for initializing a delegated property using an existing value of the delegate type. For example:

var int: Int by Indirect = 42

// this is only possible if the property delegate has a “copy initializer”
var other1: Int by Indirect($int)

// this might be possible, but is probably not a great idea as $int is not of type Int
// is there an alternative that does not have potential for confusion or implementation
// complexity but also does not require manually writing a copy initializer?
var other2: Int by Indirect = $int

With respect to SE-0030, I especially miss its composability. I wonder if the direction @Paul_Cantrell suggested of using an additional generic parameter might be a viable way to approach composition. I would really like to see this direction explored further.

If composition via generics is viable I think it pushes us in the direction of using a PropertyDelegate protocol - we would need a way to constrain the wrapped property delegate and a protocol is the natural way to do that. This would necessarily be a “magic” protocol as it isn’t expressible directly in the language, but I’m ok with that if it allows us to support composition of property delegates.

A property delegate that implemented atomics would need to use a class; it's the only way in current Swift to be sure you're not working on a temporary copy of the data. You could wrap the class in a struct, of course, but doesn't that just demonstrate that a "value type only" rule would be paper-thin?

1 Like

This would be a great feature! However:

  • I'm not a huge fan of the by syntax, as by has no obvious meaning there. Something more compact would probably be better, especially for types with a lot of delegated properties. Perhaps var[Lazy] string = "string"? Seems easier than a new keyword.
  • Is it at all possible to vend the delegate's API through the property? Would outside users have access to the $ value? I'm thinking about @jrose's Observable above. If I had a var[Observable] user: User, would there be a way to get access to the addObserver function? The example exposes a _ variable directly, so I'm not sure how it would map to the proposal. But this would be huge for creating observable properties.
  • Is the restriction on not having properties with delegates a limitation of delegates in general, or just this first proposal? If we can't declare delegates, would be be another type equivalence we could use? i.e. Could var[Observable] user: User be considered equivalent to var user: Observable<User>, at least in some contexts?
7 Likes

I have been waiting for this for years, thank you for bringing this up again.

I have one question regarding the mutability of properties with delegates. Is there any limitation that preview the properties to introduced with let keyword? I think there're a few cases where I would need to use the delegates for a constant let property, for example the lazy or delayed initialization.

3 Likes

This looks great, thanks!

One concern is that the composability has gone iiuc. So I can't specify e.g.

… by Atomic, Lazy

So there will be a lot of Delegate types that just combine others? Got a new one – now you have to write 20 variants of it if you need other behavior composed (In reality I guess there will be a handful of common ones, so I am not sure this will be a big issue)

I agree with others here that the syntax is probably the thing to like least, but that's something for another discussion I think.

Use via instead of by (which would also be a lot easier to search for with relevant results)

or delegate in front: var(Lazy) kermit: Muppet

Anyway, again thank you.

7 Likes

I don't like the new syntax, nor the old one. I would prefer having it as attributes or something like what tkrajacic proposed:

3 Likes

@Douglas_Gregor thank you so much for pushing property behaviors forward, even now a slightly different form. I just (re-)read both proposals in comparison and I see the similarities and appreciate the work and time you spent on this.

Even though both Property Behavior and Property Delegate only aim to be citizen of a variable (expect the case of 'out-of-line initialization' where let can be treated like a var) I still think we need to explore the possibilities to allow some property delegates on constants. Such constants most likely will be, like @pitiphong.p mentioned, both lazy and delayed initialized or for example just delayed immutable.


I cannot wait to get my hands on DelayedImmutable as it eradicates the need of optionals on @IBOutlet properties and also guards the property from being re-assigned later on. Ideally however I would wish to be able to write something like @IBOutlet let label: UILabel by PropertyDelegate. That PropertyDelegate should do multiple things:

  • it should act like DelayedImmutable
  • it should guard in context of a UIViewController, the controller's isViewLoaded property returns true when accessed the label through a getter.

The second point is my next problem with the current design as it does not allow access to self of the property holder. Can we somehow loosen up this restriction?


I'm also not a huge fan of the by syntax and prefer something like property behaviors had, but I see the other use cases presented by property delegates which are not expressible otherwise.


Can we have multiple overloads of the delegate's initializer? As proposed a Lazy property can be written as: var foo: Int by Lazy = 42 but if instead of an Int you had a more complex type that requires more setup we could provide a closure instead of the value. It would be very convenient if we could avoid the need of () at the end of the closure. That said, I think to have an extra overload without @autoclosure on Lazy would be great. Such an overload should allow us to write this:

var label: UILabel by Lazy = {
  let label = UILabel()
  /* configure the label here */
  return label
}

Can you please explicitly mention in the proposal if this will be somehow backwards compatible with Swift 5?


Overall I'm happy to see progress on that area. Thank you.

3 Likes

One possibility here would be to have separate access control for foo and $foo. With an Observable thing you clearly want its nature exposed in your interface, but with a Lazy one you probably don't.

If we had protocol delegates, we could make an operator to compose the delegates:

Lazy + Observable

(the + operator is wrong because the operation is not commutative but I'm not sure what to put)

Here is an example of a property delegate I would want to express to get rid of the shortcomings and a lot of programmer mistakes in projects on apple platforms:

@propertyDelegate
struct DelayedImmutableView<View: UIView> {
  private unowned let _controller: UIViewController
  private var _value: View? = nil

  init(controller: UIViewController) {
  	self._controller = controller
  }

  var value: View {
    get {
      guard _controller.isViewLoaded else {
      	fatalError("controller's view must be loaded before accessing the view")
      }
      guard let value = _value else {
        fatalError("property accessed before being initialized")
      }
      return value
    }

    // Perform an initialization, trapping if the
    // value is already initialized.
    set {
      if _value != nil {
        fatalError("property initialized twice")
      }
      _value = newValue
    }
  }
}

Then I would write something a property like this:

@IBOutlet lazy var label: UILabel by DelayedImmutableView

It would translate into this:

lazy var $label = DelayedImmutableView<UILabel>(controller: self)
var label: UILabel {
  get { return $label.value }
  set { $label.value = newValue }
}

And can be safely used from here:

func viewDidLoad() {
  super.viewDidLoad()
  // No more optionals, no more silly mistakes updating a view
  // before the controller was woken from `Nib` where you just lose
  // that data.
  self.label.text = "Swift is awesome"
}

If we could also get rid of lazy here and create multiple delegates that are also composable that would be amazing. We can potentially make @IBOutlet into a property delegate as well and move it from the language surface into UIKit. But then we definitely need a way to compose multiple delegates var label: UILabel by [IBOutlet, DelayedImmutableView].


Sad story that @IBOutlet requires the type to be optional otherwise we could already use the above delegate type.

class Controller: UIViewController {
  private lazy var __label = DelayedImmutableView<UILabel>(controller: self)
  @IBOutlet var label: UILabel { // error @IBOutlet property has non-optional type 'UILabel'
    get { return __label.value }
    set { __label.value = newValue }
  }
}
4 Likes

This is one of those features that for day to day development would only come up occasionally, but when the need does arise it would be fantastic to have. It also will allow for some 'not so nice' rough edges we have now to be cleaned up, so generally I'm all for it.

I think I'm in the same boat about the syntax which some others have mentioned above. I'm not totally sold on the by keyword as it takes a bit longer to digest when simply glancing at the property declaration (you have to look backwards and forwards through the declaration to understand how it behaves), but I do quite like the var(Lazy) examples that have been proposed as an alternative. Generally when you are dealing with properties you look towards the beginning of the declaration for the behaviour of it (mutable/immutable), so by moving the property delegate to be part of let or var it's quicker to understand the overall behaviour of the property without having to look backwards and forwards through it.

Behaviour > Name > (Type) > Value
var(Lazy) foo: Int = 1738

vs

Mutability Behaviour > Name > (Type) > Delegate Behaviour > Value
var foo: Int by Lazy = 1738

2 Likes

I like the choice of not using protocols for the feature, but I have one concern:
To me, it's still completely unclear what kind of metaprogramming capabilities will be added to Swift (if any at all), so I have no idea how it may interfere with this pitch.
Most likely, there won't be a real conflict, but I can think of flexible solutions that would render property delegates superfluous.
Given the fact that due to compatibility concerns, it becomes harder and harder to remove legacy features, it might be a good idea not to spend significant resources to catch up with Kotlin.

I had no pressing need for neither property delegates nor advanced metaprogramming in the last years, so I'd rather wait a little bit longer if this patience had the prospect of a better solution.

1 Like

A lot of people are suggesting things they find "missing" from the proposal, and I want to point out that that's one of the reasons SE-0030 had to get sent back: no one was sure that what was there was the right set. If we can agree that the stuff that is here is good, we can expand later.

I'm not saying not to talk about "missing" things at all! As usual, that helps us as a community keep from boxing ourselves in with something that's not expandable. But if there's an at all plausible way to do access control, or references to self, or something sensible with let, then we don't have to do it now.

(That's also my complaint about a lot of the alternate syntax suggestions here: they're way less expandable, and already don't account for the by Foo(context: importantConfiguration) syntax that's included in the current version of the proposal. Not having that would force explicit initialization within types and reduce the applicability outside of types.)


Responding to some specific comments:

I hate to say it, but let + Lazy is likely never to happen, because Lazy inherently has a mutating getter. You can get the same effect with DelayedImmutable, but it's never going to be truly a let.

The "overload without autoclosure" example isn't really important; like today's lazy (or non-lazy), you can get the same effect by immediately calling the closure that computes the initial value. The compiler can in theory guarantee that that's just as efficient, though I don't know if it does so today.


I think Dale's comment sums up my overall opinion: "This is one of those features that for day to day development would only come up occasionally, but when the need does arise it would be fantastic to have."

8 Likes

That's not my main concern about this proposal, I don't want to support something that is not clearly specified. I asked for clarification that this feature is not restricted to one optional initializer that is set in stone, that only accepts one Value type and thats it. This is neither flexible nor in any case extensible from the current perspective. The proposal lacks clarifications where it ultimately wants to go in the long run. All I want to say here is that we as the community should 'together' carefully design and discuss this feature instead of going the 'let's ship the first bare-bone version' road and see what we can do with it later. That would be as if you'd put bricks on your road and then later have to build a bridge around it.

That said, accumulating examples and potential use-cases of this feature is a good thing and we should not push these things back so hard, but shape the overall feature from it. ;)

1 Like

I like the pitch! I'm not enamored of by, however.

Here are a few other thoughts:

// Original `by`:
var foo by Lazy = 1738
public var foo: Int by public Lazy = 1738
public var foo: Int by Lazy(closure: { 42 })

// `of`:
var foo of Lazy = 1738
public var foo: Int of public Lazy = 1738
public var foo: Int of Lazy(closure: { 42 })

// `with`:
var foo with Lazy = 1738
public var foo: Int with public Lazy = 1738
public var foo: Int with Lazy(closure: { 42 })

// `using`:
var foo using Lazy = 1738
public var foo: Int using public Lazy = 1738
public var foo: Int using Lazy(closure: { 42 })

I'm sort of liking using. Thoughts?

(edit btw: I see the syntax highlighter is highlighting using. Am I forgetting a current keyword using?)

5 Likes