Sunday, September 17, 2006

Failure of Imagination in Language Design

Failure of imagination - that is, failure to make language constructs orthogonal because you aren't quite sure how people will need to use them together - is one of the most common and severe errors you can make in programming language design. It is an error that is often impossible to correct after the fact. There is an approach to programming language design that you can take to maximize your opportunities to err in this way: consider the interactions of individual language features and decide on a case-by-case basis if the interaction will be allowed or not (or even what the semantics should be) based on whether or not you can find use cases and which semantics best fit those use cases.

This is an approach favored by expert API designers, because the domain of API design is best suited to this approach and these designers expect their expertise to carry from one domain to the other. In an API, the components of the API fit together in particular and well-defined patterns due to the typing and subtyping relationships between the elements; the job of the API designer is to ensure that the patterns you can build with the API elements satisfy a set of use cases.

In a programming language, on the other hand, the elements that are used to assemble programs are ideally orthogonal and independent. Programmers can assemble the elements in arbitrary ways, often unexpected but surprisingly useful ways. The "patterns" movement for example records some of these ideas by creating a common terminology for newly discovered ways of assembling programs from primitives.

To be sure, use cases also play a very important role in language design, but that role is a completely different one than the kind of role that they play in API design. In API design, satisfying the requirements of the use cases is a sufficient condition for completeness. In language design, it is a necessary condition.


Roman Elizarov said...

Good language design shall also strive to reduce programming errors that expose themselves at run-time. But this desire often comes into conflict with making orthogonal features.

It is more productive to catch errors at compile-time and Java, particularly, has a good standing at doing exactly that (compared to other main-stream languages). Many of Java’s original design decisions might seem arbitrary limitations (like a ban on using arbitrary expressions as statements) caused by "a failure of imagination" unless you look at them from a standpoint of error-prevention. When you extend Java language you should strive to maintain this particular intent.

I, for one, welcomed Java Generics because they allow catching some instances of ClassCastException at compile-time that would have otherwise appeared at run-time. I, for one, will welcome any proposal that reduces instances of NullPointerException or IndexRangeOutOfBoundException (I know that the latter is hard, so I don’t except it to happen soon). I, for one, will be against any feature that adds any new unchecked exception to the language repertoire regardless of how powerful it otherwise is. When any new feature is being considered one should immediately consider what run-time errors it might raise and what extensions to the type system or other constraints might make this feature safe [from run-time unchecked exceptions]. It will be hard to correct after-the-fact.

Jochen "blackdrag" Theodorou said...

orthogonality is what in a programming language? Maybe that it doesn't "disturb" other constructs... but what are these? Do we say it is orthogonal if the grammar is still LL(k) after the change?

I think programming language design is about ideology. Of course ideology is badly paired with creativity.

If you don't care about ambiguity or context sensitive constructs, then you have much freedom, but the readability might lack.

And of course the grammar is only one aspect, the type system, having exceptions or not, build in support for parallel processing, plattform sensitivity... all these do also play a big role.

And the more complex you make something, the less freedom you do grant the developer, but also the language designer. With a very simplistic system you get the most freedom. I mean look at Lisp. The syntax is puristic, the type system neraly not there and yet it is such a mighty language. But many people don't like it, because of its might, because of its puristic language. Not only the language designer does have to fight with ideology, the programer too.

Neal Gafter said...

Roman: NullPointerException can be eliminated in a language by disallowing dynamic memory allocation of any kind. array store exceptions by not making arrays covariant. class cast exceptions by not allowing downcasts. And so on. Not everything that adds expressiveness to the language can be done with all checking at compile-time.

Lets not throw out the baby with the bathwater.

Roman Elizarov said...

Neal, you are certainly right that less-expressive languages generally suffer from fewer potential run-time errors. However, run-time errors can be eliminated in a language by a careful design in a number of ways that are available in both research literature and actual implementations. For example, look at the Java-based research language called NICE. It nicely (put intended) eliminates both NullPointerException and ClassCastException without sacrificing dynamic memory allocation or downcasting. It simply requires to explicitly add the corresponding checks where, for example, method invocation on a potentially null object is being performed and NullPointerException could have been generated at run-time. It all boils down to a powerful type-system (a well-know eliminator or run-time errors) and other various tricks in a language design.