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

Tony Allevato allevato at google.com
Fri Jun 10 10:24:34 CDT 2016


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 <
swift-evolution at swift.org> wrote:

> Proposal link:
>
>
> 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 <clattner at apple.com> 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.



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



>
> 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 at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160610/523d0fb1/attachment.html>


More information about the swift-evolution mailing list