[swift-evolution] Thoughts on property behavior out-of-band members
Brent Royal-Gordon
brent at architechies.com
Fri Feb 19 02:45:56 CST 2016
This was originally going to be part of my recent email on the SE-0030 review thread, but it got very long and convoluted, so I decided to split it off.
***
If we're going to use an `@runcible` syntax for properties, we should think about what will happen to behavior members.
The basic syntax itself is simple enough; `foo at runcible` and `foo. at runcible` are both reasonable options. (I slightly favor the dotless version because it doesn't make it look like you could capture `foo` in a variable and access `@runcible` from it, but that's a minor issue.)
More interesting is controlling the behavior members' visibility. The square bracket syntax had the advantage that you could put access control keywords alongside the declarations, like `[public resettable]`; with the `@` syntax that's now lost. We could echo `private(set)`, but the naive version of that is ridiculously wordy:
public public(@resettable) @resettable var counter = 0
Maybe we could drop the second `@resettable`—it's implied by the one in the access control list:
public public(@resettable) var counter = 0
And maybe we could drop the first `public`—it's implied, since the behavior can't be more visible than the property it's on—to get:
public(@resettable) var counter = 0
However, there are downsides to both of those. If we decide to support accessor vars with the `@json(key=)` syntax I described in the SE-0030 review thread, you start seeing things like `public(@json(key="fooBarBaz"))`, which kind of mixes unrelated things together.
And as for omitting the first `public`, well, I'm not entirely sure about the "can't be more visible" thing either. Sometimes you actually *don't* want to expose a property, but you *do* want to expose a behavior method. For instance:
public class NetworkDatabase {
private public(@resettable) var recordCache: NSCache
// You can't access the cache, but you can throw it out.
}
If that's the case, I'm not sure it's a good idea to have `public(@resettable)` imply the property is public, too.
***
Actually, I'm beginning to think that the `foo. at runcible` thing isn't a good idea at all.
One thing I've been thinking about lately is the ways we normally do behavior method-like things in Cocoa. For example, if you have a resettable property, you'll probably have something like this:
@property (assign) NSUInteger foo;
- (void)resetFoo;
It's pretty obvious, though, that we're not going to be able to Swiftify that—the clang importer isn't going to translate that into `@resettable var foo: Int`, or any other special syntax like that. It's just not going to be a realistic thing to detect and adjust during bridging.
And even if it could, that only exposes another problem: behaviors tie you *very* tightly to a particular implementation. If your property is `@resettable` and you decide you need to reset in a different way, you have no way to make that change without breaking all of your call sites. In particular, that means a public behavior in a resilient library can never be removed or even substituted for another behavior with a compatible interface.
So I'm thinking that we should try to sidestep the problem entirely and instead expose behavior members alongside the property they belong to, Cocoa-style.
Within the behavior, all of the members behave exactly as you would expect. But members with a visibility specifier are exposed on the instance using the behavior with the same visibility (capped by the property's access control), only with an uppercased version of the property's name appended to the member's name. Members with no visibility modifier, on the other hand, are not exposed outside the behavior.
For example, let's define, apply, and use `@resettable`:
public var behavior resettable<Value>: Value {
var value: Value = initialValue // no access control, so invisible
get { return value }
set { value = newValue }
public mutating func reset() { // visible at up to `public` scope
value = initialValue
}
}
struct Foo {
@resettable var bar: Int
// has a resetBar() method, which is internal because `bar` is internal.
}
var myFoo = Foo()
myFoo.resetBar()
With this in place, all behaviors are pure implementation details; you can remove them and manually implement the members they provided and nobody would be the wiser. You probably wouldn't even see behaviors in the generated headers.
(By the way, while I'm here, I'll note that it might be a good idea to bridge Objective-C null_resettable as Swift @resettable in both directions. That would mean the setter would have to accept `nil` in Objective-C only; I'm not sure if it's worth inventing a general mechanism for that.)
--
Brent Royal-Gordon
Architechies
More information about the swift-evolution
mailing list