[swift-evolution] [Proposal] Property behaviors

plx plxswift at icloud.com
Fri Dec 18 11:43:52 CST 2015


I am excited by the general direction but I have some concerns about the scope of the design at this time; specifically, it seems like it would benefit a lot from having some flexible-and-efficient way for behaviors to “reach upward” back into their container from within their custom methods (without forcing the caller to pass-in the container to each such call, of course).

I took a stab at mocking up one of the behaviors I’d like to be able to write and hit a lot of roadblocks due to the above; I’ve included it below with some commentary. 

Even though this is perhaps a rather extreme/niche behavior to want to implement, I think the issues it encountered are actually general enough that other useful behaviors will also encounter them under the proposal as sketched.

Here’s the sample use, starting with motivation.

For some APIs — e.g. CoreBluetooth — you often wind up with highly-stateful objects that receive callbacks on a specific queue, and typically also do their state-maintenance while on that same queue; these objects typically also have a “public API” with methods that are only meant for use while off the queue (e.g. from the main queue, to update the UI…).

You thus wind up with each method-and-property pretty clearly being one and only one of these:

- “on-queue”, e.g. *only* meant for use while on the object’s queue
- “off-queue”, e.g. *only* meant for use while off the object’s queue

…with concurrency-and-synchronization logic essentially amounting to only calling / using each method-and-property while appropriately on/off queue.

For a concrete example, for an implementer of CBCentralManagerDelegate:

- all the CBCentralManagerDelegate methods are "on-queue"
- all the BT-state-management methods (called in reaction to BT events) are also “on-queue”
- the public methods (e.g. for UI use, or for asking the object to do stuff) are “off-queue”
- some of the basic properties (status, is-peripheral-foo-connected?) are oddballs, and get:
  - private backing properties for use/update while on-queue
  - public off-queue accessors that do a dispatch_sync, read the backing property, and return it

…and so on.

This can all be handled today "by hand” — it just requires being careful — but it’d be nice to have a custom behavior that would streamline both the implementation of on/off queue access for properties, and make each site-of-use more self-documenting/self-verifying vis-a-vis on/off-queue status.

Here’s my best attempt (issues flagged in ALL-CAPS):

/// Object assumed to have private queue it uses for synchronization.
protocol PrivateQueueOwner : class {

  // we don’t want to leak the actual queue to the wider world,
  // so we have to bubble these up to the public API:
  func dispatchSync<R>(@noescape action: () -> R) -> R
  func dispatchBarrierSync<R>(@noescape action: () -> R) -> R
  func dispatchAsync(action: () -> ())
  func dispatchBarrierAsync(action: () -> ())

  // we assume we are managing our queues s.t. we can
  // actually get the below to work reliably:
  func isOnPrivateQueue() -> Bool

}

/// Behavior used to enforce a particular use-pattern around
/// a property of an object that uses a private queue for synchronization:
struct QueueAccess<Value> {
  var value: Value
  
  // THIS PART IS ONLY-KINDA OK:
  subscript<Container:PrivateQueueOwner>(varIn container: Container> {
    get {
      if container.isOnPrivateQueue() {
        return self.value
      } else {
        return self.container.dispatchSync() {
          return self.value
          // ^ DOES THIS ACTUALLY WORK AS I’D WANT, IF I AM A STRUCT?
        }
      }
    }
    set {
      if container.isOnPrivateQueue() { 
        self.value = newValue
      } else {
        container.dispatchBarrierAsync() {
          self.value = newValue
          // ^ DOES THIS ACTUALLY WORK AS I’D WANT, IF I AM A STRUCT?
        }
      }
    }
  }

  // EVERYTHING FROM HERE ON DOWN IS MOSTLY PROBLEMATIC:

  func onQueueUpdate(newValue: Value) { 
    assert(self.container.isOnPrivateQueue()) // <- HOW?
    self.value = newValue
  }
  
  func offQueueUpdate(newValue: Value) {
    assert(self.container.isOffPrivateQueue()) // <- HOW?
    self.container.dispatchBarrierAsync() { // <- HOW?
       self.value = newValue
       // ^ DOES THIS EVEN WORK IF I AM A STRUCT?   
    }
  }

  func offQueueAccess() -> Value {
    assert(self.container.isOffPrivateQueue()) // <- HOW?
    return self.container.dispatchSync() { // <- HOW?
      return self.value
    }
  }

  func onQueueAcccess() -> Value {
    assert(self.container.isOnPrivateQueue()) // <- HOW?
    return self.value
  }

  func offQueueAccess<R>(@noescape transform: (Value) -> R) -> R {
    assert(self.container.isOffPrivateQueue()) // <- HOW?
    return self.container.dispatchSync() { // <- HOW?
      return transform(self.value)
    }
  }

  func onQueueAcccess<R>(@noescape transform: (Value) -> R) -> R {
    assert(self.container.isOnPrivateQueue()) // <- HOW?
    return transform(self.value)
  }

}

….which if it was implementable, would wind up used like so:

public class BTCentralManagerController : NSObject, CBCentralManagerDelegate, PrivateQueueOwner {

