[Proposal:
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, 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(_:) 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
↔︎ 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(_:) 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_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution