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 [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
