Dipping my toes in the water, so to speak, with a suggestion that came to
mind. Not an expert by any means, so hopefully this isn't too laughably off
the mark. As background, I work in the biomedical sciences; with few
exceptions, those of us who write code are non-experts who learn as we go
along. What's been attractive about Swift is that it's been designed with
approachability in mind for beginning coders and those with a rudimentary
understanding of languages in the C family. Difficulty in syntax and
sophistication of the computer science concepts exposed are hurdles that
can be overcome, but what keeps me up at night are potentially subtle
things maybe easy for the expert that I or my colleagues just might not
know about, which then lead to code that appears correct at first glance,
compiles, but executes in subtly unintended ways. Those things in my field
are what could lead to retracted papers, ruined careers, etc....
This is something along those lines; also somewhat of a reprise of a theme
that's been raised tangentially in a few threads within the past month
(e.g.:
https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/001125.html\),
but I think the particular solution I'm envisioning hasn't been put forward.
The issue at hand:
Consider the following (contrived) example (call it version 1)--
class Animal {
func makeNoise() -> String {
return "generic sound"
}
}
class Cow: Animal {
override func makeNoise() -> String {
return "moo"
}
}
class Sheep: Animal {
override func makeNoise() -> String {
return "bah"
}
}
let cow = Cow()
cow.makeNoise() // "moo"
(cow as Animal).makeNoise() // "moo"
let farm: [Animal] = [Cow(), Cow(), Sheep()]
let noises: [String] = farm.map { $0.makeNoise() }
// ["moo", "moo", "bah"]
Now refactor to use a protocol. I'll give two versions, differing by a
single line.
Version 2A--
protocol Animal { }
extension Animal {
func makeNoise() -> String {
return "generic sound"
}
}
class Cow: Animal {
func makeNoise() -> String {
return "moo"
}
}
class Sheep: Animal {
func makeNoise() -> String {
return "bah"
}
}
let cow = Cow()
cow.makeNoise() // "moo"
(cow as Animal).makeNoise() // "generic sound"
let farm: [Animal] = [Cow(), Cow(), Sheep()]
let noises: [String] = farm.map { $0.makeNoise() }
// ["generic sound", "generic sound", "generic sound"]
Version 2B--
protocol Animal {
func makeNoise() -> String
}
extension Animal {
func makeNoise() -> String {
return "generic sound"
}
}
class Cow: Animal {
func makeNoise() -> String {
return "moo"
}
}
class Sheep: Animal {
func makeNoise() -> String {
return "bah"
}
}
let cow = Cow()
cow.makeNoise() // "moo"
(cow as Animal).makeNoise() // "moo"
let farm: [Animal] = [Cow(), Cow(), Sheep()]
let noises: [String] = farm.map { $0.makeNoise() }
// ["moo", "moo", "bah"]
To be sure, this difference in behavior can be justified in a variety of
ways, but it is nonetheless a surprising result at first glance. Most
concerning is that it is possible to imagine a scenario in which the
protocol in question is one provided in a third-party library, or even the
Swift standard library, while I'm writing a struct or class that implements
that protocol.
Suppose that between versions of a third-party library the Animal protocol
changes from version 2A to version 2B. My struct or class that implements
the protocol would compile without changes, and cow.makeNoise() would even
give the same result, yet there would be differences in how my code works
that would be difficult to track down. An expert would be able to spot the
difference on examination of the protocol declaration, but one would have
to be knowledgeable already about this particular issue to know what to
look for. This seems like a gotcha that can be avoided.
Proposed solution:
I think others have tried to approach this from a different angle (see, for
example:
https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/001933.html\).
My view is that there are two points potentially to address.
(1) func makeNoise() -> String { ... } within the protocol extension
indicates two different things in versions 2A and 2B (since one is
dynamically dispatched and the other is not), yet the syntax is
indistinguishable.
(2) Within a class or struct implementing a protocol, a method with the
same name as that of a method in a protocol extension (potentially in
another file, maybe not written by the same person) behaves differently
depending on whether that method name is declared in the protocol itself
(potentially in a third file, written by a third person), yet the syntax is
indistinguishable within the implementing class or struct. If the protocol
changes, whether a method is dynamically dispatched or statically
dispatched could change too, yet code in a class or struct implementing
that protocol that now behaves differently compiles all the same;
implementors are not clued into the change and may not even be aware that
changes such as this could happen.
What I'm thinking is a light-touch fix that would address (2), which would
largely mitigate the consequences of (1). Taking inspiration from syntax
used for methods in classes that override methods in superclasses, require
methods that override dynamically dispatched default implementations in
protocol extensions to use the override keyword. Likewise, forbid the
override keyword if the method being implemented instead 'masks' (would
that be the right word?) a statically dispatched method in a protocol
extension which can nonetheless be invoked by upcasting to the protocol.
In other words, I propose that in the contrived example above, version 2B
(which behaves just like version 1) requires syntax just like that of
version 1 (in class Cow and class Sheep, override func makeNoise() ->
String { ... }). Meanwhile, version 2A, which doesn't quite behave like
version 1, forbids the same syntax. That way, anyone confused about what he
or she is getting into when implementing the protocol is notified at
compile time, and code that compiles in one scenario will not compile if
the underlying protocol declaration changes.
I suppose quite a good counterargument would be that protocols exist to be
implemented, and implementations of methods required for conformance to a
protocol shouldn't need another keyword. I would be inclined to agree, but
in this case I'd point out that what would trigger the requirement for use
of the override keyword is the presence of a default implementation being
overridden, not the mere fact that a method declared within a protocol is
being implemented.
Would this be something that is desirable to other users of the language?
Easy to implement?