  internal lazy var centralManager: CBCentralManager = CBCentralManager(
    delegate: self, 
    queue: self.privateQueue, 
    options: self.centralManagerOptions()
  )
  
  private let privateQueue: dispatch_queue_t 
  
  public private(set) var (queueAccess) centralManagerState: CBCentralManagerState = .Unknown
  internal private(set) var (queueAccess) peripheralControllers: [NSUUID:BTPeripheralController] = [:]
  
  // internal API sample:

  func centralManagerDidUpdateState(_ central: CBCentralManager) {
    self.centralManagerState.queueAccess.onQueueUpdate(central.state)
  }
 
  // public API sample

  public func peripheralControllerForUUID(uuid: NSUUID) -> BTPeripheralController? {
    // this is an explicitly “off-queue” method:
    self.currentState.queueAccess.offQueueAccess() {
      return $0[uuid]
    }
  }

}

…giving us:

- safe defaults for attempted direct-property access
- self-dcoumenting/self-validating customized getters-and-setters for all internal-use scenarios

But, as the # of ALL-CAPS comments should indicate, this behavior seems *well beyond* what the proposal can provide in a natural way (e.g. we can get closer by passing-in the container to each method, but that’s a lot less natural and a lot clunkier).
 
Moreover, even if the “ability to access the container” were to be addressed, I also don’t like having to use a protocol like `PrivateQueueOwner` to make my `queueAccess` behavior re-usable; at least at present, adopted-protocol visibility is the same visibility as the type itself, so that e.g. if a public class conforms to `PrivateQueueOwner` then all the methods in `PrivateQueueOwner` are also public.

This is undesirable, as although I’d want such classes to be public, I wouldn’t want such low-level implementation details to be part of their public API.

Ideally, rather than forcing the container to adopt a protocol, I could instead do something *more* like this:

public class BTCentralManagerController : NSObject, CBCentralManagerDelegate {

  private let privateQueue: dispatch_queue_t 
  
  // configures `queueAccess` behavior to use `self.privateQueue` (with on-queue check also as-specified)
  // ...note that if this becomes possible, the syntax might need to change, b/c the below is not very readable!
  public private(set) var (queueAccess(queue: `self.privateQueue`, onQueue: `self.isOnPrivateQueue()`)) currentState: CBCentralManagerState = .Unknown
  internal private(set) var (queueAccess(queue: `self.privateQueue`, onQueue: `self.isOnPrivateQueue()`)) peripheralControllers: [NSUUID:BTPeripheralController] = [:]

}

// which somehow interacts with a declaration like this:
struct QueueAccess<Value> {

  var value: Value

  // declaration stating we expect a `queue` expression during configuration, with the 
  // following type (and the accessor automagically-synthesized via compiler magic)
  container var queue: dispatch_queue_t { get }

  // declaration stating we expect an `onQueue` expression during configuration, with the 
  // following type (and the implementation automagically-synthesized via compiler magic)
  container func onQueue() -> Bool

   func onQueueUpdate(newValue: Value) { 
    assert(self.onQueue()) 
    self.value = newValue
  }
  
  func offQueueUpdate(newValue: Value) {
    assert(!self.onQueue())
    dispatch_barrier_async(self.queue) {
      self.value  = newValue
      // ^ NOTE: this may still be a problem for struct-based behaviors...?
    }
  }
 
