Thursday, September 03, 2009

Systemic Overload

Type-based method overloading is a (mis)feature of mainstream statically typed languages. I am very much against it - I’ve spoken about it more than once in the past. I decided I should put my views on the matter in writing, just to get it out of my system. I’ll start with the better known problems, and work my way up to more exotic issues.

Throughout this post, I’ll use the term overloading to denote type based overloading. Arity based overloading, as in e.g., Erlang, is pretty harmless.

When introducing a feature into a language, one has to consider the cost/benefit ratio. You might argue that overloading lets you separate the handling of different types into different methods, instead of doing the type dispatch inside one method, which is tiresome and costly. A classic example are mathematical operators - things like +.

This argument would have merit, if the overloading was dynamic, as in multi-methods. Since it isn’t, overloading doesn’t solve this kind of problem. Not that I’m advocating multi-methods here - they have their own problems - but at least they are based on accurate type information, whereas overloading is based on crude static approximations.

This simple example illustrates the dangerous disconnect between static and dynamic information engendered by overloading.

What exactly is the benefit of type based overloading? It saves you some trouble inventing names. That’s it. This may seem important, but the only case where it is actually needed is when writing constructors - and only because the language doesn’t allow you to give them distinct names.

Constructors are a bad idea, as I’ve already explained, so let’s assume we don’t have them. For methods, type based overloading provides a trivial rewrite and that is all. I don’t think it is that hard to give the different operations different names. Sometimes, the language syntax can make it easier for you (like keyword based method names in Smalltalk), but even in conventional syntax, it isn’t that difficult.

You pay for this convenience in myriad ways. The code above exemplifies one set of issues.

Another problem is the risk of ambiguity. In most overloading schemes, you can create situations where you can’t decide which method to call and therefore declare the call illegal. Unfortunately, as the type hierarchy evolves, legal code can become illegal, or simply change its meaning.

This means that existing code breaks when you recompile, or does the wrong thing if you don’t.

Overloading is open to abuse: it allows you to give different operations the same name. Altogether, you need style guides like Effective Java to warn you to use overloading as little as possible. Language constructs that require style guides to tell you not to use them are a bit suspect.

Ok, you probably know all this. So what contribution does this post make? Well, the systemic costs of overloading in terms of designing and engineering languages are less widely appreciated.

Overloading makes it hard to interoperate with other languages. It’s harder to call your methods from another language. Such a language may have a different type system and/or different overload rules. Or it may be dynamically typed.

You often find a dynamic language implementing multi-method dispatch to approximate the behavior of overloaded methods it needs to call. This is costly at run time, and is a burden on the language implementor.

Scala supports overloading primarily so it can call Java; you might say overloading is a contagious disease, transmitted from one language to another through close contact.

In general, overloading adds complexity to the language; it tends to interact with all sorts of other features, making those features harder to learn, harder to use, and harder to implement. In particular, any change to the type system is almost certain to interact with type based overloading.

Collection&lt String&gt cs;foo(cs);/* Which of these is legal? What would happen if we didn’t use erasure? You have 3 seconds. */

Each of the above exhibits shows a specific type system extension which gets entangled with overloading.

You might say you don’t care; these are pretty sick examples, and the language designers sorted out the rules. What is their suffering to you? Well, these complications all have cost, and since resources are finite, they come at the expense of other, more beneficial things.

Tangent: People tend to think that the language design and implementation teams at large companies like Sun or Microsoft command vast resources. In reality, the resources are spread pretty thin considering the scope of the task at hand.

The more time is spent chasing down these issues, the less is spent focusing on doing stuff that actually buys you something. Products ship later and have more bugs and less desirable features, because people spent time worrying about gratuitous complications.

This is not hypothetical. I know the poor soul who wrote the spec for this might have spent his time doing something else, like day dreaming or adding closures. The compiler writers could have fixed more outstanding bugs, perhaps even reaching a state where there were no open bugs. I know they tried. The testers could have tested for other, more pressing issues and discovered more bugs to open, before shipping. These are the wages of sin - and the sin is unjustified complexity.

