[Review] SE-0091: Improving operator requirements in protocols


(Chris Lattner) #1

Hello Swift community,

The review of "SE-0091: Improving operator requirements in protocols" begins now and runs through May 23. The proposal is available here:

  https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md

Reviews are an important part of the Swift evolution process. All reviews should be sent to the swift-evolution mailing list at

  https://lists.swift.org/mailman/listinfo/swift-evolution

or, if you would like to keep your feedback private, directly to the review manager.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and contribute to the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  * What is your evaluation of the proposal?
  * Is the problem being addressed significant enough to warrant a change to Swift?
  * Does this proposal fit well with the feel and direction of Swift?
  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

  https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

-Chris Lattner
Review Manager


(Brent Royal-Gordon) #2

The review of "SE-0091: Improving operator requirements in protocols" begins now and runs through May 23. The proposal is available here:

  https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md

  * What is your evaluation of the proposal?

I'm in favor, with one small concern:

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)
  }

This trampoline operator, and all of the others in the proposal, appears to be 100% pure boilerplate. Could Swift generate them for us?

(Specifically, I'm suggesting that if protocol P defines a `static func $!(args) -> ret`, Swift should automatically generate a global `func #! <T: P> (args) -> ret`, substituting `T` for any `Self`s among the parameters or return values.)

  * Is the problem being addressed significant enough to warrant a change to Swift?

Yes. Speeding up type inference is a Good Thing™.

  * Does this proposal fit well with the feel and direction of Swift?

It *is* a little odd and hacky, particularly on the class side. I'm not saying it's any worse than the status quo, but it doesn't feel like a full solution yet.

  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Short of Perl 6's solution, which is to introduce full multimethods into the language, I don't think I've seen much that's better. (Other than the trampoline boilerplate, that is.)

  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading.

···

--
Brent Royal-Gordon
Architechies


(Matthew Johnson) #3

  * What is your evaluation of the proposal?

Mostly an enthusiastic +1. This really cleans up one aspect of Swift that seemed un-Swifty.

Like others, I would prefer to see the trampoline automatically generated. This is not just a boilerplate issue. Consider the following from the proposal:

This would only search the global namespace and find * <T: FooProtocol> as a match. The name lookup will not search for operators defined as type members, so the concrete implementation of Foo.* would be ignored; the trampoline operator would explicitly call it. The only way to reference a type member operator is to fully-qualify it with its type's name, and it may only be called using function-call syntax.

This implies that a user could implement a more specific overload of global * for a concrete type (such as one that takes (Foo, Foo) as its arguments), which would bypass the trampoline operator. While we would not recommend that a user do this, it's not necessarily compelling to forbid it either.

The consequences of this allow for confusing ambiguity. I can implement a type that conforms to two or more unrelated protocols that both have the same operator requirement. This means there will be an ambiguity issue when top-level lookup sees multiple trampoline definitions for which my type is an equally good match.

The only way to resolve the ambiguity is to manually define a top-level operator definition for my concrete type. This has great potential to be confusing to users. It also means that by retroactively conforming a type to a new protocol I can introduce ambiguity into code that used to work as expected, which would be even more confusing and frustrating. The most unfortunate aspect of this is that we all know the trampolines *should* all have the same implementation that just calls through to the static / class operator method defined on the type itself.

This potential for ambiguity does not currently exist in Swift precisely because each type is required to define its own top-level operator implementation which is always going to be the best match during overload resolution.

If the compiler manages trampolines it can take responsibility for 1) ensuring that they all simply call through to the static implementation as expected and 2) provide trampolines for concrete types to resolve ambiguity when necessary.

Maybe these ambiguity issues are acceptable for a short time if we just can’t get automatic trampolines into Swift 3, but we should consider them carefully before moving ahead.

  * Is the problem being addressed significant enough to warrant a change to Swift?

Yes. The current mechanism feels out of place in Swift.

  * Does this proposal fit well with the feel and direction of Swift?

Very much so.

  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Most languages I know of the allow operator overloading do so in a similar way. Some of them use an `adding` style naming convention for operator methods which I find very distasteful. I’m really happy Swift is not going to take that path.

  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I followed the initial discussion and gave the specific details of the proposal a close look.

···

More information about the Swift evolution process is available at

  https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

-Chris Lattner
Review Manager

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


(Nicola Salmoria) #4

* What is your evaluation of the proposal?

I'm generally in strong support, having long been a proponent of removing
operators from protocols (the first occurrence was in this thread:
http://article.gmane.org/gmane.comp.lang.swift.evolution/7935)

I have several comments about the details of the proposal, however.

1) At the beginning, in the "Proposed solution" section, the proposal says
"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."

Later, however, there is a "Deprecation of non-static protocol operators"
section which suggest to do exactly that, and this is reiterated in the
"Impact on existing code" section.

Since I think that the deprecation of global operator overloads is the
crucial point of the proposal, I assume that the former is an oversight.

2) The method signatures in the examples are not up to date with the current
Swift 3 syntax. For example:

protocol Equatable {
  static func ==(lhs: Self, rhs: Self) -> Bool
}

should be:

protocol Equatable {
  static func ==(_ lhs: Self, _ rhs: Self) -> Bool
}

3) As has already been noted by many others, the suggested syntax for
prefix/postfix operators is overcomplicated. The proposal is:

// These are deprecated, of course, but used here just to serve as an
// example.
static prefix func ++(_ value: inout Self) -> Self
static postfix func ++(_ value: inout Self) -> Self

We don't need that. Since the 'operators' declared inside protocols are
effectively just normal methods (apart from their names), we just need to
name the parameters accordingly:

static func ++(prefix value: inout Self) -> Self
static func ++(postfix value: inout Self) -> Self

4) I don't agree with the request to limit to static methods for the
operator implementations.
I support this for symmetrical binary operators like +, but there are other
operators like += that seem to work better with members. That is, the
proposed declaration:

static func +=(_ lhs: inout Self, _ rhs: Self)

is more similar to the global += operator definition, but is less clear than:

mutating func +=(_ rhs: Self)

this is apparent also at the call site. With the proposed syntax, one would
need to do:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
    T.+=(lhs, rhs)
}

while with a member function this would read more naturally as:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
    lhs.+=(rhs)
}

5) the proposal mentions the open question of ambiguities between the dot
syntax to access methods and operators whose name starts with a dot.
This seems to be a real issue: I don't think

return T....(minimum, maximum)

looks any good, even if the compiler was able to parse it.

However, this just means that the methods used to implement operators with
problematic names would need to use different names. Arguably, the only
cases where one would really want to use methods with operator names is for
arithmetical operators. Custom operators like ... are better expressed as
methods with more significant names.

6) It seems somewhat arbitrary to restrict method names to match an
operator, nor to put requirements on the function signature. I'd say there
are two cases, either the compiler can handle a method name that uses
special characters, or it can't. If it can't, matching an operator name
won't help. If it can, why put limits? There could be other creative uses of
such names, which we would be ruling out for no particular reason. This is
something that seems better left to the author of the protocol.

7) Automatic generation of trampoline functions is out of scope so I'm not
going to talk much about it, I only want to mention that it would make sense
to consider making such a feature as general as possible, instead of
focusing exclusively on operators.

For example, think of the common mathematical functions like sin, cos, etc.
It could make sense to give them the same treatment as operators, declaring
them as part of the FloatingPoint protocol but preserving the global
functions too.
It might even make sense to be able to create trampolines not only from
global space to a type, but also from one type to another type, or even for
all methods of a type (e.g. when boxing a value inside another type).

* Is the problem being addressed significant enough to warrant a change to

Swift?

Absolutely. The handling of operators in protocols has been one of the worst
pain points in my use of Swift.

* Does this proposal fit well with the feel and direction of Swift?

Yes; it significantly increases clarity and consistency.

* If you have used other languages or libraries with a similar feature,

how do you feel that this proposal compares to those?

I only have experience with C++ operator overloading, which is much less
advanced.

* How much effort did you put into your review? A glance, a quick reading,

or an in-depth study?

An in-depth study of the proposal, and I read all the relevant threads on
the mailing list.

···

--
Nicola


(Leonardo Pessoa) #5

Hello Swift community,

