[swift-evolution] [Review] SE-0091: Improving operator requirements in protocols
Jordan Rose
jordan_rose at apple.com
Mon May 23 23:58:30 CDT 2016
[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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160523/2764b1f1/attachment.html>
More information about the swift-evolution
mailing list