Now, for the sake of balance, I should say that overloading, and language complexity in general, do have one advantage I haven’t yet mentioned. They open up great opportunities for training, support and consulting. You can even write some really cool books full of language puzzlers.

It’s worth noting that this kind of overloading is only a concern in languages with a mandatory type system. If you use optional typing (or just dynamic typing), you aren’t allowed to let the static types of the arguments change the meaning of the method invocation. This keeps you honest.

Will future language designs avoid the problems of overloading? I wish I was confident that was the case, but I realize overloading is an entrenched tradition, and thus not easily eradicated.

However, the overarching point I want to make is that the costs of complexity are insidious - unobvious in the short term but pervasive over the longer haul. KISS (Keep It Simple, S - where the binding of S is open to interpretation).

Answers:

The issue here is whether we auto-unbox Integer(3) first to produce an int (and call foo(int)) or resolve overloading in favor of foo(Number) and don’t unbox at all. Java does the latter. The reason is to remain compatible with older versions.

This is ambiguous. Except for the first declaration, no method is more specific than the others.

They all have the same erasure, and so the example is illegal. If we did not use erasure, than foo(Collection &lt String &gt) would be the most specific method.

15 comments:

My view is that overloading in Java is mostly needed between primitive types: it's often useful to provide foo(int), foo(long), and foo(double) versions. One could do this with foo(Number), autoboxing, and RTTI, but that's more of a headache than overloading.

Occasionally there are other types that are semantically closely related, but for whatever reasons have no type relationships. For example, before Comparable, I wound up with methods overloaded on Numbers and Strings, and so on.

Overloading is the static analogue of multi-method dispatch. Multi-method dispatch is useful in and of itself - consider the contortions that the Visitor pattern has to go through with double dispatch just to end up in the right method depending on the kind of argument. Even then Visitor is limited to a number of distinct types which the visited and visiting have agreed upon, limiting extension in derived types.

When you're in the extreme end of generic programming in e.g. C++, this static analogue really starts coming into its own. Here, overloading is not equivalent to choosing distinct names, because the appropriate name is unknown to the caller - the overload may be chosen based on the type of a type argument.

I consider your examples specious to a fairly large degree, as having overloads where the parameter types are subtypes of one another, and a decision must be made based on the degree of derivation or conversion rules, is usually only done for performance reasons - to prevent the requirement for expensive class-type testing, expensive boxing and unboxing, etc. - things that are only really expensive when done billions of times, but expensive none the less and accounted for in the API so that the optimization takes place with little or no effort from the programmer. More usually, overloads are done based on quite distinct types.

For example, consider a Save method. Is it really better to have SaveToStream, SaveToFile, SaveToBinaryWriter, SaveToTextWriter, SaveToXmlWriter methods? Or would a single overloaded Save method not lead to less verbose, more clear, and easier read code - with no possibility of confusion, as none of the receiver types are subtypes of one another?

And even if overloading spares you from inventing names, a questionable advantage, you still have to invent them for different return types (at least in all languages known to me). Think of readInt(), readLong() and so on.

I disagree about the fact that multiple dispatch is slow and I don't see where it has problems (perhaps you could write a follow-up article and justify your claims).

There has been success in whole programme analysis with optimising away runtime type checks for multiple dispatch.

However, multiple dispatch has limits, e.g. in Dylan there's parameter list congruency. I'm not sure, whether this is a reasonable limit in the sense that it prohibits programmers from doing dangerous things or if it is actually a limitation, but not a problem.

I didn't say multiple dispatch was slow. I did say that if you implement it especially to call overloaded methods in another language, you are paying a run time cost that is unfortunate.

The main issue with multiple dispatch a la CLOS (and I believe this covers Dyla) is the fact that calls can be ambiguous, and can become ambiguous after the fact. Thus multiple dispatch improves modularity in own way, but undermines it in another.

Is an extension to Java that adds design patterns, one of the patterns is multiple dispatch (instead of visitor). The implementation isn't slow and isn't ambiguous. Sp I think multiple dispatch is possible in a mainstream OO language.