The review of "SE-0091: Improving operator requirements in protocols" begins now and runs through May 23. The proposal is available here:

        https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md

        * What is your evaluation of the proposal?

Allow for consistent code when implementing operators. I'd go a bit
further and say any operator functions should be declared like this
but this seems to be the case for another proposal.

        * Is the problem being addressed significant enough to warrant a change to Swift?

Yes.

        * Does this proposal fit well with the feel and direction of Swift?

Yes.

        * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Yes. In C# operator overloading is done exactly like this (but also
only like this).

        * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

A quick reading.


(plx) #6

Mixed; there are very good ideas here but combined in IMHO some questionably-strict ways. I understand that starting small and extending later is preferable, and I understand the Swift 3 situation.

In favor of these:
- allowing static methods to have “operator names”
- deprecating operator requirements in protocols
- providing default operator definitions as trampolines to the static methods

It’s really not clear to me if this is anything more than just:

- adding the ability to give (static/class) methods operator names
- removing the ability for protocols to declare operators
- make all operator implementations “explicit"…
- ...but providing “protocol operators” generically (e.g. via suitable “trampolines”)

…if that’s all there is to this, I’m ok with it for now, but have a suggestion:

I’d *prefer* there be an explicit annotation, e.g. to straw-man it a bit, something like one of these:

  @trampolined static func +(lhs: Self, rhs: Self) -> Self
  @trampolined(operator) static func +(lhs: Self, rhs: Self) -> Self

…which for now would just produce a compiler warning if no corresponding operator function is defined, but could someday be used to trigger trampoline synthesis.

The reason I request explicit syntax for this is b/c it seems *highly* likely that a subsequent enhancement will be to allow a similar mechanism to specify functions like `abs` and `sin` should be similarly trampolined (in their case into free functions); it’d be nice to have a common mechanism for them now, even if their implementation is to be deferred.

That said, it’d *concern me* if this proposal somehow planned to enforce that “operators” *always* call through to the operator-named function; it ought to be possible to e.g. define operators like so:

  protocol AngularCoordinateProtocol {

    associatedtype Rotation: RotationProtocol

    static func rotated(angle angle: Self, by rotation: Self.Rotation) -> Self

  }

  func +<A:AngularCoordinateProtocol>(lhs: A, rhs: A.Rotation) -> A { return A.rotated(angle: lhs, by: rhs) }
  func -<A:AngularCoordinateProtocol>(lhs: A, rhs: A.Rotation) -> A { return A.rotated(angle: lhs, by: -rhs) }

…and it seems like this proposal wouldn’t preclude that, but I’m not 100% I understand it on that point.

Also, I can’t say I’m a fan of having the prefix/postfix handled by argument labels.

How hard would it be to e.g. simply allow something like this:

  func ==<T:Equatable>(lhs: T, rhs: T) -> Bool {
    return lhs T.== rhs
  }

…instead of the `T.==(lhs,rhs)` syntax?

···

On May 17, 2016, at 10:33 PM, Chris Lattner via swift-evolution <swift-evolution@swift.org> wrote:

Hello Swift community,

The review of "SE-0091: Improving operator requirements in protocols" begins now and runs through May 23. The proposal is available here:

  https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md

Reviews are an important part of the Swift evolution process. All reviews should be sent to the swift-evolution mailing list at

  https://lists.swift.org/mailman/listinfo/swift-evolution

or, if you would like to keep your feedback private, directly to the review manager.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and contribute to the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  * What is your evaluation of the proposal?
  * Is the problem being addressed significant enough to warrant a change to Swift?
  * Does this proposal fit well with the feel and direction of Swift?
  * If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  * How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

  https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

-Chris Lattner
Review Manager

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


(Jordan Rose) #7

[Proposal: https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md]

Hi, Tony. Thanks for working on this. I have to say I’m incredibly concerned with this direction, for two main reasons. I’ll try to break them down here. (Sorry for squeaking in at the end of the review period!)

Overrides

People up until now have been fairly unhappy with how operators are statically dispatched and can’t be overridden. We went all the way towards providing == that automatically calls isEqual(_:slight_smile: on NSObject, but then you provide == for your subclass and it never gets called…no, wait, it does get called when you do a simple test, but not from the actual code that has NSObject as a static type.

This proposal stays in that space: the proposed “trampoline” operator will dispatch based on the static type of the objects, not the dynamic type. Why? Consider using == on an NSURL and an NSString, both statically typed as NSObject. Given the definition of the trampoline from the proposal

func == <T: Equatable>(lhs: T, rhs: T) -> Bool {
  return T.==(lhs, rhs)
}

T can’t possibly be anything but NSObject. (Neither NSURL nor NSString matches the types of both ‘lhs’ and ‘rhs’.) This isn’t a regression from the current model, as you say, but it does make the current model even more surprising, since normally you’d expect methods to be dynamically dispatched.

Here’s an alternate formation of the trampoline that’s a little better about this…

func == <T: Equatable>(lhs: T, rhs: T) -> Bool {
  return lhs.dynamicType.==(lhs, rhs)
}

…but I’m still not convinced. (People are especially likely to get this wrong without the trampolines being synthesized.)

One more note: at one point Joe Groff was investigating the idea that conformances wouldn’t be inherited onto subclasses, which would mean no more implicit ‘required’ initializers. Instead, the compiler would perform most operations by upcasting to the base class, and then converting to the protocol type or calling the generic function. In this world, T would always be NSObject, never a subclass, and we’d have to come up with something else. I think this model is still worth investigating and I wouldn’t want to close off our options just for the sake of “cleaning house”.

It’s possible that there’s not actually a reason to override operators in practice, which would make pretty much all of these concerns go away. (== is special; imagine we had an operation for checking equality within types and one across type hierarchies and ignore it for now.) I think it’d be worth investigating where operators are overridden today, and not just in Swift, to make sure we cover those use cases too.

(Please forgive all of the Foundation type examples that may soon be value types. They’re convenient.)

Assignment Operators

A mutating requirement and a static method with an inout parameter mean different things for a conforming class: the former can only access the class’s properties, while the latter can replace the caller’s reference as well.

class Player { … }

extension Player {
  static func roulette(_ player: inout Player) {
    if randomValue() > 0.1 {
      player.roundsSurvived += 1
    } else {
      // Replace this player…but not any other references to them!
      player = Player()
    }
  }

  /*mutating*/ func solitaire() {
    self.roundsSurvived += 1
    // Cannot replace ‘self’
    //self = Player()
  }
}

I’m not sure if one of these is obviously better than the other (more capable :left_right_arrow:︎ more footgun). I agree with Nicola's point about mutating methods looking better than static methods taking an inout parameter, but that probably shouldn’t be the ultimate deciding factor.

I know we want to improve type-checker performance, and reducing the number of overloads seems like a way to do that, but I’m not convinced it actually will in a significant way (e.g. “you can now use ten operators in a chain instead of seven” is not enough of a win). It still seems like there ought to be a lot of low-hanging fruit in that area that we could easily clear away, like “an overload containing a struct type will never match any input but that struct type”.

I personally really want to move operators into types, but I want to do it by doing member lookup on the type, and fall back to global operators only if something can’t be found there. That approach

- also has potential to improve type-checker performance
- also removes operators from the global namespace
- also removes the need for “override points” (implementation methods like NSObject.isEqual(_:slight_smile: and FloatingPoint.isLessThan(_:))

It does privilege the left-hand side of a binary operator, but I think that’s acceptable for the operators we’ve seen in practice. (Of course we would need real data to back that up.)

I think that about sums up my concerns and my interest in an alternate proposal. Again, I’m sorry for coming to this so late and for skimming the latest discussion on it; I’m sure “my” proposal has already come up, and I know it has its own flaws. I think I’m just not convinced that this is sufficiently better to be worth the churn and closing off of other potential avenues.

Best,
Jordan


(Douglas Gregor) #8

Proposal link:

  https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md

Hi all,

The review of "SE-0091: Improving operator requirements in protocols" begins now and runs through May 23. The proposal is available here:

My apologies for being very, very late with this review. As has been noted elsewhere, the core team has been rather DoS’d for the last few weeks, and even very important things are getting lost in the shuffle.

I support the goals of this proposal, but I’m strongly against the approach it takes because it is fundamentally based on forwarding functions in the global scope.

My Complaints with the Proposal
1) Adding an operator function to a type doesn’t “just work”, which is surprising. Let’s do something silly and add a ‘*’ operator to repeat strings:

extension String {
  static func *(lhs: String, rhs: Int) -> String { … }
}

If I try to use this in the obvious way

print(“hello” * 3)

I will get a compiler error. I have two paths at this point, neither of which is obvious: either I need to find (or author!) a protocol to conform to that allows my particular brand of ‘*’ to match and has a global forwarding operator, or I need to implement the operator at the global scope:

  func *(lhs: String, rhs: Int) -> String { ... }

2) Creating a new operator now requires more boilerplate:
  a) An operator definition, e.g.,

