Pitch: Property Delegates

I really like via. It's short, unique, and does a good job of suggesting the indirection.

7 Likes

Composition is something we weren't able to adequately solve with the property behaviors design in 2015-2016, either. In particular, composing Atomic and Lazy is fraught, because composing them in the wrong order will result in surprisingly bad semantics. As natural as it is to want to compose delegates at the use site, that cautionary tale makes me thing it's better to have compositions described as specific delegate types.

Doug

6 Likes

Function-local and global variables are also supported; I've clarified this in the latest revision at https://github.com/DougGregor/swift-evolution/blob/property-delegates/proposals/NNNN-property-delegates.md.

Doug

1 Like

Thank you for the explanation. I read the proposal after posting my question and understand how it would work now.

So do I understand correctly that the reason that it must be var is that the Property will be converted into a computed property in the end which is already require the var keyword not related to the Property Delegations concept itself?

Ah maybe I misunderstood the question then. Couldn't you just make the $foo public and do $foo.addObserver(…)?

I completely understand that. It would just be a bit unfortunate if I want to package my own behavior in a delegate type, but also want the property to be observable for example, that I'd literally have to copy the implementation from the official one into my code as well.

I do understand that "copy code in" is oversimplified and actually the very reason the easy composability doesn't exist in the first place, but I just remember how simple it was in Objective-C to add atomic for example to the declaration of a property – and there it was.

I mean if I want to have a property be Observable (suppose this would be a possible delegate) then there would need to be an ObservableAtomic, ObservableLazy,… and so on. Maybe I'm overthinking this, but I suppose there will be delegates that will be very frequently used, and this means we'll need a LOT of variants.

2 Likes

Yeah, it looks like that would work with no problem.

If we're going to bikeshed by, let's bikeshed it from first principles.

If we want to choose a keyword that reads well, we have to consider what is around that keyword. On the left (in the proposed syntax, at least), we have a type or property name which will usually be a noun, because the Swift API Design Guidelines state:

  • The names of other types, properties, variables, and constants should read as nouns.

But what belongs on the right side of the keyword? What sort of name should you give to a property delegate?

Many of the examples in this thread are adjectives—Lazy, DelayedMutable, Copying (participles are adjectives), CopyOnWrite, Observable, Synchronized, and Indirect all are. But UnsafeMutablePointer and UserDefault are nouns, and I think they may represent a category of delegates specifying where a property is stored. Imagine DatabaseColumn or XPC, for instance. Or Box, for that matter.

The by keyword works with some nouns, but not all, and doesn't really work with adjectives.

var foo: Foo by Lazy
var foo: Foo by DelayedMutable
var foo: Foo by Copying    // This one's okay because you reinterpret it as a verb.
var foo: Foo by CopyOnWrite
var foo: Foo by Observable
var foo: Foo by Synchronized
var foo: Foo by Indirect

var foo: Foo by UnsafeMutablePointer
var foo: Foo by UserDefault
var foo: Foo by DatabaseColumn
var foo: Foo by XPC
var foo: Foo by Box    // wut

The adjectives could be adapted into nouns by adding a suffix like Property or Var. If we did that, in would read pretty well; if we wanted a slightly more unique keyword, within or inside would work too.

var foo: Foo in LazyVar
var foo: Foo in DelayedMutableVar
var foo: Foo in CopyingVar
var foo: Foo in CopyOnWriteVar
var foo: Foo in ObservableVar
var foo: Foo in SynchronizedVar
var foo: Foo in IndirectVar

var foo: Foo in UnsafeMutablePointer
var foo: Foo in UserDefault
var foo: Foo in DatabaseColumn
var foo: Foo in XPC
var foo: Foo in Box

This might also make us rethink the metaphor we use to describe this feature: you could describe it as specifying a "container" to store the property inside, which might lead you to call this feature something like "property containers".

On the other hand, the nouns can usually be adapted into adjectives by adding some suffix too, although the appropriate suffix would vary by the exact nature of the noun. Adjectives would pair well with keywords like is or but:

var foo: Foo but Lazy
var foo: Foo but DelayedMutable
var foo: Foo but Copying
var foo: Foo but CopyOnWrite
var foo: Foo but Observable
var foo: Foo but Synchronized
var foo: Foo but Indirect

var foo: Foo but UnsafeMutablePointerStored
var foo: Foo but UserDefaultStoring
var foo: Foo but DatabaseColumnFetched
var foo: Foo but XPCBacked
var foo: Foo but Boxed

