[Proposal] Improving operator requirements in protocols


(Tony Allevato) #1

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:

(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.


(Dave Abrahams) #2

Tony, thanks for writing this up!

···

on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

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
}

// Trampolines
func += <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)
}

How does one distinguish between calls to a static prefix operator and a
static postfix operator with the same name?

--
Dave


(Dave Abrahams) #3

Do we not have essentially all the same problems with classes, even with
today's Equatable? If not, what are the differences?

···

on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

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.

--
Dave


(Dave Abrahams) #4

FWIW, I think you have omitted an important (maybe even the main)
argument for this arrangement, which is not type checker performance but
improved user experience by getting piles of overloads out of the global
namespace.

···

on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

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.

--
Dave


(Chris Lattner) #5

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!

This is really great, thank you for pushing forward on this Tony! I am really optimistic that this will allow us to finally put to rest the weirdness we have with protocols and operators.

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
}

This sounds great. Operators are clearly “static" members, and adding the keyword here is great for consistency when you end up *implementing* an operator inline in a class. At that point, you want to see in the source where it is a “static” operator or a “class” operator (dynamic dispatch).

You don’t include it as part of your proposal, but I’d strongly suggest that we deprecate and/or remove the existing non-static operator requirement syntax. I really don’t like ending up in a situation where we have both “instance” and “static” operators, where these two cases have different declaration and use syntax.

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

They can also be ‘class’ methods in a class.

-Chris

···

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


(Dmitri Gribenko) #6

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 and is pasted below.

Hi Tony,

I'd like to post this feedback on behalf of Dave Abrahams, Maxim
Moiseev and myself.

We are in favor of your proposal, but we would like to request a few
modifications.

1. Could you remove the section about the stretch goal to generate
the trampolines automatically? We think that the proposal provides
enough value by itself, and we generating the trampolines will just
complicate the implementation, delaying the user model improvement.
This can come later.

2. The section about the classes is written in a way that seems to
imply that the proposal regresses the way Equatable works with
classes. We don't think this is the case. Could you change to just
acknowledge the problem and say that we are not fixing it in this
proposal?

You wrote to Dave:

We do have some problems today, such as above where using `==` on a `(Base, Subclass as Base)` pair ends up calling `==(Base, Base)` because we lack multiple dispatch. What surprised me though was that the `eq` call between two `Subclass` instances passed to the trampoline operator ended up calling `Base.eq`. I would have expected `Subclass.eq` to be called there since the generic argument `T` was bound to `Subclass`. Today, a non-generic `==(Subclass, Subclass)` operator *does* do the right thing.

The reason why the trampoline calls the ==(Base,Base) overload in
Subclass is because it is a part of the override chain for the base
class method that introduces the conformance.

The reason why the non-generic ==(Subclass,Subclass) operator does the
right thing is because it is a better match for overload resolution of
== at the callsite. But if you have a (Subclass as Base, Subclass as
Base) pair, even today it will call ==(Base,Base).

Therefore, we think that this section describes the exact problem that
we have today.

3. Could you explain in more detail how the name lookup works in the
proposal? In particular, it would be good to emphasize that regular
name lookup done when type checking an infix expression would not find
an operator defined as a type member.

4. What do you think about adding a rule to disallow defining member
operators that don't satisfy a protocol requirement?

Dmitri

···

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

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Tony Allevato) #7

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

The first stab I would take at is, what if we included the token "prefix"
or "suffix" before the operator name in the expression, like this?

    return T.prefix ++(&value)
    return T.postfix ++(&value)

But that could start to look like an invocation of "++" on a static
property "T.prefix". I haven't dug into the parser to determine if that
would even be feasible or not.

···

On Mon, May 2, 2016 at 1:20 PM Dave Abrahams via swift-evolution < swift-evolution@swift.org> wrote:

Tony, thanks for writing this up!

on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

> 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
> }
>
> // Trampolines
> func += <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)
> }

How does one distinguish between calls to a static prefix operator and a
static postfix operator with the same name?


(Tony Allevato) #8

We do have some problems today, such as above where using `==` on a `(Base,
Subclass as Base)` pair ends up calling `==(Base, Base)` because we lack
multiple dispatch. What surprised me though was that the `eq` call between
two `Subclass` instances passed to the trampoline operator ended up calling
`Base.eq`. I would have expected `Subclass.eq` to be called there since the
generic argument `T` was bound to `Subclass`. Today, a non-generic
`==(Subclass, Subclass)` operator *does* do the right thing.