infix operator +++ { }

  b) A protocol that describes this new operator,

protocol Concatable {
  func operator+++(lhs: Self, rhs: Self) -> Self
}

  c) A forwarding operator function based on that protocol

func operator+++<T : Concatable>(lhs: T, rhs: T) -> T {
  return T.+++(lhs, rhs)
}

Yes, creating a new operator shouldn’t be the easiest thing in the world, but that is a ton of boilerplate. Moreover…

3) The protocols used to describe these operators aren’t really natural: they are bare-bones, purely-syntactic protocols that have no meaning other than to do forwarding. Just putting “+” into Arithmetic isn’t good enough: we’ll need another one for Strideable, and we’ll almost surely end up with a “HasBinaryPlus” protocol like this:

protocol HasBinaryPlus {
  func operator+(lhs: Self, rhs: Self) -> Self
}

so that other non-arithmetic types that want to introduce a binary plus with this form can opt to the protocol rather than having to write the forwarding function I complained about in (1). Moreover, Arithmetic will inherit this HasBinaryPlus. Scale that out and you have Arithmetic being composed of a pile of meaningless syntactic protocols: HasBinaryPlus, HasBinaryMinus, HasBinaryStar, HasBinarySlash, HasPrefixPlus, HasPrefixMinus. It makes Arithmetic confusing because the requirements are scattered.

It’s not even that there is just one protocol per operator, either: even just with the standard library, + will have at least two protocols associated with it: HasBinaryPlus and Strideable to cover the various cases in the standard library. It’s probably not enough, and there will surely be more protocols created for binary + simply to provide the forwarding functions.

4) The rule prohibiting operator functions defined in a type that don’t conform to a protocol limits retroactive modeling. If you don’t have a protocol in hand, you have to use a global operator.

5) Forwarding functions aren’t good for tools. Under this proposal, if I write “1 + 2” and use a tool to look at which “+” it resolved to, what will we see? The generic forwarding operator. Even though I could look in the source and see this:

extension Int {
  static func +(lhs: Int, rhs: Int) -> Int { … }
}

and even those that’s what will get called, my tools aren’t going to interpret the body of the global forwarding function for + to resolve it in the obvious way.

The Good Parts

With all that negative, one might get the impression that I don’t like operators in types. I think there are improvements here:

I) Writing an operator function in a type/extension of a type is far more natural that writing one at global scope for the common case. Even if you’re not planning on conforming to a protocol, it just feels right that (say) String + String should be defined in an extension of String. It’s better for tooling (which can more easily associate the operator + with the String type), code organization, works with the new meaning of the “private” access modifier, and simply feels like Swift.

II) The requirement to use “static” on the operator function requirement in the protocol makes perfect sense to me. It’s a lot clearer, and communicates the semantics better. I can’t recall why we didn’t do this in the first place.

III) The goal to reduce the total number of overloads is laudable. It can help type checker performance (fewer overloads == less exponential behavior) and improve diagnostics (fewer candidates to display on error). The key insight here is that we don’t want to consider both a generic operator based on some protocol (e.g., + for Arithmetic types) and the operator functions that are used to satisfy the corresponding requirement.

An Alternative Approach

Let’s accept (I) and (II). But, let’s make operator lookup always be global, so that it sees all operators defined at either module scope or within a type/extension of a type. This gives us the syntactic improvements of the SE-0091 “immediately”, and eliminates all five of my complaints above: the natural Swift thing of defining your functionality within the type or an extension thereof “just works”. It’s weird in the sense that operators will be the only place where we do such global lookup—finding entries at both global and type scope. However, SE-0091 is introducing a different weird name lookup rule, and it feels like there’s really no way to avoid it: we simply don’t want normal lexical name lookup for operators when they can be defined in types.

This approach does not (directly) give any of the type checker performance/QoI improvements of (III). However, we can achieve that by making the key insight of (III) part of the semantic model: when we find all operators, we also find the operators in the protocols themselves. The operators in the protocols are naturally generic, e.g., the Arithmetic + effectively has a generic function type like this:

  <Self: Arithmetic> (Self, Self) -> Self

which is basically what the forwarding functions look like in SE-0091 at a type level. Then, we say that we do not consider an operator function if it implements a protocol requirement, because the requirement is a generalization of all of the operator functions that satisfy that requirement. With this rule, we’re effectively getting the same effects of SE-0091’s approach to (III)—but it’s more automatic.

Note that this approach could change semantics. When you type-check a generic function, you’re inferring the generic type arguments. That could end up type checking differently than considering a more specific function. For example, let’s say we were allowed to fulfill an operator function requirement with an operator function that was a subtype of the requirement (a commonly-requested feature), e.g.,

class Super { }

class Sub : Super, Equatable {
  static func ==(lhs: Super, rhs: Super) -> Bool { … } // note: currently ill-formed, but requested often
}

func testMe(sup: Super) -> Bool {
  return sup == sup // error: Equatable.== fails because “Super” is not equatable,
                     // and Sub.== isn’t considered because it satisfies the Equatable.== requirement
}

I suspect this is acceptable. If we ever did start to allow one to satisfy a requirement with something that is a subtype, perhaps we just wouldn’t extend that rule to operator function requirements. Note that it’s possible that you can trigger this in the current type system as well—I haven’t tried.

One could experiment with this solution just with the standard library: take away all of the concrete +’s and map them to “Arithmetic.add” or “Strideable.add” to get down to the minimal set, and then put the forwarding functions in to see how well the type checker copes with it (e.g., performance, diagnostics, what unexpected breakage do we see). There’s a way to push the experiment further—by teaching the type checker to do this pruning rule, which doesn’t actually depend on introducing the ability to define an operator function within a type—but of course that requires more implementation effort.

  - Doug

···

On May 17, 2016, at 8:33 PM, Chris Lattner <clattner@apple.com> wrote:


(Kevin Lundberg) #9

I was just about to respond to this proposal with the same feedback.
This code is purely mechanical and could be written wrong by a protocol
author.

Additionally, I am generally +1 for the same reasons as Brent, but I
have another caveat as well:

Defining prefix and postfix functions looks like this in the proposal:

  static prefix func ++(value: inout Self) -> Self
  static postfix func ++(value: inout Self) -> Self

yet the proposal suggests calling them this way from these boilerplate
methods:

prefix func ++ <T: SomeProtocol>(value: inout T) -> T {
  return T.++(prefix: &value)
  }
postfix func ++ <T: SomeProtocol>(value: inout T) -> T {
    return T.++(postfix: &value)
  }

Having this mismatch between argument labels at the declaration and call
site feels strange to me, and it is inconsistent with how function
argument labels work everywhere else in the language. What if the prefix
and postfix keywords also required the functions to be disambiguated
somehow in a way that the current method naming scheme allows?

  static prefix func ++(prefix value: inout Self) -> Self
  static postfix func ++(postfix value: inout Self) -> Self

This will give a clear signal for callers to know how they should call
the function by looking at the declaration, instead of relying on a
weird edge case for these kind of operator functions. `prefix` and
`postfix` don't necessarily need to be the actual names of the arguments
that are forced by the compiler, but they should be required to be
different in the same way that you can't re-declare the same identical
function signature any other time (aside from current global
prefix/postfix functions of course).

- Kevin

···

On 5/18/2016 12:03 AM, Brent Royal-Gordon via swift-evolution wrote:

I'm in favor, with one small concern:

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)
  }

This trampoline operator, and all of the others in the proposal, appears to be 100% pure boilerplate. Could Swift generate them for us?

