[swift-evolution] [Proposal] Property behaviors

Kevin Ballard kevin at sb.org
Thu Dec 17 19:08:04 CST 2015


On Thu, Dec 17, 2015, at 04:36 PM, Joe Groff wrote:
>>> Resettable properties
>>
>> The implementation here is a bit weird. If the property is nil, it
>> invokes the initializer expression, every single time it's accessed.
>> And the underlying value is optional. This is really implemented
>> basically like a lazy property that doesn't automatically initialize
>> itself.
>>
>> Instead I'd expect a resettable property to have eager-
>> initialization, and to just eagerly re-initialize the property
>> whenever it's reset. This way the underlying storage isn't Optional,
>> the initializer expression is invoked at more predictable times, and
>> it only invokes the initializer once per reset.
>>
>> The problem with this change is the initializer expression needs to
>> be provided to the behavior when reset() is invoked rather than when
>> the getter/setter is called.
>
> Yeah, the implementation here is admittedly contorted around the fact
> reset() can't receive the initializer expression directly, and we
> don't want to store it anywhere in the property.

We definitely can't have behaviors storing closures. Any closure that
references `self` would then become a retain cycle. And of course for
value types the closure would have captured an outdated copy of self. I
think we should state upfront that any closure type given to a behavior
should be required to be @noescape.

Something I meant to suggest but forgot was the idea that maybe a
Behavior at runtime should be a struct that's initialized every time it
needs to do something, and takes init arguments for the initializer and
any relevant accessors (e.g. willSet, didSet). This is similar to how
you already have it optionally take that stuff, but if the struct is
initialized fresh on each access, with the initializer and accessors
being provided as @noescape bound closures (implemented by hidden
function declarations), then even things like reset() will have access
to all the info it needs to do whatever it wants. Although I'm not quite
sure offhand how to model reset() being able to mutate the property if
the behavior doesn't keep the property storage inline (which, as I
previously mentioned, has composability problems). Maybe each method
would actually take an inout parameter as the first argument that it can
use to read from/write to the underlying property?

Alternatively, properties could not even have a runtime struct
representation but just be collections of static functions. Each
function would take an inout parameter for the underlying property, and
then optional parameters for the various things the user can specify
(initializer and accessors). This would make more sense when using the
explicit `behavior` keyword because you'd just model this as functions
nested inside the behavior scope, with no type declaration anywhere.

>>
>>> The backing property has internal visibility by default
>>
>> In most cases I'd recommend private by default. Just because I have
>> an internal property doesn't mean the underlying implementation
>> detail should be internal. In 100% of the cases where I've written a
>> computed property backed by a second stored property (typically named
>> with a _ prefix), the stored property is always private, because
>> nobody has any business looking at it except for the class/struct it
>> belongs to.
>>
>> Although actually, having said that, there's at least one behavior
>> (resettable) that only makes sense if it's just as visible as the
>> property itself (e.g. so it should be public on a public property).
>>
>> And come to think of it, just because the class designer didn't
>> anticipate a desire to access the underlying storage of a lazy
>> property (e.g. to check if it's been initialized yet) doesn't mean
>> the user of the property doesn't have a reason to get at that.
>>
>> So I'm actually now leaning to making it default to the same
>> accessibility as the property itself (e.g. public, if the property
>> is public). Any behaviors that have internal implementation details
>> that should never be exposed (e.g. memoized should never expose its
>> box, but maybe it should expose an accessor to check if it's
>> initialized) can mark those properties/methods as internal or
>> private and that accessibility modifier would be obeyed. Which is to
>> say, the behavior itself should always be accessible on a property,
>> but implementation details of the behavior are subject to the normal
>> accessibility rules there.
>
> I don't think we can default to anything more than internal. Public
> behaviors become an API liability you can never resiliently change,
> and we generally design to ensure that API publication is a conscious
> design decision.

I'm inclined to say that some behaviors (like resettable and
synchronized) are conceptually part of the public API (assuming the
property is public), and some behaviors (like lazy and property
observers) aren't.

With the `behavior` keyword we could let each behavior decide for itself
what its default accessibility level should be. That is a bit unusual,
but otherwise everyone who uses `resettable` on a public property will
actually have to say `public resettable` for it to make sense, and
similarly `synchronized` should typically be visible to consumers of the
API (even though there's no actual API on the behavior itself to use)
because the threading behavior of an API is part of its public contract.
Of course you could always say `private synchronized` if you wanted to
override this.

-Kevin Ballard
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151217/66a7f0a5/attachment.html>


More information about the swift-evolution mailing list