> On Jun 10, 2016, at 8:24 AM, Tony Allevato <[email protected]> wrote:
> 
> Thanks for your feedback, Doug! I've addressed some of your concerns inline.
> 
> On Thu, Jun 9, 2016 at 10:16 PM Douglas Gregor via swift-evolution 
> <[email protected] <mailto:[email protected]>> wrote:
> Proposal link:
> 
>       
> https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md
>  
> <https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md>
> 
> Hi all,
> 
>> On May 17, 2016, at 8:33 PM, Chris Lattner <[email protected] 
>> <mailto:[email protected]>> wrote:
>> 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.

My point (1) is about that confusion, and also that the most natural place to 
put an operator—inside a type or extension thereof—is not the correct place to 
put the operator.

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

You’re correct that I am not required to introduce a protocol for the new 
operator. If I don’t, however, then we don’t get any of the type-checker 
performance or QoI benefits from this proposal. Worse, if we introduce the 
protocol later—and don’t go refactor all of the global operator functions into 
members—we also don’t get the type-checker performance or QoI benefits. So, 
even if the proposal doesn’t specifically require what I’m complaining about in 
(2), it’s very likely to happy in the standard library and become “lore” for 
how to define operators.

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

Autogenerating trampolines eliminates one bit of the boilerplate, yes.

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

If the trampoline functions don’t cover enough types, or we end up with lots of 
different trampoline functions (one for FloatingPoint, one for IntegerProtocol, 
another for Stridable, and so on), then we won’t actually end up with a 
significantly smaller overload set. So either we lose those benefits, or we end 
up slicing the protocols into smaller, reusable (but meaningless parts).

Let’s look at it this way: FloatingPoint will cover maybe 2-5 types (Float, 
Double, CGFloat (Darwin), Float80 (Intel), HalfFloat (maybe someday?)) . We’ll 
either need to abstract it further—into Arithmetic so we pick up the integral 
types, for example, or further so we can pick up String + String—or start 
adding more protocols that cover the same + syntax and have their own 
forwarding functions.

> 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

We’ve thought about this, and it doesn’t work in Swift’s type system. It’s 
similar to the rule that C++ uses for operator lookup—called Argument Dependent 
Lookup, or ADL—which looks in the types of the operands (and a whole pile of 
types and namespaces related to it). But that doesn’t work in a language with 
bidirectional type inference like Swift has, because you have to also consider 
the return type of the operator and there’s not necessarily any point at which 
you know all of the input types and the result type. Let’s do something fun:

extension String {
  func /(lhs: Character, rhs: Character) -> String
}

let s: String = ‘a’ / ‘b’

We need to look in String to find this operator, which we can only do when we 
consider the return type. One doesn’t even need to involve the result type, 
because the way Swift handles literals makes it very hard to answer the 
question “what type is this expression going to have?”. Consider:

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

let x = 1 +++ 2

There’s essentially no way to find that +++ based on the types, because 
literals don’t have types by themselves. However, if we did find the +++ based 
on some global lookup, the result will type-check. 

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

I think there’s always going to be a potential for ambiguity in this space. I’d 
only be particularly concerned about it if our rule caused people to have to 
duplicate operator functions in different types so that at least one of them 
would be found.

        - Doug

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to