Adjectives would also read nicely in attribute position (unsurprising, since most attributes are adjectives and English word order puts adjectives before the nouns they're attached to). Using a $ prefix as a strawman:

$Lazy var foo: Foo
$DelayedMutable var foo: Foo
$Copying var foo: Foo
$CopyOnWrite var foo: Foo
$Observable var foo: Foo
$Synchronized var foo: Foo
$Indirect var foo: Foo

$UnsafeMutablePointerStored var foo: Foo
$UserDefaultStoring var foo: Foo
$DatabaseColumnFetched var foo: Foo
$XPCBacked var foo: Foo
$Boxed var foo: Foo

However, this would have some undesirable features—particularly, if you needed to use DelegateName(…) syntax to initialize the delegate, the probably-very-complicated initialization code appears before the declaration rather than after. It would also be tempting to actually make these be attributes, but that could be confusing because unlike normal attributes, you can only have one delegate per property.

I don't have any specific suggestion or proposal I want to push from this set; I just thought this might be a useful way to frame syntax discussions about this feature.

(Note: Some of the syntaxes here were originally suggested by others, but I'm to blame for any bad opinions.)

11 Likes

You can just make the value property of the composite delegate type a delegate itself.

3 Likes

I still find via to look favorable here. It can be used with nouns and verbs alike.

var foo: Foo via Lazy
var foo: Foo via DelayedMutable
var foo: Foo via Copying
var foo: Foo via CopyOnWrite
var foo: Foo via Observable
var foo: Foo via Synchronized
var foo: Foo via Indirect

var foo: Foo via UnsafeMutablePointer
var foo: Foo via UserDefault
var foo: Foo via DatabaseColumn
var foo: Foo via XPC
var foo: Foo via Box
11 Likes

You wouldn't need to copy the entire implementation. Remember that property delegates are also ordinary types, and it's okay to use them that way:

@propertyDelegate
struct SynchronizedLazy<Value> {
  var underlyingDelegates: Synchronized<Lazy<Value>>
  // initializer matches Lazy's
  init(initialValue: @escaping @autoclosure () -> Value) {
    underlyingDelegates = Synchronized(initialValue: Lazy(initialValue: initialValue()))
  }
  var value: Value {
    get { return underlyingDelegates.value.value }
    set { underlyingDelegates.value.value = newValue }
  }
}
1 Like

Of course, stupid me! :roll_eyes:
That actually sounds quite good, thx!

1 Like

I don't see why this is a problem. Many attributes are exclusive with each other, and many attributes will be exclusive with delegates however we spell them. It seems to me that having two syntaxes for modifying declarations (@attribute and baremodifier) is better than having three. It also seems we've already agreed that we shouldn't allow multiple delegates to be applied to the same declaration, so there's no compositionality argument against attributes, either.

I don't think this syntax would be awkward. We already have attributes with arguments and this syntax looks fine on its own line when necessary. In fact I think this syntax has some distinct advantages. Placing modifiers before the declaration itself reads better (IMO) and works better in multi-line declarations. There is a loose analogy with the backing property that is declared which is nice. Lastly, it provides a logical way for the syntax to support direct initialization of the backing property.

Here are some examples:

public $Lazy var foo: Int

// line wrapping
public $Lazy(closure: {42 })
var foo: Int

// different access levels
private $Lazy(closure: {42 })
public var foo: Int

// different access levels
public $Lazy = anExistingLazyDelegate
var foo: Int

// error: redundant initialization
public $Lazy = anExistingLazyDelegate
var foo: Int = 42

$UnsafeMutablePointer(mutating: addressOfAnInt)
var someInt: Int
2 Likes

I like this method of reasoning. The delegates are themselves type names, and nouns allow consistency. I like through or via, best. by is still the runner‐up. in and but feel imprecise and awkward to me.

var foo: Foo through LazyComputation // All right, this one is awkward to noun‐ify.
var foo: Foo through DelayedMutation
var foo: Foo through Copying
var foo: Foo via CopyOnWrite
var foo: Foo via Observation
var foo: Foo by Synchronization
var foo: Foo by Indirection

var foo: Foo through UnsafeMutablePointer
var foo: Foo through UserDefault
var foo: Foo via DatabaseColumn
var foo: Foo via XPC
var foo: Foo by Box

I have some sketchy ideas about how we might be able to handle composition using generic property delegates. If I have time to think them through this weekend and they work out ok I'll write them up.

Is this going to be a sneaky form of higher-kinded types? :smile:

Anyway, syntax only makes a difference for composition of delegates if you're relying on using a syntax which naturally orders the delegates (e.g. using successive by clauses), which seems extremely subtle to be relying on — quick, which of by Lazy by Atomic or by Atomic by Lazy is the correct ordering? (The answer is neither, you need a custom implementation — unless Atomic unconditionally works by taking a lock, in which case whichever of those that puts Atomic on the outside is correct.)

2 Likes

The ideas are still sketchy, but hopefully it wouldn't need them because then it would only be a distant dream, not something that might work in the near future. :wink:

The general idea is to use a small number of protocols to allow delegates to declare how they participate in composition. Some delegates would simply not be directly composable and would still require manual composition. It wouldn't cover all cases of composition but maybe it would be able to cover a lot of common cases in a safer way (i.e. as long as the delegates involved are well designed composition wouldn't result in surprising behavior).

1 Like

Okay. If it's declarative if some way then it should work regardless of syntax, right? So it's purely additive to this proposal.

If it works out and we intend to pursue it I think it may motivate modifying the design of this proposal to use a PropertyDelegate protocol instead of an attribute (even if composition was left as a future direction).

2 Likes