[swift-evolution] [Proposal] Improving operator requirements in protocols
Tony Allevato
allevato at google.com
Mon May 2 11:44:20 CDT 2016
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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160502/b1cabbb0/attachment.html>
More information about the swift-evolution
mailing list