My last post tried to make a case in favor of static typing based on the fact that it allows us to do overload resolution. At the time I hadn’t read this post on Gilad Bracha’s Newspeak blog. In the thread on another post, he summarized the sprit of this essay when he commented that:
“Static type based overloading is a truly bad idea, with no redeeming value whatsoever”
I’m not going to claim that I know language design better than Bracha. I will, however, disagree with this extreme position on overloading. If you haven’t read Bracha’s essay, please do so before you proceed…
Let’s first touch on the examples that Bracha used to illustrate his case. Some of these examples relate to legacy issues in Java, and are thus not inherent to languages with overloading. I’ll happily dismiss them since I don’t have to deal with Java.
The rest involve overloads with the following two properties:
- The methods are all defined within a single class
- The methods are specialized on types that are related by inheritance
I claim that it is the combination of these two properties that is the crux of the argument. If the types involved are not related by inheritance, the “gotcha” aspect of figuring out which overload will be called goes away. And because the methods are all defined in one class (by one programmer?) we can trivialize the cost associated with renaming one of the overloads, or of planning to avoid the situation altogether.
For this limited case – “(1) and (2)” – I actually buy the argument. Static overloading in this case doesn’t do what you want. But what Bracha neglects to mention is that a pure object-oriented message send doesn’t achieve the desired result either! What you want in this case is dispatch on the run-time types of multiple arguments, aka multiple dispatch, aka multimethods.
There are legitimate concerns with multimethods (which Bracha notes) as expressed in e.g. CLOS and Dylan. There are, however, other approaches that are more suitable for a new language. That is a discussion for another day.
Having ceded the argument in the “(1) and (2)” case, in favor of multimethods, that leaves us with the remaining cases, which Bracha didn’t directly address.
The “(1) but not (2)” case is harmless – there is no chance of ambiguity in dispatch. Multimethods subsume this case for overloading anyway, so I don’t think it is particularly useful to discuss.
The remaining cases must all deal with methods that weren’t defined within a single class. We might also presume, then, that we should consider the possibility that the methods involved were defined by different programmers, working at different organizations.
Suppose programmer A defines their
Widget class version 1.0. Programmer B decides to use it as the base class for their
SuperWidget has extended
Widget by adding a new message “
doSomethingSuper” with semantics that are tied into B’s product.
Unbeknownst to B, though, A has been upgrading
Widget for version 1.1 by adding their own “
doSomethingSuper” method, with completely different semantics (after all, B doesn’t know about A’s product). If B tries to upgrade to the new version of
Widget, then what happens?
In a language like Python or SmallTalk,
SuperWidget will accidentally override the base class definition of “
doSomethingSuper“. Now clients that try to use a
SuperWidget as a
Widget 1.1 will fail because
SuperWidget responds to a message with an unexpected behavior.
If you try this same scenario out in C# and the Microsoft CLR, you’ll find that previously-compiled versions of
SuperWidget keep working with
Widget 1.1, and clients that use it as a
Widget will have no problems. If you recompile
SuperWidget after the upgrade, you will be told that your “
doSomethingSuper” method might introduce an ambiguity – you will be forced to decorate it explicitly as either an
override of the base-class method, or a
new method that just happens to have the same name.
The secret that makes this technique work is – you guessed it – static overload resolution. This is exactly the opposite of Bracha’s claim about static overloading in his essay:
“This means that existing code breaks when you recompile, or does the wrong thing if you don’t”
In this case, however, it is the overloading-free languages which inhibit the modular extensibility of the system, and static overloading that makes it possible for another language to avoid the problem.
Overloading is generally not something we pursue, even when our languages support it. Instead, we simply recognize that it is something that arises inevitably when we develop large software systems that aggregate and extend components developed by other programmers and other organizations. The space of useful identifiers is just too small to avoid all conflicts.
Given this fact, I choose to use tools that recognize the inevitability of name conflicts and give me mechanisms for resolving them.