  // etc...
 
}

…which, if possible, would obviously make behaviors a lot more specialized than they are under the current proposal (e.g. they would seemingly need a lot of specialized help from the compiler to be able to access those variables without either creating a strong reference to the container or wasting a lot of space with redundant stored properties, and might be their own specialized type, rather than an otherwise-vanilla struct-or-class as per the current proposal). 

But, if the above were possible, the behaviors would be a *lot* more re-usable, and it’d be unnecessary to have the container adopt a particular protocol.

Note also that even if the “unwanted API pollution” issue were resolved — e.g. by making it possible to somehow privately-adopt a protocol, or equivalent — there’d still be the issue of getting efficient access to the container to address, if these use cases are to be supported.

So that’s my reaction; if you read it this far, thanks for your attention.

I’d *completely* understand if the reaction here is simply that such uses are out-of-scope for this proposal; that seems perfectly reasonable!

But keep in mind, the general issue of reaching-up from custom methods of behaviors can show up in simpler contexts:

// convenience logic:
private extension UIViewController {

  func viewHasEnteredWindow() -> Bool {
     return self.viewIfLoaded()?.window != nil ?? false
  }
}

// custom behavior:
struct MustSetBeforeVisibility<Value> {
  value: Value?
  
  // THIS PART OK:
  subscript<Container:UIViewController>(varIn container: Container> -> Value? {
    get {
      if container.viewHasEnteredWindow() {
        guard let v = self.value else { 
          fatalError(“Should’ve set property \(self) by now, b/c our VC’s view is in the window (vc: \(container))”)
        }
        return v
      } else {
        return self.value // perfectly-cromulent to be unset at this point in lifecycle
      }
    }
    set {
      if !container.viewHasEnteredWindow() {
        self.value = newValue
      } else {
        fatalError(“Not meant to be set after we have become potentially-visible!")
      }
    }
  }

  // HOW TO DO THIS PART:
  /// non-optional convenience accessor; only meant for use once our view has 
  /// become potentially-visible
  func direct() -> Value {
    if !container.viewHasEnteredWindow() { // <- HOW?
       fatalError(“Trying to do direct-access on \(self) too soon!")
    } else if let v = self.value {
       return v
    } else {
       fatalError(“Trying to do direct-access, but never set the value for \(self) appropriately!")
    }
  }

}

…which is basically another take on a “smarter” implicitly-unwrapped-optional. You don’t *need* a function like `direct()`, but you might want it, and it might be nice to be able to differentiate “using too soon” and “forgot to set the value”.


> On Dec 17, 2015, at 11:37 AM, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Hi everyone. Chris stole my thunder already—yeah, I've been working on a design for allowing properties to be extended with user-defined delegates^W behaviors. Here's a draft proposal that I'd like to open up for broader discussion. Thanks for taking a look!
> 
> -Joe
> 
> https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3 <https://gist.github.com/jckarter/f3d392cf183c6b2b2ac3>
> 
> Lazy
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#lazy>
> Memoization
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#memoization>
> Delayed Initialization
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#delayedinitialization>
> Resettable properties
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#resettableproperties>
> Synchronized Property Access
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#synchronizedpropertyaccess>
> NSCopying
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#nscopying>
> Referencing Properties with Pointers
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#referencingpropertieswithpointers>
> Property Observers
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#propertyobservers>
> Detailed design
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#detaileddesign>
> Impact on existing code
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#impactonexistingcode>
> Alternatives considered/to consider
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#alternativesconsideredtoconsider>Declaration syntax
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#declarationsyntax>
> Syntax for accessing the backing property
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#syntaxforaccessingthebackingproperty>
> Defining behavior requirements using a protocol
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#definingbehaviorrequirementsusingaprotocol>
> A behavior declaration
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#abehaviordeclaration>
> Naming convention for behaviors
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#namingconventionforbehaviors>
> TODO
>  <file:///Users/jgroff/src/s/swift-evolution/proposals/XXXX-property-delegates.md#todo>
> 
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151218/6711fdff/attachment.html>


More information about the swift-evolution mailing list