[swift-evolution] [Review] SE-0030 Property Behaviors

Jonathan Tang jonathan.d.tang at gmail.com
Thu Feb 11 04:12:29 CST 2016


On Wed, Feb 10, 2016 at 2:00 PM, Douglas Gregor via swift-evolution <
swift-evolution at swift.org> wrote:

> Hello Swift community,
>
> The review of SE-0030 "Property Behaviors" begins now and runs through
> February, 2016. The proposal is available here:
>
>
> https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md
> <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-proposal.md>
>
> Reviews are an important part of the Swift evolution process. All reviews
> should be sent to the swift-evolution mailing list at
>
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
> or, if you would like to keep your feedback private, directly to the
> review manager. When replying, please try to keep the proposal link at the
> top of the message:
>
> Proposal link:
>
>
> https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md
>
> Reply text
>
> Other replies
>
> <https://github.com/apple/swift-evolution#what-goes-into-a-review-1>What
> goes into a review?
>
> The goal of the review process is to improve the proposal under review
> through constructive criticism and, eventually, determine the direction of
> Swift. When writing your review, here are some questions you might want to
> answer in your review:
>
>    - What is your evaluation of the proposal?
>
> Would it be out-of-scope to propose extending this to functions?  I can
think of several use-cases:

1.) Providing both static & singleton accessors, for the convenience of
users of your library.  In a library I've been writing, I've found that all
my configuration calls take this form:

public static func useCoreData() -> Self { return
sharedInstance().useCoreData() }
public func useCoreData() { /* Code here; */ return self }

public static func useNotifications() -> Self { return
sharedInstance().useNotifications() }
public func useNotifications() { /* Code here; */ return self }

I'd love to be able to do (using the ... syntax for the proposed splat
operator being discussed in another thread):

public behavior func [StaticChainable] name(...ArgumentType) -> ResultType {
  public static func name(...args) -> Self { return
sharedInstance().name(...args) }
  public func name() { body; return self }
}

public func [StaticChainable] useCoreData() { /* Code here */ }
public func [StaticChainable] useNotifications() { /* Code here */ }

2.) Providing both "whole" and "parts" versions of Composite Pattern
methods.  Very often, you might have a tree structure where each element
has the same type (or conforms to the same protocol).  You want a method
with an optional first arg to specify which element you're operating on:

public func move(toElement: DOMElement, edge: EdgeType) { /* Code here */ }
public func move(childWithID id: String, toElement elem: DOMElement, edge:
EdgeType {
  children[id].move(toElement: elem, edge: edge)
}

public func hide() { /* Code here */ }
public func hide(childWithID id: String) { children[id].hide() }

public func show() { /* Code here */}
public func show(childWithID id: String) { children[id].show() }

vs.

public behavior func [Composite] name(...ArgumentType) -> ResultType where
Self: HasChildren {
  public func name(...args) { body }
  public func name(childWithID id: String, ...args) {
self.children[id].name(...args) }
}

public func [Composite] move(toElement: DOMElement, edge: EdgeType) { /*
Code here */ }
public func [Composite] show() { /* Code here */ }
public func [Composite] hide() { /* Code here */ }

3.) Or you have a number of convenience functions that differ only in what
helpers they invoke:

public func remove(elementAtIndex i: Index) { /* Code here */ }
public func remove(element: Generator.Element) { remove(elementAtIndex:
indexOf(element)) }
public func remove(firstMatchingPredicate pred: Generator.Element throws ->
Bool) {
   remove(elementAtIndex: indexOf(pred))
}

public func insertAfter(elementAtIndex i: Index, newElement:
Generator.Element) { /* Code here */ }
public func insertAfter(element: Generator.Element, newElement:
Generator.Element) {
   remove(elementAtIndex: indexOf(element))
}
public func insertAfter(firstMatchingPredicate pred: Generator.Element
throws -> Bool, newElement: Generator.Element) {
   remove(elementAtIndex: indexOf(pred))
}

public func insertBefore(elementAtIndex i: Index, newElement:
Generator.Element) { /* Code here */ }
public func insertBefore(element: Generator.Element, newElement:
Generator.Element) {
   remove(elementAtIndex: indexOf(element))
}
public func insertBefore(firstMatchingPredicate pred: Generator.Element
throws -> Bool, newElement: Generator.Element) {
   remove(elementAtIndex: indexOf(pred))
}

4.) Sometimes you need to define a combination of functions and related
properties, like in the Builder Pattern.  (Note the usage of "return self"
here - that's why this is not done with straight settable properties, it's
so the user can chain set calls together and avoid repeating the object
name):

private var foo : String
public func setFoo(newValue: String) -> Self { foo = newValue; return self }

private var bar : String
public func setBar(newValue: String) -> Self { bar = newValue; return self }

private var baz : String
public func setBaz(newValue: String) -> Self { baz = newValue; return self }

(Also this is a good opportunity to use the "set once" behavior mentioned
in the proposal, but I won't repeat that code here.)

public behavior var [Builder] name: Type = initialValue {
  private var name: Type = initialValue
  public func set##name(newValue: Type) -> Self { name = newValue; return
self }
}

public var [Builder] foo: String
public var [Builder] bar: String
public var [Builder] baz: String

Notice that this is a "var" behavior rather than a "func" behavior...I'd
envision the keyword matching the type of entity you *decorate*, not the
type of entity generated.  The declarations are public because the access
modifier of the declaration should set a *maximum* visibility, not a
minimum (i.e. you can always make individual instances of a behavior more
restrictive).  ## is a token-pasting operation similar to the C
preprocessor; it lets you construct a new name out of identifiers.  I'd
assume it would do automatic capitalization of the second argument to
preserve camelCasing.