I mainly called it out because the problems we have with `==` today are
slightly different than the problems encountered when testing out the new
model. If the decision is "operators with Self constraints are
fundamentally hard with classes and we don't necessarily expect them to
work consistently", then that's fine, but I wanted to make sure it wasn't a
hole in the proposal.

···

On Mon, May 2, 2016 at 1:25 PM Dave Abrahams via swift-evolution < swift-evolution@swift.org> wrote:

on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

> 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.

Do we not have essentially all the same problems with classes, even with
today's Equatable? If not, what are the differences?


(Tony Allevato) #9

I suppose I was treating that as a side effect of the type checking
improvements (piles of overloads removed == faster type checking), but I
can update the proposal to call it out more specifically.

···

On Mon, May 2, 2016 at 1:30 PM Dave Abrahams via swift-evolution < swift-evolution@swift.org> wrote:

on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

> 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.

FWIW, I think you have omitted an important (maybe even the main)
argument for this arrangement, which is not type checker performance but
improved user experience by getting piles of overloads out of the global
namespace.


(Shawn Erickson) #10

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@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dave Abrahams) #11

    Tony, thanks for writing this up!

    > 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
    > }
    >
    > // Trampolines
    > func += <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)
    > }

    How does one distinguish between calls to a static prefix operator and a
    static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

    lhs T.+= rhs
    
    T.++x
    x T.++

The first stab I would take at is, what if we included the token "prefix" or
"suffix" before the operator name in the expression, like this?

return T.prefix ++(&value)
return T.postfix ++(&value)

But that could start to look like an invocation of "++" on a static property
"T.prefix". I haven't dug into the parser to determine if that would even be
feasible or not.

These are not unreasonable either:

return prefix T.++(&value)
return postfix T.++(&value)

···

on Mon May 02 2016, Tony Allevato <allevato-AT-google.com> wrote:

On Mon, May 2, 2016 at 1:20 PM Dave Abrahams via swift-evolution > <swift-evolution@swift.org> wrote:
    on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:

--
Dave


(Chris Lattner) #12

I’m not sure if this is exactly right, but it seems close. I think that something like this is probably the best way to go, since it composes properly in arbitrary expressions. It does have a surface level weirdness to it, but it also "makes sense” in terms of how operators work.

-Chris

···

On May 2, 2016, at 1:56 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

   How does one distinguish between calls to a static prefix operator and a
   static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

   lhs T.+= rhs

   T.++x
   x T.++


(Chris Lattner) #13

Hmm, a thought going in a slightly different direction: if these static functions were called like any other function, there might not be a need for having special rules for parameter labels, which can then be freed to denote prefix and postfix operators. In other words, we could have:

* for infix operators, no labels, like so: `static func + (_ lhs: T, _ rhs: T)`, used like this: `T.+(1, 2)`
* for prefix and postfix operators, a label, like so: `static func + (prefixing value: T)`, used like this: `T.+(prefixing: 1)`

Using labels for this is pretty nice. "T.++(prefix: abc)” and “T.++(postfix: abc)” seem like they could work!

-Chris

···

On May 2, 2016, at 4:59 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Mon, May 2, 2016 at 6:26 PM, David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On May 2, 2016, at 5:58 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On May 2, 2016, at 1:56 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

  How does one distinguish between calls to a static prefix operator and a
  static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

  lhs T.+= rhs

  T.++x
  x T.++

I’m not sure if this is exactly right, but it seems close. I think that something like this is probably the best way to go, since it composes properly in arbitrary expressions. It does have a surface level weirdness to it, but it also "makes sense” in terms of how operators work.

Yeah… Maybe with parens?
T.++(x)
(x)T.++
Or is that worse?

- Dave Sweeris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #14

Yeah… Maybe with parens?
T.++(x)
(x)T.++
Or is that worse?

- Dave Sweeris

···

On May 2, 2016, at 5:58 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

On May 2, 2016, at 1:56 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org> wrote:

  How does one distinguish between calls to a static prefix operator and a
  static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

  lhs T.+= rhs

  T.++x
  x T.++

I’m not sure if this is exactly right, but it seems close. I think that something like this is probably the best way to go, since it composes properly in arbitrary expressions. It does have a surface level weirdness to it, but it also "makes sense” in terms of how operators work.


(Xiaodi Wu) #15

Hmm, a thought going in a slightly different direction: if these static
functions were called like any other function, there might not be a need
for having special rules for parameter labels, which can then be freed to
denote prefix and postfix operators. In other words, we could have:

* for infix operators, no labels, like so: `static func + (_ lhs: T, _ rhs:
T)`, used like this: `T.+(1, 2)`
* for prefix and postfix operators, a label, like so: `static func +
(prefixing value: T)`, used like this: `T.+(prefixing: 1)`

···

On Mon, May 2, 2016 at 6:26 PM, David Sweeris via swift-evolution < swift-evolution@swift.org> wrote:

On May 2, 2016, at 5:58 PM, Chris Lattner via swift-evolution < > swift-evolution@swift.org> wrote:

On May 2, 2016, at 1:56 PM, Dave Abrahams via swift-evolution < > swift-evolution@swift.org> wrote:

  How does one distinguish between calls to a static prefix operator and a
  static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

  lhs T.+= rhs

  T.++x
  x T.++

I’m not sure if this is exactly right, but it seems close. I think that
something like this is probably the best way to go, since it composes
properly in arbitrary expressions. It does have a surface level weirdness
to it, but it also "makes sense” in terms of how operators work.

Yeah… Maybe with parens?

T.++(x)
(x)T.++

Or is that worse?

- Dave Sweeris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #16

Yes, I think that’s better.

I’m not sure how I feel about the “ing” at the end of the labels, but conceptually your idea is clearly the way to go, IMHO.

- Dave Sweeris

···

On May 2, 2016, at 6:59 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Hmm, a thought going in a slightly different direction: if these static functions were called like any other function, there might not be a need for having special rules for parameter labels, which can then be freed to denote prefix and postfix operators. In other words, we could have:

* for infix operators, no labels, like so: `static func + (_ lhs: T, _ rhs: T)`, used like this: `T.+(1, 2)`
* for prefix and postfix operators, a label, like so: `static func + (prefixing value: T)`, used like this: `T.+(prefixing: 1)`

On Mon, May 2, 2016 at 6:26 PM, David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On May 2, 2016, at 5:58 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On May 2, 2016, at 1:56 PM, Dave Abrahams via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

  How does one distinguish between calls to a static prefix operator and a
  static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

  lhs T.+= rhs

  T.++x
  x T.++

I’m not sure if this is exactly right, but it seems close. I think that something like this is probably the best way to go, since it composes properly in arbitrary expressions. It does have a surface level weirdness to it, but it also "makes sense” in terms of how operators work.

Yeah… Maybe with parens?
T.++(x)
(x)T.++
Or is that worse?

- Dave Sweeris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #17

Maybe one minimalist approach could be to have these take two arguments as
though it's an infix operator where one of lhs or rhs is Void:

T.++(&value, ()) //postfix
T.++((), &value) // prefix

···

On Mon, May 2, 2016 at 15:56 Dave Abrahams via swift-evolution < swift-evolution@swift.org> wrote:

on Mon May 02 2016, Tony Allevato <allevato-AT-google.com> wrote:

> On Mon, May 2, 2016 at 1:20 PM Dave Abrahams via swift-evolution > > <swift-evolution@swift.org> wrote:
>
> Tony, thanks for writing this up!
>
> on Mon May 02 2016, Tony Allevato <swift-evolution@swift.org> wrote:
>
> > 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
> > }
> >
> > // Trampolines
> > func += <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)
> > }
>
> How does one distinguish between calls to a static prefix operator
and a
> static postfix operator with the same name?
>
> Ah, that's a tricky one that I don't have an immediate answer to, so I'm
> definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

    lhs T.+= rhs

    T.++x
    x T.++

> The first stab I would take at is, what if we included the token
"prefix" or
> "suffix" before the operator name in the expression, like this?
>
> return T.prefix ++(&value)
> return T.postfix ++(&value)
>
> But that could start to look like an invocation of "++" on a static
property
> "T.prefix". I haven't dug into the parser to determine if that would
even be
> feasible or not.

These are not unreasonable either:

return prefix T.++(&value)
return postfix T.++(&value)

--
Dave
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tony Allevato) #18

>
> How does one distinguish between calls to a static prefix operator
and a
> static postfix operator with the same name?
>
> Ah, that's a tricky one that I don't have an immediate answer to, so I'm
> definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

    lhs T.+= rhs

    T.++x
    x T.++

Even though this seemed a little odd when you first suggested it in the
other thread, it's growing on me now that we have to deal with this
ambiguity.

Fortunately the only situation I foresee where a person would explicitly
use this notation would be in the trampoline operator, so even if it looks
a little odd, at least it's isolated.

