[swift-evolution] [Proposal] Improving operator requirements in protocols

Shawn Erickson shawnce at gmail.com
Mon May 2 18:26:59 CDT 2016


I like the direction this takes things! Thanks for hammering out a proposal
to get the ball rolling.

-Shawn

On Mon, May 2, 2016 at 9:51 AM Tony Allevato via swift-evolution <
swift-evolution at swift.org> wrote:

> I've written a proposal to formalize some of the discussion that was had
> over in the thread for the `FloatingPoint` protocol proposal regarding
> improvements to operator requirements in protocols that do not require
> named methods be added to the protocol and conforming types. Thanks to
> everyone who was participating in that discussion!
>
> The proposal can be viewed in this pull request
> <https://github.com/apple/swift-evolution/pull/283> and is pasted below.
>
>
> Improving operator requirements in protocols
>
>    - Proposal: SE-NNNN
>    <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-improving-operators-in-protocols.md>
>    - Author(s): Tony Allevato <https://github.com/allevato>
>    - Status: TBD
>    - Review manager: TBD
>
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#introduction>
> Introduction
>
> When a type conforms to a protocol that declares an operator as a
> requirement, that operator must be implemented as a global function defined
> outside of the conforming type. This can lead both to user confusion and to
> poor type checker performance since the global namespace is overcrowded
> with a large number of operator overloads. This proposal mitigates both of
> those issues by proposing that operators in protocols be declared
> statically (to change and clarify where the conforming type implements it)
> and use generic global trampoline operators (to reduce the global overload
> set that the type checker must search).
>
> Swift-evolution thread: Discussion about operators and protocols in the
> context of FloatingPoint
> <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160425/015807.html>
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#motivation>
> Motivation
>
> The proposal came about as a result of discussion about SE-0067: Enhanced
> Floating Point Protocols
> <https://github.com/apple/swift-evolution/blob/master/proposals/0067-floating-point-protocols.md>.
> To implement the numerous arithmetic and comparison operators, this
> protocol defined named instance methods for them and then implemented the
> global operator functions to delegate to them. For example,
>
> public protocol FloatingPoint {
>   func adding(rhs: Self) -> Self
>   // and others
> }
> public func + <T: FloatingPoint>(lhs: T, rhs: T) -> T {
>   return lhs.adding(rhs)
> }
>
> One of the motivating factors for these named methods was to make the
> operators generic and reduce the number of concrete global overloads, which
> would improve the type checker's performance compared to individual
> concrete overloads for each conforming type. Some concerns were raised
> about the use of named methods:
>
>    - They bloat the public interface. Every floating point type would
>    expose mutating and non-mutating methods for each arithmetic operation, as
>    well as non-mutating methods for the comparisons. We don't expect users to
>    actually call these methods directly but they must be present in the public
>    interface because they are requirements of the protocol. Therefore, they
>    would clutter API documentation and auto-complete lists and make the
>    properties and methods users actually want to use less discoverable.
>    - Swift's naming guidelines encourage the use of "terms of art" for
>    naming when it is appropriate. In this case, the operator itself is the
>    term of art. It feels odd to elevate (2.0).adding(2.0).isEqual(to: 4.0) to
>    the same first-class status as 2.0 + 2.0 == 4.0; this is the situation
>    that overloaded operators were made to prevent.
>    - Devising good names for the operators is tricky; the swift-evolution
>    list had a fair amount of bikeshedding about the naming and preposition
>    placement of isLessThanOrEqual(to:) in order to satisfy API
>    guidelines, for example.
>    - Having both an adding method and a + operator provides two ways for
>    the user to do the same thing. This may lead to confusion if users think
>    that the two ways of adding have slightly different semantics.
>
> Some contributors to the discussion list have expressed concerns about
> operators being members of protocols at all. I feel that removing them
> entirely would be a step backwards for the Swift language; a protocol is
> not simply a list of properties and methods that a type must implement, but
> rather a higher-level set of requirements. Just as properties, methods, and
> associated types are part of that requirement set, it makes sense that an
> arithmetic type, for example, would declare arithmetic operators among its
> requirements as well.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#inconsistency-in-the-current-operator-design-with-protocols>Inconsistency
> in the current operator design with protocols
>
> When a protocol declares an operator as a requirement, that requirement is
> located *inside* the protocol definition. For example, consider Equatable:
>
> protocol Equatable {
>   func ==(lhs: Self, rhs: Self) -> Bool
> }
>
> However, since operators are global functions, the actual implementation
> of that operator for a conforming type must be made *outside* the type
> definition. This can look particularly odd when extending an existing type
> to conform to an operator-only protocol:
>
> extension Foo: Equatable {}
> func ==(lhs: Foo, rhs: Foo) -> Bool {
>   // Implementation goes here
> }
>
> This is an odd inconsistency in the Swift language, driven by the fact
> that operators must be global functions. What's worse is that every
> concrete type that conforms to Equatable must provide the operator
> function at global scope. As the number of types conforming to this
> protocol increases, so does the workload of the compiler to perform type
> checking.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#proposed-solution>Proposed
> solution
>
> The solution described below is an *addition* to the Swift language. This
> document does *not* propose that the current way of defining operators be
> removed or changed at this time. Rather, we describe an addition that
> specifically provides improvements for protocol operator requirements.
>
> When a protocol wishes to declare operators that conforming types must
> implement, we propose adding the ability to declare operator requirements
> as static members of the protocol:
>
> protocol Equatable {
>   static func ==(lhs: Self, rhs: Self) -> Bool
> }
>
> Then, the protocol author is responsible for providing a generic global
> *trampoline* operator that is constrained by the protocol type and
> delegates to the static operator on that type:
>
> func == <T: Equatable>(lhs: T, rhs: T) -> Bool {
>   return T.==(lhs, rhs)
> }
>
> Types conforming to a protocol that contains static operators would
> implement the operators as static methods defined*within* the type:
>
> struct Foo: Equatable {
>   let value: Int
>
>   static func ==(lhs: Foo, rhs: Foo) -> Bool {
>     return lhs.value == rhs.value
>   }
> }
> let f1 = Foo(value: 5)let f2 = Foo(value: 10)let eq = (f1 == f2)
>
> When the compiler sees an equality expression between two Foos like the
> one above, it will call the global == <T: Equatable> function. Since T is
> bound to the type Foo in this case, that function simply delegates to the
> static methodFoo.==, which performs the actual comparison.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#benefits-of-this-approach>Benefits
> of this approach
>
> By using the name of the operator itself as the method, this approach
> avoids bloating the public interfaces of protocols and conforming types
> with additional named methods, reducing user confusion. This also will lead
> to better consistency going forward, as various authors of such protocols
> will not be providing their own method names.
>
> For a particular operator, this approach also reduces the number of global
> instances of that operator. Instead of there being one instance per
> concrete type conforming to that protocol, there is a single generic one
> per protocol. This should have a positive impact on type checker
> performance by splitting the lookup of an operator's implementation from
> searching through a very large set to searching through a much smaller set
> to find the generic trampoline and then using the bound type to quickly
> resolve the actual implementation.
>
> Similarly, this behavior allows users to be more explicit when referring
> to operator functions as first-class operations. Passing an operator
> function like + to a generic algorithm will still work with the
> trampoline operators, but in situations where type inference fails and the
> user needs to be more explicit about the types, being able to write T.+ is
> a cleaner and unambiguous shorthand compared to casting the global + to
> the appropriate function signature type.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#other-kinds-of-operators-prefix-postfix-assignment>Other
> kinds of operators (prefix, postfix, assignment)
>
> Static operator methods have the same signatures as their global
> counterparts. So, for example, prefix and postfix operators as well as
> assignment operators would be defined the way one would expect:
>
> protocol SomeProtocol {
>   static func +=(lhs: inout Self, rhs: Self)
>   static prefix func ~(value: Self) -> Self
>
>   // This one is deprecated, of course, but used here just to serve as an
>   // example.
>   static postfix func ++(value: inout Self) -> Self
> }
> // Trampolinesfunc += <T: SomeProtocol>(lhs: inout T, rhs T) {
>   T.+=(&lhs, rhs)
> }prefix func ~ <T: SomeProtocol>(value: T) -> T {
>   return T.~(value)
> }postfix func ++ <T: SomeProtocol>(value: inout T) -> T {
>   return T.++(&value)
> }
>
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#open-issue-class-types-and-inheritance>Open
> issue: Class types and inheritance
>
> While this approach works well for value types, these static operators may
> not work as expected for class types when inheritance is involved, and more
> work may be needed here.
>
> We can currently model the behavior we'd like to achieve by using a named
> eq method instead of the operator itself. (Note that we are *not* proposing
> that the function be named eq in the final design; this was done simply
> to perform the experiment with today's compiler.) Then we implement both
> the new method and the current == operator and compare their behaviors.
> For example:
>
> protocol ProposedEquatable {
>   static func eq(lhs: Self, _ rhs: Self) -> Bool
> }
> class Base: ProposedEquatable, Equatable {
>   static func eq(lhs: Base, _ rhs: Base) -> Bool {
>     print("Base.eq")
>     return true
>   }
> }func ==(lhs: Base, rhs: Base) -> Bool {
>   print("==(Base, Base)")
>   return true
> }
> class Subclass: Base {
>   static func eq(lhs: Subclass, _ rhs: Subclass) -> Bool {
>     print("Subclass.eq(Subclass, Subclass)")
>     return true
>   }
> }func ==(lhs: Subclass, rhs: Subclass) -> Bool {
>   print("==(Subclass, Subclass)")
>   return true
> }
> func eq<T: ProposedEquatable>(lhs: T, _ rhs: T) -> Bool {
>   return T.eq(lhs, rhs)
> }
> let x = Subclass()let y = Subclass()let z = y as Base
>
> eq(x, y)  // prints "Base.eq"
> eq(x, z)  // prints "Base.eq"
>
> x == y    // prints "==(Subclass, Subclass)"
> x == z    // prints "==(Base, Base)"
>
> The result of eq(x, y) was a bit surprising, since the generic argument T is
> bound to Subclass and there should be no dynamic dispatch at play there.
> (Is the issue that since Base is the class explicitly conforming to
> ProposedEquatable, this is locking in Self being bound as Base, causing
> that overload to be found in the compiler's search? Or is this a bug?)
>
> An attempt was also made to fix this using dynamic dispatch, by
> implementing eq as a class method instead of astatic method:
>
> protocol ProposedEquatable {
>   static func eq(lhs: Self, _ rhs: Self) -> Bool
> }
> class Base: ProposedEquatable, Equatable {
>   class func eq(lhs: Base, _ rhs: Base) -> Bool {
>     print("Base.eq")
>     return true
>   }
> }func ==(lhs: Base, rhs: Base) -> Bool {
>   print("==(Base, Base)")
>   return true
> }
> class Subclass: Base {
>   override class func eq(lhs: Base, _ rhs: Base) -> Bool {
>     print("Subclass.eq(Base, Base)")
>     return true
>   }
>   class func eq(lhs: Subclass, _ rhs: Subclass) -> Bool {
>     print("Subclass.eq(Subclass, Subclass)")
>     return true
>   }
> }func ==(lhs: Subclass, rhs: Subclass) -> Bool {
>   print("==(Subclass, Subclass)")
>   return true
> }
> func eq<T: ProposedEquatable>(lhs: T, _ rhs: T) -> Bool {
>   return T.eq(lhs, rhs)
> }
> let x = Subclass()let y = Subclass()let z = y as Base
>
> eq(x, y)  // prints "Subclass.eq(Base, Base)"
> eq(x, z)  // prints "Base.eq"
>
> x == y    // prints "==(Subclass, Subclass)"
> x == z    // prints "==(Base, Base)"
>
> This helped slightly, since at least it resulting in a method on the
> expected subclass being called, but this still means that anyone
> implementing this operator on subclasses would have to do some casting, and
> it's awkward that subclasses would be expected to write its operator in
> terms of the conforming base class.
>
> It should also be noted (code not provided here) that using instance
> methods does not solve this problem, presumably for the same
> dispatch-related reasons that the class methods called the version with
> Base arguments.
>
> However, the lack of multiple dispatch in Swift means that the operators
> we have today don't necessarily work the way a user would expect (for
> example, the x == z expression above), so it's debatable whether this is
> a significant concern.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#stretch-goal-automatically-generating-trampolines>Stretch
> goal: Automatically generating trampolines
>
> To further ease the use of protocol-defined operators, the compiler could
> automatically define the trampoline operator function at global scope. For
> example, a protocol and operator of the form
>
> protocol SomethingAddable {
>   static func +(lhs: Self, rhs: Self) -> Self
> }
>
> could automatically produce a generic global trampoline operator
> constrained by the protocol type (by substituting forSelf), with the same
> visibility as the protocol. The body of this would simply delegate to the
> static/class operator of the concrete type:
>
> func + <τ_0: SomethingAddable>(lhs: τ_0, rhs: τ_0) -> τ_0 {
>   return τ_0.+(lhs, rhs)
> }
>
> This approach could be extended for heterogeneous parameter lists:
>
> protocol IntegerAddable {
>   static func +(lhs: Self, rhs: Int) -> Self
> }
> // Auto-generated by the compilerfunc + <τ_0: IntegerAddable>(lhs: τ_0, rhs: Int) -> τ_0 {
>   return τ_0.+(lhs, rhs)
> }
>
> Additional generic constraints could even be propagated to the trampoline
> operator:
>
> protocol GenericAddable {
>   static func + <Arg: AnotherProtocol>(lhs: Self, rhs: Arg) -> Self
> }
> // Auto-generated by the compilerfunc + <τ_0: GenericAddable, τ_1: AnotherProtocol>(lhs: τ_0, rhs: τ_1) -> τ_0 {
>   return τ_0.+(lhs, rhs)
> }
>
> One major benefit of this is that neither the protocol author nor
> developers writing types conforming to that protocol would have to write
> *any* code that lives outside the protocol. This feels clean and
> consistent.
>
> This feature, however, may be more controversial, because:
>
>    - It involves the compiler implicitly generating glue code behind the
>    scenes, which is less discoverable and may be considered "magic".
>    - It raises the question of whether users should be allowed to define
>    their own trampolines that match the signatures of the auto-generated ones,
>    and if so, how the conflict is resolved.
>    - Defining the trampoline operator manually requires a trivial amount
>    of effort, and that effort is a one-time exercise by the protocol author.
>
> In addition, automatic trampoline generation is a much deeper change that
> would likely not be implementable in the Swift 3 timeline, so we will defer
> this for a future proposal and deeper discussion later.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#detailed-design>Detailed
> design
>
> Currently, the Swift language allows the use of operators as the names of
> global functions and of functions in protocols. This proposal is
> essentially asking to extend that list to include static/class methods of
> protocols and concrete types and to support referencing them in expressions
> using the . operator.
>
> Interestingly, the production rules themselves of the Swift grammar for
> function declarations *already* appear to support declaring static
> functions inside a protocol or other type with names that are operators. In
> fact, declaring a static operator function in a protocol works today (that
> is, the static modifier is ignored).
>
> However, defining such a function in a concrete type fails with the error operators
> are only allowed at global scope.This area
> <https://github.com/apple/swift/blob/797260939e1f9e453ab49a5cc6e0a7b40be61ec9/lib/Parse/ParseDecl.cpp#L4444>
>  of Parser::parseDeclFunc appears to be the likely place to make a change
> to allow this.
>
> In order to support *calling* a static operator using its name, the
> production rules for *explicit-member-expression* would need to be
> updated to support operators where they currently only support identifiers:
>
> *explicit-member-expression* → *postfix-expression* ­. *identifier*
> *­generic-argument-clause­**opt*­
> *explicit-member-expression* → *postfix-expression* ­­. *operator*
> *­generic-argument-clause­**opt*­
> *explicit-member-expression* → *postfix-expression* ­­. *­identifier* ­(
> *­argument-names­* )­
> *explicit-member-expression* → *postfix-expression* ­­. *­operator* ­(
> *­argument-names­* )­
>
> For consistency with other static members, we could consider modifying
> *implicit-member-expression* as well, but referring to an operator
> function with only a dot preceding it might look awkward:
>
> *implicit-member-expression* → . *­identifier­*
> *implicit-member-expression* → . *operator­*
>
> Open question: Are there any potential ambiguities between the dot in the
> member expression and dots in operators?
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#impact-on-existing-code>Impact
> on existing code
>
> The ability to declare operators as static/class functions inside a type
> is a new feature and would not affect existing code. Likewise, the ability
> to explicitly reference the operator function of a type (e.g., Int.+ or Int.+(5,
> 7) would not affect existing code.
>
> Changing the way operators are declared in protocols (static instead of
> non-static) would be a breaking change. However, since the syntax forms are
> mutually exclusive, we may wish to let them coëxist for the time being.
> That is, protocols that declare non-static operators would have them
> satisfied by global functions, and protocols that declare static operators
> would have them satisfied by static methods. While this provides two ways
> for developers to do the same thing, reducing breakage is a greater goal.
> We can consider deprecating non-static operators in protocols to lead
> developers to the new syntax and then remove it in a later version of Swift.
>
> Applying this change to the protocols already in the Swift standard
> library (such as Equatable) would be a breaking change, because it would
> change the way by which subtypes conform to that protocol. It might be
> possible to implement a quick fix that hoists a global operator function
> into the subtype's definition, either by making it static and moving the
> code itself or by wrapping it in an extension.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#alternatives-considered>Alternatives
> considered
>
> One alternative would be to do nothing. This would leave us with the
> problems cited above:
>
>    - Concrete types either provide their own global operator overloads,
>    increasing the workload of the type checker...
>    - ...*or* they define generic operators that delegate to named
>    methods, but those named methods bloat the public interface of the type.
>    - Furthermore, there is no consistency required for these named
>    methods among different types; each can define its own, and subtle
>    differences in naming can lead to user confusion.
>
> Another alternative would be that instead of using static methods,
> operators could be defined as instance methods on a type. For example,
>
> protocol SomeProtocol {
>   func +(rhs: Self) -> Self
> }
> struct SomeType: SomeProtocol {
>   func +(rhs: SomeType) -> SomeType { ... }
> }
> func + <T: SomeProtocol>(lhs: T, rhs: T) -> T {
>   return lhs.+(rhs)
> }
>
> There is not much to be gained by doing this, however. It does not solve
> the dynamic dispatch problem for classes described above, and it would
> require writing operator method signatures that differ from those of the
> global operators because the first argument instead becomes the implicit
> self. As a matter of style, when it doesn't necessarily seem appropriate
> to elevate one argument of an infix operator—especially one that is
> commutative—to the special status of "receiver" while the other remains an
> argument.
>
> Likewise, commutative operators with heterogeneous arguments are more
> awkward to implement if operators are instance methods. Consider a
> contrived example of a CustomStringProtocol type that supports
> concatenation with Characterusing the + operator, commutatively. With
> static operators and generic trampolines, both versions of the operator are
> declared in CustomStringProtocol, as one would expect:
>
> protocol CustomStringProtocol {
>   static func +(lhs: Self, rhs: Character) -> Self
>   static func +(lhs: Character, rhs: Self) -> Self
> }
> func + <T: CustomStringProtocol>(lhs: T, rhs: Character) -> T {
>   return T.+(lhs, rhs)
> }func + <T: CustomStringProtocol>(lhs: Character, rhs: T) -> T {
>   return T.+(lhs, rhs)
> }
>
> Likewise, the implementation of both operators would be contained entirely
> within the conforming types. If these were instance methods, it's unclear
> how the version that has the Character argument on the left-hand side
> would be expressed in the protocol, or how it would be implemented if an
> instance of Character were the receiver. Would it be an extension on the
> Character type? This would split the implementation of an operation that
> logically belongs to CustomStringProtocolacross two different locations
> in the code, which is something we're trying to avoid.
>
> <https://github.com/allevato/swift-evolution/blob/master/proposals/0000-improving-operators-in-protocols.md#acknowledgments>
> Acknowledgments
>
> Thanks to Chris Lattner and Dave Abrahams for contributing to the early
> discussions, particularly regarding the need to improve type checker
> performance by genericizing protocol-based operators.
> _______________________________________________
> 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/20160502/c12ea504/attachment.html>


More information about the swift-evolution mailing list