(Specifically, I'm suggesting that if protocol P defines a `static func $!(args) -> ret`, Swift should automatically generate a global `func #! <T: P> (args) -> ret`, substituting `T` for any `Self`s among the parameters or return values.)


(Tony Allevato) #10

I'm in favor, with one small concern:

> 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)
> }

This trampoline operator, and all of the others in the proposal, appears
to be 100% pure boilerplate. Could Swift generate them for us?

(Specifically, I'm suggesting that if protocol P defines a `static func

$!(args) -> ret`, Swift should automatically generate a global `func #! <T:
> (args) -> ret`, substituting `T` for any `Self`s among the parameters or
return values.)

That was actually part of the original write-up :slight_smile: You can see the removed
content from the diff here:
https://github.com/apple/swift-evolution/pull/283/commits/841514fbd7d11ce1ab6dad8ee3c6473d6c550789

The core team felt that auto-generating the trampolines was too ambitious
for the Swift 3 timeline but that the rest of the improvements should not
be held up by it. I definitely intend to propose auto-trampolines as a
natural follow-up proposal to this (assuming it's accepted) for Swift 3.x/4.

···

On Tue, May 17, 2016 at 9:03 PM Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:


(Tony Allevato) #11

> * What is your evaluation of the proposal?

I'm generally in strong support, having long been a proponent of removing
operators from protocols (the first occurrence was in this thread:
http://article.gmane.org/gmane.comp.lang.swift.evolution/7935)

I have several comments about the details of the proposal, however.

1) At the beginning, in the "Proposed solution" section, the proposal says
"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."

Later, however, there is a "Deprecation of non-static protocol operators"
section which suggest to do exactly that, and this is reiterated in the
"Impact on existing code" section.

Since I think that the deprecation of global operator overloads is the
crucial point of the proposal, I assume that the former is an oversight.

I could probably do a better job of clarifying the wording here. The
proposal does *not* deprecate *all* global operator overloads. Global
operators can still be implemented as they have been in Swift. So if you
have a concrete type like `struct Matrix`, you can still define at the
global level `func +(lhs: Matrix, rhs: Matrix) -> Matrix`.

What's being deprecated is the current syntax used to define operator
requirements inside protocols (by making the functions static) and the
manner by which subtypes conform (ditto, through static methods instead of
global functions).

2) The method signatures in the examples are not up to date with the
current
Swift 3 syntax. For example:

protocol Equatable {
  static func ==(lhs: Self, rhs: Self) -> Bool
}

should be:

protocol Equatable {
  static func ==(_ lhs: Self, _ rhs: Self) -> Bool
}

Unless I'm mistaken, from looking at the Swift 3 branch of stdlib, the
syntax changes don't appear to apply to operator functions. Since they are
a special case that don't have argument labels, it wouldn't make sense to
require them (or rather, the placeholders) here.

3) As has already been noted by many others, the suggested syntax for
prefix/postfix operators is overcomplicated. The proposal is:

// These are deprecated, of course, but used here just to serve as an
// example.
static prefix func ++(_ value: inout Self) -> Self
static postfix func ++(_ value: inout Self) -> Self

We don't need that. Since the 'operators' declared inside protocols are
effectively just normal methods (apart from their names), we just need to
name the parameters accordingly:

static func ++(prefix value: inout Self) -> Self
static func ++(postfix value: inout Self) -> Self

4) I don't agree with the request to limit to static methods for the
operator implementations.
I support this for symmetrical binary operators like +, but there are other
operators like += that seem to work better with members. That is, the
proposed declaration:

static func +=(_ lhs: inout Self, _ rhs: Self)

is more similar to the global += operator definition, but is less clear
than:

mutating func +=(_ rhs: Self)

this is apparent also at the call site. With the proposed syntax, one would
need to do:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
    T.+=(lhs, rhs)
}

while with a member function this would read more naturally as:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
    lhs.+=(rhs)
}

I considered this, but eventually settled on "everything is static" for
consistency. As you mention, there's a stronger argument to be made for
assignment operators to have "left hand side is the receiver" semantics
than there are for standard infix operators, but from a consistency point
of view (and ease of learning), I think having everything static and the
signatures of the static operators matching those of the global operators
is preferable. (Which is also why, as I mentioned in a previous reply, I
would be open to dropping the prefix/postfix keyword and making it an
argument label instead, in both contexts.)

5) the proposal mentions the open question of ambiguities between the dot
syntax to access methods and operators whose name starts with a dot.
This seems to be a real issue: I don't think

return T....(minimum, maximum)

looks any good, even if the compiler was able to parse it.

However, this just means that the methods used to implement operators with
problematic names would need to use different names. Arguably, the only
cases where one would really want to use methods with operator names is for
arithmetical operators. Custom operators like ... are better expressed as
methods with more significant names.

If there is a strong case where an operator is better implemented as a
global operator and a named method, this proposal still allows that, since
it's not deprecating all global operator definitions. A protocol could
certainly have a requirement that is a named method, and provide a global
generic operator that calls it.

6) It seems somewhat arbitrary to restrict method names to match an
operator, nor to put requirements on the function signature. I'd say there
are two cases, either the compiler can handle a method name that uses
special characters, or it can't. If it can't, matching an operator name
won't help. If it can, why put limits? There could be other creative uses
of
such names, which we would be ruling out for no particular reason. This is
something that seems better left to the author of the protocol.

IMO, to reduce potential confusion, I would argue that a function whose
name is the same as a defined operator should conform to the requirements
(such as argument count) of that operator. It's certainly worth discussion,
though! With that being said, it may be easier on users to "rule something
out" now and open it up later if need be, rather than to leave it open for
people to use and decide it needs to be closed later.

···

On Wed, May 18, 2016 at 10:02 AM Nicola Salmoria via swift-evolution < swift-evolution@swift.org> wrote:

7) Automatic generation of trampoline functions is out of scope so I'm not
going to talk much about it, I only want to mention that it would make
sense
to consider making such a feature as general as possible, instead of
focusing exclusively on operators.

For example, think of the common mathematical functions like sin, cos, etc.
It could make sense to give them the same treatment as operators, declaring
them as part of the FloatingPoint protocol but preserving the global
functions too.
It might even make sense to be able to create trampolines not only from
global space to a type, but also from one type to another type, or even for
all methods of a type (e.g. when boxing a value inside another type).

> * Is the problem being addressed significant enough to warrant a change
to
Swift?

Absolutely. The handling of operators in protocols has been one of the
worst
pain points in my use of Swift.

> * Does this proposal fit well with the feel and direction of Swift?

Yes; it significantly increases clarity and consistency.

> * If you have used other languages or libraries with a similar feature,
how do you feel that this proposal compares to those?

I only have experience with C++ operator overloading, which is much less
advanced.

> * How much effort did you put into your review? A glance, a quick
reading,
or an in-depth study?

An in-depth study of the proposal, and I read all the relevant threads on
the mailing list.

--
Nicola

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


(Jordan Rose) #12

[Proposal: https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md]

Some comments on Nicola’s points (my own comments to come separately):

2) The method signatures in the examples are not up to date with the current
Swift 3 syntax. For example:

protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}

should be:

protocol Equatable {
static func ==(_ lhs: Self, _ rhs: Self) -> Bool
}

Operator functions (and subscript indexes) implicitly have no argument labels today, so this isn’t technically wrong. You can see that there are no labels when you use the functions like, well, functions:

func test(input: Int) { print(input) }
(test)(2) // error: missing argument label 'input:' in call
(+)(2, 3) // okay

It may be worth changing, though.

4) I don't agree with the request to limit to static methods for the
operator implementations.
I support this for symmetrical binary operators like +, but there are other
operators like += that seem to work better with members. That is, the
proposed declaration:

static func +=(_ lhs: inout Self, _ rhs: Self)

is more similar to the global += operator definition, but is less clear than:

mutating func +=(_ rhs: Self)

this is apparent also at the call site. With the proposed syntax, one would
need to do:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
   T.+=(lhs, rhs)
}

while with a member function this would read more naturally as:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
   lhs.+=(rhs)
}

I strongly agree with this for assignment operators, and I also think it’s important for things that make sense to override in a subclass. However, it then puts prefix and postfix operators right back in the space of needing a keyword instead of using argument label.

5) the proposal mentions the open question of ambiguities between the dot
syntax to access methods and operators whose name starts with a dot.
This seems to be a real issue: I don't think

return T....(minimum, maximum)

looks any good, even if the compiler was able to parse it.

However, this just means that the methods used to implement operators with
problematic names would need to use different names. Arguably, the only
cases where one would really want to use methods with operator names is for
arithmetical operators. Custom operators like ... are better expressed as
methods with more significant names.

Backtick escaping could help with this as well:

return T.`…`(minimum, maximum)

It’s still not great but it’s at least less of a soup.

Jordan


(David Sweeris) #13

Yeah, I've been meaning to suggest that. And 'T.++(x)' and '(x)T.++' for prefix and postfix, respectfully.

- Dave Sweeris

···

On May 18, 2016, at 18:07, plx via swift-evolution <swift-evolution@swift.org> wrote:

How hard would it be to e.g. simply allow something like this:

func ==<T:Equatable>(lhs: T, rhs: T) -> Bool {
   return lhs T.== rhs
}

…instead of the `T.==(lhs,rhs)` syntax?


(Brent Royal-Gordon) #14

It does privilege the left-hand side of a binary operator, but I think that’s acceptable for the operators we’ve seen in practice. (Of course we would need real data to back that up.)

I'm not quite so convinced that privileging the left side isn't a big deal—think of all the mandatory type checks and other chicanery in an `-isEqual:` implementation—but I wonder if we can avoid privileging the left-hand side at all. For instance, suppose use of an infix operator "looked" like this:

  (lhs, rhs).==()

And method dispatch on a tuple was defined to use the dynamic types of all members of the tuple, with some kind of multimethod dispatch system. Then both operands participate equally in the dispatch, and a case with disparate subclasses will fall back to a superclass.

  // Note: This is, admittedly, an incredibly funky syntax.
  protocol Equatable {
    extension <T: Equatable>(T, T) {
      func ==() -> Bool
    }
  }

  class Car: Equatable {
    let vin: String
  }
  extension (Car, Car) {
    func ==() -> Bool {
      return self.0.vin == self.1.vin
    }
  }
  
  class Taxi: Car {
    var cabNumber: String
  }
  extension (Taxi, Taxi) {
    func ==() -> Bool {
      return self.0.cabNumber == self.1.cabNumber
    }
  }
  
  class Uber: Car {
    var driverID: Int64
  }
  extension (Uber, Uber) {
    func ==() -> Bool {
      return self.0.driverID == self.1.driverID
    }
  }

  // Note that I'm erasing the subclasses here.
  let taxi: Automobile = Taxi()
  let uber: Automobile = Uber()

  taxi == taxi // Compares by cabNumber
  uber == uber // Compares by driverID
  taxi == uber // Compares by vin

Multimethod dispatch at runtime is relatively slow, but it *would* give us the exact behavior we want.

···

--
Brent Royal-Gordon
Architechies


(Tony Allevato) #15

[Proposal:
https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md
]

Hi, Tony. Thanks for working on this. I have to say I’m incredibly
concerned with this direction, for two main reasons. I’ll try to break them
down here. (Sorry for squeaking in at the end of the review period!)

No worries, thanks for the detailed feedback! I've tried to address your
concerns inline.

*Overrides*

People up until now have been fairly unhappy with how operators are
statically dispatched and can’t be overridden. We went all the way towards
providing == that automatically calls isEqual(_:slight_smile: on NSObject, but then you
provide == for your subclass and it never gets called…no, wait, it *does* get
called when you do a simple test, but not from the actual code that has
NSObject as a static type.

This proposal stays in that space: the proposed “trampoline” operator will
dispatch based on the *static* type of the objects, not the dynamic type.
Why? Consider using == on an NSURL and an NSString, both statically typed
as NSObject. Given the definition of the trampoline from the proposal

func == <T: Equatable>(lhs: T, rhs: T) -> Bool {

  return T.==(lhs, rhs)
}

T can’t possibly be anything but NSObject. (Neither NSURL nor NSString
matches the types of both ‘lhs’ and ‘rhs’.) This isn’t a regression from
the current model, as you say, but it does make the current model *even
more surprising,* since normally you’d expect methods to be dynamically
dispatched.

Setting Objective-C aside for a moment, is this example consistent with
Swift's design philosophies about type safety though? Swift doesn't even
let you compare types that seem reasonably compatible, like Float and
Double. Should we expect users to be able to compare completely distinct
types like NSURL and NSString without a little extra work?

If having `==` map to `NSObject.isEqual` is important for Objective-C
compatibility, then there's no reason the runtime can't provide the
following operator:

func ==(lhs: NSObject, rhs: NSObject) -> Bool {
  return lhs.isEqual(rhs)
}

which should be more specific than the generic one and always get chosen in
the context you desire.

The situation is a little better when one of the types being compared has
the other type as a base, because then implementing the operators as class
methods instead of static methods does the right thing (with the only
caveat being you have to cast the arguments down to the correct type).

Here’s an alternate formation of the trampoline that’s a little better
about this…

func == <T: Equatable>(lhs: T, rhs: T) -> Bool {

  return lhs.dynamicType.==(lhs, rhs)
}

…but I’m still not convinced. (People are especially likely to get this
wrong without the trampolines being synthesized.)

One more note: at one point Joe Groff was investigating the idea that
conformances wouldn’t be inherited onto subclasses, which would mean no
more implicit ‘required’ initializers. Instead, the compiler would perform
most operations by upcasting to the base class, and then converting to the
protocol type or calling the generic function. In this world, T would
*always* be NSObject, never a subclass, and we’d have to come up with
something else. I think this model is still worth investigating and I
wouldn’t want to close off our options just for the sake of “cleaning
house”.

It’s possible that there’s not actually a reason to override operators in
practice, which would make pretty much all of these concerns go away. (==
is special; imagine we had an operation for checking equality within types
and one across type hierarchies and ignore it for now.) I think it’d be
worth investigating where operators are overridden today, and not just in
Swift, to make sure we cover those use cases too.

(Please forgive all of the Foundation type examples that may soon be value
types. They’re convenient.)

*Assignment Operators*

A mutating requirement and a static method with an inout parameter mean
different things for a conforming class: the former can only access the
class’s properties, while the latter can replace the caller’s *reference* as
well.

class Player { … }

extension Player {
  static func roulette(_ player: inout Player) {
    if randomValue() > 0.1 {
      player.roundsSurvived += 1
    } else {
      // Replace this player…but not any other references to them!
      player = Player()
    }
  }

  /*mutating*/ func solitaire() {
    self.roundsSurvived += 1
    // Cannot replace ‘self’
    //self = Player()
  }
}

I’m not sure if one of these is obviously better than the other (more
capable :left_right_arrow:︎ more footgun). I agree with Nicola's point about mutating
methods *looking* better than static methods taking an inout parameter,
but that probably shouldn’t be the ultimate deciding factor.

You make a good point here. This is hitting on one of the fundamental
problems I've had with mutating requirements in protocols in some of my own
projects, which is that even when a class type conforms to such a protocol
(or worse, even if the protocol itself is restricted to `class` types
only), if I want to use it in a generic context with that protocol as a
constraint, I still have to pass it unnecessarily as an inout argument.

On the one hand, fixing that underlying problem might help us here as well.
An obvious alternative would be to disallow `inout` on class conformances
to assignment operators, but that introduces inconsistency that I'm trying
to avoid. I'm not sure if it's any better, either.

I know we want to improve type-checker performance, and reducing the
number of overloads *seems* like a way to do that, but I’m not convinced
it actually will in a significant way (e.g. “you can now use ten operators
in a chain instead of seven” is not enough of a win). It still seems like
there ought to be a lot of low-hanging fruit in that area that we could
easily clear away, like “an overload containing a struct type will never
match any input but that struct type”.

I personally really want to move operators into types, but I want to do it
by doing member lookup on the type, and fall back to global operators only
if something can’t be found there. That approach

- also has potential to improve type-checker performance
- also removes operators from the global namespace
- also removes the need for “override points” (implementation methods like
NSObject.isEqual(_:slight_smile: and FloatingPoint.isLessThan(_:))

It does privilege the left-hand side of a binary operator, but I think
that’s acceptable for the operators we’ve seen in practice. (Of course we
would need real data to back that up.)

As Brent pointed out in his reply, without multiple dispatch, you don't
really benefit from privileging the lhs argument, and in fact you can end
up in situations where the behavior is surprising if you don't implement
both orders. For example, in your (NSString, NSURL) example, each class
would have to be extended to explicitly support comparison with the other
in order to support commutativity if equality was an instance method. I'm
not sure that's any better for those particular types than just having the
operators global in the first place.

My argument would be that there are still significant enough benefits to
move forward:

- for value types and for binary operators that have the same typed
arguments, static methods provide a clear benefit w.r.t. consistency (the
declaration of the global operator and the static operator look the same)
and obvious semantics
- for binary operators with differently typed arguments, static operators
support commutativity much better than instance methods because both
implementations live in the most natural type for that particular case (for
example, String + Character and Character + String)
- if later on we can auto-generate trampolines, the choice of instance vs.
static methods for value types becomes a wash because the compiler is
effectively doing the same dispatch under the hood anyway
- for class types, regardless of whether one is a base of the other or both
share a common third base type, neither static nor instance methods
completely solve the problem and won't until/unless Swift supports multiple
dispatch, and the proposed behavior is not a regression in those cases

···

On Mon, May 23, 2016 at 9:58 PM Jordan Rose <jordan_rose@apple.com> wrote:

I think that about sums up my concerns and my interest in an alternate
proposal. Again, I’m sorry for coming to this so late and for skimming the
latest discussion on it; I’m sure “my” proposal has already come up, and I
know it has its own flaws. I think I’m just not convinced that this is
sufficiently *better* to be worth the churn and closing off of other
potential avenues.

Best,
Jordan


(Tony Allevato) #16

Thanks for your feedback, Doug! I've addressed some of your concerns inline.

Proposal link:

https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md

Hi all,

The review of "SE-0091: Improving operator requirements in protocols"
begins now and runs through May 23. The proposal is available here:

My apologies for being very, very late with this review. As has been noted
elsewhere, the core team has been rather DoS’d for the last few weeks, and
even very important things are getting lost in the shuffle.

I support the goals of this proposal, but I’m strongly against the
approach it takes because it is fundamentally based on forwarding functions
in the global scope.

*My Complaints with the Proposal*
1) Adding an operator function to a type doesn’t “just work”, which is
surprising. Let’s do something silly and add a ‘*’ operator to repeat
strings:

extension String {
  static func *(lhs: String, rhs: Int) -> String { … }
}

If I try to use this in the obvious way

print(“hello” * 3)

I will get a compiler error. I have two paths at this point, neither of
which is obvious: either I need to find (or author!) a protocol to conform
to that allows my particular brand of ‘*’ to match and has a global
forwarding operator, or I need to implement the operator at the global
scope:

  func *(lhs: String, rhs: Int) -> String { ... }

The unambiguous answer here is that you need to define the answer at the
global scope; no protocols come into play here. This proposal only
discusses what happens *if* a protocol defines an operator as a
*requirement*; that does not apply to the case of extending an existing
type with a new operator, so it's unaffected by these proposed changes.

I'll concede that it has the possibility to introduce confusion as there
are now two places where operators can be defined, with different meanings.

2) Creating a new operator now requires more boilerplate:
a) An operator definition, e.g.,

infix operator +++ { }

b) A protocol that describes this new operator,

protocol Concatable {
  func operator+++(lhs: Self, rhs: Self) -> Self
}

c) A forwarding operator function based on that protocol

func operator+++<T : Concatable>(lhs: T, rhs: T) -> T {
  return T.+++(lhs, rhs)
}

Yes, creating a new operator shouldn’t be the easiest thing in the world,
but that is a ton of boilerplate. Moreover…

This isn't the case—creating a new operator does not require defining a
protocol. Defining a new infix operator like `+++` would work just as it
did before; this proposal does not change that. I'm *not* proposing that
all operators *must* be implemented through protocol conformance; I'm
merely proposing changes to the way that they are implemented for
conformances. If a user defines `+++`, they can implement it with a global
function, without any protocol or trampoline introduced.

The additional burden is only on protocol authors (not authors of operators
in general) to provide the trampoline method. My initial way of addressing
that was to auto-generate the trampolines, eliminating the extra
boilerplate, but that was unfortunately deemed too ambitious for Swift 3.
However, I feel this still a step in the right direction and those issues
can be resolved later without breaking anything proposed here, while still
providing the other benefits described in the proposal.

3) The protocols used to describe these operators aren’t really natural:
they are bare-bones, purely-syntactic protocols that have no meaning other
than to do forwarding. Just putting “+” into Arithmetic isn’t good enough:
we’ll need another one for Strideable, and we’ll almost surely end up with
a “HasBinaryPlus” protocol like this:

protocol HasBinaryPlus {
  func operator+(lhs: Self, rhs: Self) -> Self
}

so that other non-arithmetic types that want to introduce a binary plus
with this form can opt to the protocol rather than having to write the
forwarding function I complained about in (1). Moreover, Arithmetic will
inherit this HasBinaryPlus. Scale that out and you have Arithmetic being
composed of a pile of meaningless syntactic protocols: HasBinaryPlus,
HasBinaryMinus, HasBinaryStar, HasBinarySlash, HasPrefixPlus,
HasPrefixMinus. It makes Arithmetic confusing because the requirements are
scattered.

It’s not even that there is just one protocol per operator, either: even
just with the standard library, + will have at least two protocols
associated with it: HasBinaryPlus and Strideable to cover the various cases
in the standard library. It’s probably not enough, and there will surely be
more protocols created for binary + simply to provide the forwarding
functions.

Again, this isn't the case. Operators are not required to be implemented
through protocols, but any protocol that requires an operator can include
it and there is no reason that they would have to be restricted to one
operator per protocol. Maybe this wasn't clear since I focused a lot on
`Equatable` in my proposal for simplicity, but the motivating example was
`FloatingPoint`, which would implement several:

    protocol FloatingPoint {
      static func +(lhs: Self, rhs: Self) -> Self
      static func -(lhs: Self, rhs: Self) -> Self
      static func *(lhs: Self, rhs: Self) -> Self
      static func /(lhs: Self, rhs: Self) -> Self
      // others...
}

As you can see, the protocol still has *semantic* meaning and is not just a
bag of syntax.

4) The rule prohibiting operator functions defined in a type that don’t
conform to a protocol limits retroactive modeling. If you don’t have a
protocol in hand, you have to use a global operator.

5) Forwarding functions aren’t good for tools. Under this proposal, if I
write “1 + 2” and use a tool to look at which “+” it resolved to, what will
we see? The generic forwarding operator. Even though I could look in the
source and see this:

extension Int {
  static func +(lhs: Int, rhs: Int) -> Int { … }
}

and even those that’s what will get called, my tools aren’t going to
interpret the body of the global forwarding function for + to resolve it in
the obvious way.

*The Good Parts*

With all that negative, one might get the impression that I don’t like
operators in types. I think there are improvements here:

I) Writing an operator function in a type/extension of a type is far more
natural that writing one at global scope for the common case. Even if
you’re not planning on conforming to a protocol, it just feels right that
(say) String + String should be defined in an extension of String. It’s
better for tooling (which can more easily associate the operator + with the
String type), code organization, works with the new meaning of the
“private” access modifier, and simply feels like Swift.

II) The requirement to use “static” on the operator function requirement
in the protocol makes perfect sense to me. It’s a lot clearer, and
communicates the semantics better. I can’t recall why we didn’t do this in
the first place.

III) The goal to reduce the total number of overloads is laudable. It can
help type checker performance (fewer overloads == less exponential
behavior) and improve diagnostics (fewer candidates to display on error).
The key insight here is that we don’t want to consider both a generic
operator based on some protocol (e.g., + for Arithmetic types) *and* the
operator functions that are used to satisfy the corresponding requirement.

*An Alternative Approach*

Let’s accept (I) and (II). But, let’s make operator lookup always be
global, so that it sees all operators defined at either module scope or
within a type/extension of a type. This gives us the syntactic improvements
of the SE-0091 “immediately”, and eliminates all five of my complaints
above: the natural Swift thing of defining your functionality within the
type or an extension thereof “just works”. It’s weird in the sense that
operators will be the only place where we do such global lookup—finding
entries at both global and type scope. However, SE-0091 is introducing a
different weird name lookup rule, and it feels like there’s really no way
to avoid it: we simply don’t want normal lexical name lookup for operators
when they can be defined in types.

This approach does not (directly) give any of the type checker
performance/QoI improvements of (III). However, we can achieve that by
making the key insight of (III) part of the semantic model: when we find
all operators, we also find the operators in the protocols themselves. The
operators in the protocols are naturally generic, e.g., the Arithmetic +
effectively has a generic function type like this:

<Self: Arithmetic> (Self, Self) -> Self

which is basically what the forwarding functions look like in SE-0091 at a
type level. Then, we say that we do not consider an operator function if it
implements a protocol requirement, because the requirement is a
generalization of all of the operator functions that satisfy that
requirement. With this rule, we’re effectively getting the same effects of
SE-0091’s approach to (III)—but it’s more automatic.

I like this suggestion very much, and I would support it—especially if it's
easier to implement than the trampoline generation that I proposed for the
same effect.

Thinking about it further, type checking that should be fairly
straightforward, right? If we ignore classes/subclassing for the time
being, an operator function can only have one or two arguments. Let's say
we have the following:

    let t1: T
    let t2: T
    let t = t1 + t2

If we eliminate global lookup for operators, this means that the + operator
*must* be implemented on T; so rather than searching the entire global
namespace for +(lhs: T, rhs: T), it just has to look in T for a matching +.

Likewise, heterogeneous argument lists can still be supported:

    let t: T
    let u: U
    let v = t + u

The operator must live as a static function in T or U, or it doesn't exist.
So lookup should again be fast. By looking up the operator in both T and U,
as opposed to just one or the other, this supports users being able to
define operators where they most logically make sense; for example,
hypothetically:

    protocol CustomStringProtcol {
        static func +(lhs: Self, rhs: Character) -> Self
        static func +(lhs: Character, rhs: Self) -> Self
    }
    struct CustomString: CustomStringProtocol { ... }

    let t: Character
    let u: String
    let v = t + u // looks up + with compatible arguments in String and
Character, finds it in String

There's the potential for ambiguity if both types implement operators that
match, but that may not be cause for concern.

···

On Thu, Jun 9, 2016 at 10:16 PM Douglas Gregor via swift-evolution < swift-evolution@swift.org> wrote:

On May 17, 2016, at 8:33 PM, Chris Lattner <clattner@apple.com> wrote:

Note that this approach could change semantics. When you type-check a
generic function, you’re inferring the generic type arguments. That could
end up type checking differently than considering a more specific function.
For example, let’s say we were allowed to fulfill an operator function
requirement with an operator function that was a subtype of the requirement
(a commonly-requested feature), e.g.,

class Super { }

class Sub : Super, Equatable {
  static func ==(lhs: Super, rhs: Super) -> Bool { … } // note: currently
ill-formed, but requested often
}

func testMe(sup: Super) -> Bool {
  return sup == sup // error: Equatable.== fails because “Super” is not
equatable,
                     // and Sub.== isn’t considered because it satisfies
the Equatable.== requirement
}

I suspect this is acceptable. If we ever did start to allow one to satisfy
a requirement with something that is a subtype, perhaps we just wouldn’t
extend that rule to operator function requirements. Note that it’s possible
that you can trigger this in the current type system as well—I haven’t
tried.

One could experiment with this solution just with the standard library:
take away all of the concrete +’s and map them to “Arithmetic.add” or
“Strideable.add” to get down to the minimal set, and then put the
forwarding functions in to see how well the type checker copes with it
(e.g., performance, diagnostics, what unexpected breakage do we see).
There’s a way to push the experiment further—by teaching the type checker
to do this pruning rule, which doesn’t actually depend on introducing the
ability to define an operator function within a type—but of course that
requires more implementation effort.

- Doug

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


(Brent Royal-Gordon) #17

Additionally, I am generally +1 for the same reasons as Brent, but I
have another caveat as well:

Defining prefix and postfix functions looks like this in the proposal:

static prefix func ++(value: inout Self) -> Self
static postfix func ++(value: inout Self) -> Self

yet the proposal suggests calling them this way from these boilerplate
methods:

prefix func ++ <T: SomeProtocol>(value: inout T) -> T {
return T.++(prefix: &value)
}
postfix func ++ <T: SomeProtocol>(value: inout T) -> T {
   return T.++(postfix: &value)
}

I actually found this bizarre too, but forgot to mention it. My suggested solution runs in the other direction: We should require that *all* unary operator declarations and references use `prefix` or `postfix` as a parameter label. Thus, the trampoline operators themselves would be written as:

  func ++ <T: SomeProtocol>(prefix value: inout T) -> T {
      return T.++(prefix: &value)
  }
  
  func ++ <T: SomeProtocol>(postfix value: inout T) -> T {
      return T.++(postfix: &value)
  }

Not would be written as:

  func ! <B: BooleanType>(prefix value: B) -> Bool

While force-unwrap (if we had inout return values) would be written:

  func ! <T>(postfix value: inout Optional<T>) -> inout T

`prefix` and `postfix` would be eliminated from the language as declaration modifiers, except when declaring custom operators (which is already the Land Of Ad-Hoc Syntax).

···

--
Brent Royal-Gordon
Architechies


(Xiaodi Wu) #18

I too have a little trouble with the suggested trampoline syntax:

static prefix func ++(value: inout Self) -> Self
static postfix func ++(value: inout Self) -> Self

I agree with Brent that the most appropriate way to write the trampoline
itself would be something more like:

static func ++ (prefix value: inout Self) -> Self

No particular need to eliminate declaration modifiers altogether quite yet
for the non-trampoline operators. The reasoning is this: a "prefix func" or
a "postfix func" would be used at the call site by actually prefixing or
postfixing the value. By contrast, the trampoline functions are called like
any other static member -- i.e. `T.++(prefix: foo)` -- and thus the
function itself is not really a "prefix func" or a "postfix func", just a
func with a parameter label.

···

On Tue, May 17, 2016 at 9:57 PM, Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:

> Additionally, I am generally +1 for the same reasons as Brent, but I
> have another caveat as well:
>
> Defining prefix and postfix functions looks like this in the proposal:
>
> static prefix func ++(value: inout Self) -> Self
> static postfix func ++(value: inout Self) -> Self
>
> yet the proposal suggests calling them this way from these boilerplate
> methods:
>
> prefix func ++ <T: SomeProtocol>(value: inout T) -> T {
> return T.++(prefix: &value)
> }
> postfix func ++ <T: SomeProtocol>(value: inout T) -> T {
> return T.++(postfix: &value)
> }

I actually found this bizarre too, but forgot to mention it. My suggested
solution runs in the other direction: We should require that *all* unary
operator declarations and references use `prefix` or `postfix` as a
parameter label. Thus, the trampoline operators themselves would be written
as:

        func ++ <T: SomeProtocol>(prefix value: inout T) -> T {
            return T.++(prefix: &value)
        }

        func ++ <T: SomeProtocol>(postfix value: inout T) -> T {
            return T.++(postfix: &value)
        }

Not would be written as:

        func ! <B: BooleanType>(prefix value: B) -> Bool

While force-unwrap (if we had inout return values) would be written:

        func ! <T>(postfix value: inout Optional<T>) -> inout T

`prefix` and `postfix` would be eliminated from the language as
declaration modifiers, except when declaring custom operators (which is
already the Land Of Ad-Hoc Syntax).

--
Brent Royal-Gordon
Architechies

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


(Kevin Lundberg) #19

I do like this to a point, but it ascribes special compiler behavior to specific argument labels which feels odd. If someone defined another operator function with a completely different argument label, would it just be ignored? If a developer makes a typo, that could be a subtle bug that may be confusing. Or should the compiler only allow those two argument labels for unary operator functions? That feels strange since it's not a restriction present anywhere else, and it starts to feel like the labels have a dual purpose that also falls into the realm of keywords.

Either option doesn't quite sit right with me, even though the lack of duplication that this solution has does look better, admittedly.

···

--
Kevin Lundberg

On May 18, 2016, at 12:57 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

Additionally, I am generally +1 for the same reasons as Brent, but I
have another caveat as well:

Defining prefix and postfix functions looks like this in the proposal:

static prefix func ++(value: inout Self) -> Self
static postfix func ++(value: inout Self) -> Self

yet the proposal suggests calling them this way from these boilerplate
methods:

prefix func ++ <T: SomeProtocol>(value: inout T) -> T {
return T.++(prefix: &value)
}
postfix func ++ <T: SomeProtocol>(value: inout T) -> T {
  return T.++(postfix: &value)
}

I actually found this bizarre too, but forgot to mention it. My suggested solution runs in the other direction: We should require that *all* unary operator declarations and references use `prefix` or `postfix` as a parameter label. Thus, the trampoline operators themselves would be written as:

   func ++ <T: SomeProtocol>(prefix value: inout T) -> T {
       return T.++(prefix: &value)
   }
   
   func ++ <T: SomeProtocol>(postfix value: inout T) -> T {
       return T.++(postfix: &value)
   }

Not would be written as:

   func ! <B: BooleanType>(prefix value: B) -> Bool

While force-unwrap (if we had inout return values) would be written:

   func ! <T>(postfix value: inout Optional<T>) -> inout T

`prefix` and `postfix` would be eliminated from the language as declaration modifiers, except when declaring custom operators (which is already the Land Of Ad-Hoc Syntax).

--
Brent Royal-Gordon
Architechies


(Nicola Salmoria) #20

> * What is your evaluation of the proposal?

I'm generally in strong support, having long been a proponent of removing
operators from protocols (the first occurrence was in this thread:
http://article.gmane.org/gmane.comp.lang.swift.evolution/7935)

I have several comments about the details of the proposal, however.

1) At the beginning, in the "Proposed solution" section, the proposal says
"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."

Later, however, there is a "Deprecation of non-static protocol operators"
section which suggest to do exactly that, and this is reiterated in the
"Impact on existing code" section.

Since I think that the deprecation of global operator overloads is the
crucial point of the proposal, I assume that the former is an oversight.

I could probably do a better job of clarifying the wording here. The
proposal does *not* deprecate *all* global operator overloads. Global
operators can still be implemented as they have been in Swift. So if you
have a concrete type like `struct Matrix`, you can still define at the
global level `func +(lhs: Matrix, rhs: Matrix) -> Matrix`.

What's being deprecated is the current syntax used to define operator
requirements inside protocols (by making the functions static) and the
manner by which subtypes conform (ditto, through static methods instead of
global functions).

OK, I guess the unclear part is when you talk about "an addition that
specifically provides improvements for protocol operator requirements."
This is not just an addition; it's intended to completely replace the
protocol operator syntax.

2) The method signatures in the examples are not up to date with the
current
Swift 3 syntax. For example:

protocol Equatable {
  static func ==(lhs: Self, rhs: Self) -> Bool
}

should be:

protocol Equatable {
  static func ==(_ lhs: Self, _ rhs: Self) -> Bool
}

Unless I'm mistaken, from looking at the Swift 3 branch of stdlib, the
syntax changes don't appear to apply to operator functions. Since they are
a special case that don't have argument labels, it wouldn't make sense to
require them (or rather, the placeholders) here.

I don't agree with this.

Operators are called like this:

x = y + z

Of course it doesn't make sense to have parameter labels there.

But the ones inside the protocol are not operators. They are methods, and
are called like methods. They happen to have funny names, but they are
still methods, and are called like this:

x = T.+(y, z)

In this case not only it makes sense for the parameters to have labels, but
making them behave differently from normal methods would be inconsistent,
and a step backwards from all the progress that has been made in Swift 3 on
that front.

3) As has already been noted by many others, the suggested syntax for
prefix/postfix operators is overcomplicated. The proposal is:

// These are deprecated, of course, but used here just to serve as an
// example.
static prefix func ++(_ value: inout Self) -> Self
static postfix func ++(_ value: inout Self) -> Self

We don't need that. Since the 'operators' declared inside protocols are
effectively just normal methods (apart from their names), we just need to
name the parameters accordingly:

static func ++(prefix value: inout Self) -> Self
static func ++(postfix value: inout Self) -> Self

4) I don't agree with the request to limit to static methods for the
operator implementations.
I support this for symmetrical binary operators like +, but there are
other
operators like += that seem to work better with members. That is, the
proposed declaration:

static func +=(_ lhs: inout Self, _ rhs: Self)

is more similar to the global += operator definition, but is less clear
than:

mutating func +=(_ rhs: Self)

this is apparent also at the call site. With the proposed syntax, one
would
need to do:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
    T.+=(lhs, rhs)
}

while with a member function this would read more naturally as:

func +=<T: Foo>(_ lhs: inout T, _ rhs: T) {
    lhs.+=(rhs)
}

I considered this, but eventually settled on "everything is static" for
consistency. As you mention, there's a stronger argument to be made for
assignment operators to have "left hand side is the receiver" semantics
than there are for standard infix operators, but from a consistency point
of view (and ease of learning), I think having everything static and the
signatures of the static operators matching those of the global operators
is preferable.

I think this would better be left as a choice to the author of the
protocol. There doesn't seem to be any technical reason to place this
restriction.

(Which is also why, as I mentioned in a previous reply, I would be open to
dropping the prefix/postfix keyword and making it an argument label
instead, in both contexts.)

5) the proposal mentions the open question of ambiguities between the dot
syntax to access methods and operators whose name starts with a dot.
This seems to be a real issue: I don't think

return T....(minimum, maximum)

looks any good, even if the compiler was able to parse it.

However, this just means that the methods used to implement operators with
problematic names would need to use different names. Arguably, the only
cases where one would really want to use methods with operator names is
for
arithmetical operators. Custom operators like ... are better expressed as
methods with more significant names.

If there is a strong case where an operator is better implemented as a
global operator and a named method, this proposal still allows that, since
it's not deprecating all global operator definitions. A protocol could
certainly have a requirement that is a named method, and provide a global
generic operator that calls it.

6) It seems somewhat arbitrary to restrict method names to match an
operator, nor to put requirements on the function signature. I'd say there
are two cases, either the compiler can handle a method name that uses
special characters, or it can't. If it can't, matching an operator name
won't help. If it can, why put limits? There could be other creative uses
of
such names, which we would be ruling out for no particular reason. This is
something that seems better left to the author of the protocol.

IMO, to reduce potential confusion, I would argue that a function whose
name is the same as a defined operator should conform to the requirements
(such as argument count) of that operator. It's certainly worth discussion,
though! With that being said, it may be easier on users to "rule something
out" now and open it up later if need be, rather than to leave it open for
people to use and decide it needs to be closed later.

This doesn't seem different to me from having multiple functions with the
same name and different signature, which Swift allows without problems.
Again, I think this is a choice that the author of the protocol should
make, and there doesn't seem to be any technical reason to require
otherwise.

···

On Wed, May 18, 2016 at 8:03 PM, Tony Allevato <allevato@google.com> wrote:

On Wed, May 18, 2016 at 10:02 AM Nicola Salmoria via swift-evolution < > swift-evolution@swift.org> wrote:

7) Automatic generation of trampoline functions is out of scope so I'm not
going to talk much about it, I only want to mention that it would make
sense
to consider making such a feature as general as possible, instead of
focusing exclusively on operators.

For example, think of the common mathematical functions like sin, cos,
etc.
It could make sense to give them the same treatment as operators,
declaring
them as part of the FloatingPoint protocol but preserving the global
functions too.
It might even make sense to be able to create trampolines not only from
global space to a type, but also from one type to another type, or even
for
all methods of a type (e.g. when boxing a value inside another type).

> * Is the problem being addressed significant enough to warrant a change
to
Swift?

Absolutely. The handling of operators in protocols has been one of the
worst
pain points in my use of Swift.

> * Does this proposal fit well with the feel and direction of Swift?

Yes; it significantly increases clarity and consistency.

> * If you have used other languages or libraries with a similar feature,
how do you feel that this proposal compares to those?

I only have experience with C++ operator overloading, which is much less
advanced.

> * How much effort did you put into your review? A glance, a quick
reading,
or an in-depth study?

An in-depth study of the proposal, and I read all the relevant threads on
the mailing list.

--
Nicola

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