5.) Sometimes you need a whole suite of classes, functions, and variables.
For example, think of event handlers:

public behavior class [Event] name {
  public class name##Event : Event {
    body

    func preventDefault() { ... }
  }

  public protocol name##Listener {
    public func on##name(event: name##Event)
  }

  private var listeners = [name##Listener]()
  public func add##name##Listener(listener: name##Listener) {
    listeners.append(listener)
  }
  public func remove##name##Listener(listener: name##Listener) {
    listeners.remove(atIndex: listeners.indexOf(listener))
  }

  func fire##name##Event(event: name##Event) {
    for listener in listeners {
      listener(event)
    }
  }
}

class [Event] Click {
  let windowCoordinates: Point
  let viewCoordinates: Point
}

class [Event] TouchDown {
  struct Touch {
    let coordinates: Point
    let force: Double
  }
  let touches: [Touch]
}


The way I'd envision this extension working is that the "behavior" keyword
could be followed by any top-level entity: let/var, func, class, struct,
enum.  The behavior then forms a template for entities to be generated and
expanded in place.  'name' is bound to the name of the entity, 'body' to
the statement body from the grammar.  Functions can define individual
arguments, types, and a result type, or can use whatever the outcome of the
splatting discussion is as "all remaining arguments, taken from the
instantiating func declaration."  Classes can also bind the supertype and
associated types.  I don't think it's necessary to be able to reach into
the body and parse it for individual statements; at least, none of the
use-cases I thought of require this.

All the scoping & storage concerns are as per the original proposal.  Code
within the body is not allowed to access code inside the behavior (beyond -
possibly? - calling public member functions or properties that are defined
by the behavior).  When generating names from behavior expansion, the
compiler checks for name conflicts with any other symbols.  Code within the
behavior is not allowed to inspect or destructure the body beyond the
bindings already described, but may call other methods either within the
behavior or within the hosting class if the class conforms to a protocol
(as per the existing proposal).  (Perhaps some form of limited
destructuring could be allowed as a generalization of accessor
requirements; allow binding names to functions with the 'func' keyword in
classes, binding to variables with 'let' in function bodies, etc.  Haven't
really thought this through, may end up being too complicated.)

A couple features of the existing proposal can be removed under this
generalization.  The syntax for calling a method of a property isn't
necessary; instead of x.[lazy].clear, define "func clear##name" inside the
behavior and call it as clearX().  (This also eliminates the need to have
debates over what that syntax should be. :-))  It also lifts the
restriction on 'let' parameters, as these are just another top-level entity
that could be expanded to a set of other entities.  They wouldn't allow
accessors or get/set blocks, however; the purpose of this would not be to
allow computed 'let' properties, it would be to fold in additional
functionality into these declarations.  One could imagine libraries that
allow clients to auto-delegate all method calls in a protocol to a
sub-object, for example:

public behavior let [DelegateCNContactPicker] name: Type = initialValue
      where Type: CNContactPickerDelegate, Self: CNContactPickerDelegate,  {
  public func contactPickerDidCancel(...args) {
name.contactPickerDidCancel(...args) }
  public func contactPicker(...args) { name.contactPicker(...args)
  // Should it include overloads implicitly with ...args?  Or should each
be overridden explicitly?
}

and then its use:

class MyViewController: CNContactPickerDelegate {
  private let [DelegateCNContactPicker] myObjectThatReallyHandlesIt =
MyContactPickerDelegate()
  // No forwarding methods needed, and no need to expose
myObjectThatReallyHandlesIt to the outside world.
}

I think this mechanism may also be able to subsume default methods on
protocols - instead of using an extension, you could use a behavior with a
protocol constraint.  I haven't really thought through the implications of
this, though, in particular the possibility of protocols with storage.  May
not be possible, in which case we wouldn't want to allow behaviors on
protocols.


>
>    - Is the problem being addressed significant enough to warrant a
>    change to Swift?
>
> Yes.  There are a number of use-cases here, and it generalizes several
special-case features that are currently baked into the compiler.


>
>    - Does this proposal fit well with the feel and direction of Swift?
>
> I think so, but I'll let people with a better sense of the feel and
direction of Swift make that decision.


>
>    - If you have used other languages or libraries with a similar
>    feature, how do you feel that this proposal compares to those?
>
> It reminds me a lot of Lisp macros, and in particular the ability to
define many entities at once or intercept the definition of those entities
(used to good effect in CLOS).  This is one of the best features of Lisp;
it lets you eliminate much of the boilerplate that shows up in
production-quality libraries for Java, C#, C++, etc.

Behaviors let you side-step many of the hygiene/gensym issues of Lisp
because they can't appear in unrestricted contexts the way Lisp macros
can.  Even generalized to allow function & class bodies, the set of symbols
that may be visible inside the behavior body is limited to keywords &
argument lists in the language definition itself; you don't need to build a
whole symbol table to see which symbols are visible inside the behavior,
and you don't need to worry about inserting arbitrary blocks of code into
arbitrary insertion points.  This limits the flexibility of behaviors a bit
(eg. you could never do something like loop or destructuring-bind as a
behavior), but it still seems like they cover a lot of the most annoying
boilerplate cases.

It's also somewhat similar to aspect-oriented programming, and to Python
decorators.  Seems to be both more and less powerful than these - more
powerful in that you can define multiple get/set/accessors at once and
scope any code required by them, and less powerful because as it currently
stands you can't intercept functions.


>
>    - How much effort did you put into your review? A glance, a quick
>    reading, or an in-depth study?
>
>
Read through the proposal fully, skimmed some of the responses, wrote up a
lot of examples.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160211/ce2f9d65/attachment.html>


More information about the swift-evolution mailing list