Thoughts on property behavior out-of-band members


(Brent Royal-Gordon) #1

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@runcible` and `foo.@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.@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


(Curt Clifton) #2

This is an interesting direction, Brent. I really like the resilience benefits.

If we went this way, I wonder how we would deal with name spacing when we add behavior composition. That is, if two behaviors define members with the same name, how would we disambiguate between the members at the usage site? Concretely:

var behavior plistBacked<Value>: Value {
  func reset() { … }
  …
}

var behavior resettable<Value>: Value {
  func reset() { … }
  …
}

class Foo {
  @plistBacked @resettable var problemChild: String
}

let foo = Foo()
foo.ResetProblemChild // Which reset() is invoked?

Disambiguating at point of use would require exposing the behaviors in the interface, negating the benefits. It seems like the disambiguation could be done at the declaration site somehow, at which point I realize that we essentially have the diamond inheritance problem. Under this generated-methods approach, adopting multiple behaviors on a single property is essentially multiple inheritance.

Am I missing something?

Cheers,

Curt

···

On Feb 19, 2016, at 12:45 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

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@runcible` and `foo.@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.@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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Sean Heber) #3

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@runcible` and `foo.@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.)

A quick thought I had while reading this is what if the access syntax looked like this:

@resetable(foo).reset()
@runcible(spoon).finish = Material.silver

I think that would read pretty well - as if you’re accessing something that “wraps” the target var (which, basically, you are).

l8r
Sean


(Brent Royal-Gordon) #4

If we went this way, I wonder how we would deal with name spacing when we add behavior composition. That is, if two behaviors define members with the same name, how would we disambiguate between the members at the usage site?

It's a fair question. One possibility is make the `foo.@runcible` format available, but only in private scope with no way to widen it. The type applying the property would then be able to define a `resetFoo()` method which called whichever version it wanted to expose.

(In theory, you could do this for *all* members exposed on a behavior, and require the applying type to define cover members for whatever it wanted to expose. I don't like that idea, though; I think that would add a burdensome amount of boilerplate.)

···

--
Brent Royal-Gordon
Architechies