[Proposal:
https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md
]
I definitely think this is an improvement over the last version! Nice
work, Tony and Doug.
I *am* a little confused about the implementation, though. The proposal
says this:
Instead, Swift should always perform operator lookup universally such that
it sees all operators defined at either module scope or within a
type/extension of a type. This gives us the syntactic
improvements immediately and the natural Swift thing of defining your
functionality within the type or an extension thereof just works.
and then later says
Therefore, we can achieve the performance improvements by making that
insight 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.
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 as if users had
declared trampoline operators, but it's automatic.
How do we know if an operator function implements a protocol requirement?
Well, we can track it explicitly in the modules that define the protocol
and that define conformances of specific types to the protocol.
Alternatively, we try the protocol requirements *first*. Once we’ve
inferred the ‘Self’ type of the protocol (which is part of trying the
protocol requirement), we can look up a conformance and the witness to see
which witnesses should no longer be considered.
What happens when an operator function implements a protocol requirement,
but is also more general than that?
*Right now*, it’s only really possible when you’re using a global generic
operator, because we require exact type matches between requirements and
witnesses. If/when we allow the witness to be a supertype of the
requirement, you’ll start to see more of the semantic effects of this
model, because shadowing the witness with the requirement can reject code
that is well-formed now.
That’s why the shadowing behavior needs to be part of the semantic model.
Implementation will let us settle the exact details so we can state those
semantics more precisely.
And if we do find the implementation in the protocol, what conformance do
we use to invoke the function when the types involved aren’t all 'Self’?
If there isn’t a reference to ‘Self’, type inference for the use of the
protocol requirement will fail.
I still prefer the rule that says we perform lookup into the left type and
the right type, then fall back to top-level scope for backwards
compatibility.
We thought about it a lot, and it’s not implementable in a way that’s
consistent with type inference, because one of the left or right types
might not be known yet, and you also need to consider the context type for
something like, e.g.,
let x: UInt = 1 + 2
Separately from the lookup rules, I’m still unhappy with the class
problem. The proposal states this:
We expect classes to implement the static operators in the protocol using
`class` methods instead of `static` methods, which allows subclases to
override them.
However, if lookup only finds the method in the protocol, it’s unclear
whether this will call a conforming class's method, a static type’s method,
or a dynamic type’s method; if it’s not the last, it’s hardly an
“override”. I maintain that this is the wrong behavior for any class
hierarchy that *does* include heterogeneous operations, including
"assignment operators, operators for chaining tasks, DSLs for constraint
systems, etc” (me, from last time).
More from last time:
- 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
I guess I’m not convinced of the chain of reasoning here. “Multi-method
dispatch is the most correct way to solve the problem” is fine; “therefore,
anything short of that isn’t worth doing” is where I get stuck. Instance
methods partially solve the problem, and it’s possible (again, no data on
hand) that they solve the problem in the majority of cases.
(It’s also possible that the prevalence of OO has made people prefer
operators that can be dispatched based on the left-hand side, so I guess
I’d want to go look at, e.g. Haskell and Perl to see what operators don’t
fit in that bucket.)
I guess I’d summarize my stance as “this proposal enshrines our current
problems with operator semantics in order to improve consistency in the
syntax” (with “enshrines” meaning “makes harder to change later”), and that
doesn’t seem like a good enough reason to change from what we have now.
…and I have to say I still feel that way. It’s not clear how much of a
performance win we’ll get, and it’s not clear these are the right
semantics, and it *is* clear that operators interact poorly with classes.
This is a good point, and one that’d I’d missed from the previous
discussions. Left-bias is probably defensible