> The first stab I would take at is, what if we included the token
"prefix" or
> "suffix" before the operator name in the expression, like this?
>
> return T.prefix ++(&value)
> return T.postfix ++(&value)
>
> But that could start to look like an invocation of "++" on a static
property
> "T.prefix". I haven't dug into the parser to determine if that would
even be
> feasible or not.

These are not unreasonable either:

return prefix T.++(&value)
return postfix T.++(&value)

I could get behind this one as well.

···

On Mon, May 2, 2016 at 1:56 PM Dave Abrahams <dabrahams@apple.com> wrote:

on Mon May 02 2016, Tony Allevato <allevato-AT-google.com> wrote:
> On Mon, May 2, 2016 at 1:20 PM Dave Abrahams via swift-evolution > > <swift-evolution@swift.org> wrote:


(Tony Allevato) #19

I like this suggestion a lot. Using labels has a nice elegance to it and
makes the usage very clear. If we drop the -ing like Chris suggests, it
also aligns nicely with the same keywords used to define them.

I'm not a parser expert, but this seems like it might be easier to parse
than fully-qualified operators. It is visually, at least.

···

On Mon, May 2, 2016 at 4:59 PM Xiaodi Wu via swift-evolution < swift-evolution@swift.org> wrote:

Hmm, a thought going in a slightly different direction: if these static
functions were called like any other function, there might not be a need
for having special rules for parameter labels, which can then be freed to
denote prefix and postfix operators. In other words, we could have:

* for infix operators, no labels, like so: `static func + (_ lhs: T, _
rhs: T)`, used like this: `T.+(1, 2)`
* for prefix and postfix operators, a label, like so: `static func +
(prefixing value: T)`, used like this: `T.+(prefixing: 1)`

On Mon, May 2, 2016 at 6:26 PM, David Sweeris via swift-evolution < > swift-evolution@swift.org> wrote:

On May 2, 2016, at 5:58 PM, Chris Lattner via swift-evolution < >> swift-evolution@swift.org> wrote:

On May 2, 2016, at 1:56 PM, Dave Abrahams via swift-evolution < >> swift-evolution@swift.org> wrote:

  How does one distinguish between calls to a static prefix operator and a
  static postfix operator with the same name?

Ah, that's a tricky one that I don't have an immediate answer to, so I'm
definitely open to creative thoughts here.

One possibility: just use “qualified operator” notation.

  lhs T.+= rhs

  T.++x
  x T.++

I’m not sure if this is exactly right, but it seems close. I think that
something like this is probably the best way to go, since it composes
properly in arbitrary expressions. It does have a surface level weirdness
to it, but it also "makes sense” in terms of how operators work.

Yeah… Maybe with parens?

T.++(x)
(x)T.++

Or is that worse?

- Dave Sweeris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Pyry Jahkola) #20

FWIW, the problems with class inheritance and Equatable can be fixed by changing the definition of Equatable into using an associatedtype for the RHS:

protocol Equatable {
    typealias EqualSelf = Self
    func == (lhs: Self, rhs: EqualSelf) -> Bool
    func != (lhs: Self, rhs: EqualSelf) -> Bool
}
func != <T : Equatable>(lhs: T, rhs: T.EqualSelf) -> Bool {
    return !(lhs == rhs)
}

This way, a generic function of type Subclass will expect a comparison of type `(Subclass, Subclass.EqualSelf) -> Bool`, i.e. `(Subclass, Base) -> Bool`, so it'll do the right comparison.

I'm not proposing this change because I haven't found but contrived use for it. But if there's demand (and a real use case) for e.g. making an existential of Equatable (say, a struct AnyEquatable), then this could be the fix.

Value types will obviously keep working just like before, since T and T.EqualSelf are synonymous for them.

— Pyry

···

We do have some problems today, such as above where using `==` on a `(Base, Subclass as Base)` pair ends up calling `==(Base, Base)` because we lack multiple dispatch. What surprised me though was that the `eq` call between two `Subclass` instances passed to the trampoline operator ended up calling `Base.eq`. I would have expected `Subclass.eq` to be called there since the generic argument `T` was bound to `Subclass`. Today, a non-generic `==(Subclass, Subclass)` operator *does* do the right thing.

The reason why the trampoline calls the ==(Base,Base) overload in
Subclass is because it is a part of the override chain for the base
class method that introduces the conformance.

The reason why the non-generic ==(Subclass,Subclass) operator does the
right thing is because it is a better match for overload resolution of
== at the callsite. But if you have a (Subclass as Base, Subclass as
Base) pair, even today it will call ==(Base,Base).