Friday, January 11, 2008

Is the Java Language Dying?

We tend to think of programming languages in two categories: "living languages" in which we should seriously consider developing new code, and "legacy languages" that we mainly use, if at all, because we have to maintain an existing code base. The act of classifying a language into one or the other category helps us decide what, if anything, we might consider doing to change the language. If a language is primarily a legacy language, changes should be aimed at making it easier to maintain and modify existing bodies of code. A living language, on the other hand, also benefits from changes that make it easier to design, develop, and maintain new code. Living languages evolve to reduce accidental complexity.

"What does a high-level language accomplish? It frees a program from much of its accidental complexity. An abstract program consists of conceptual constructs: operations, datatypes, sequences, and communication. The concrete machine program is concerned with bits, registers, conditions, branches, channels, disks, and such. To the extent that the high-level language embodies the constructs wanted in the abstract program and avoids all lower ones, it eliminates a whole level of complexity that was never inherent in the program at all." -No Silver Bullet - Fred Brooks

Programs written in legacy languages tend to exhibit a high degree of accidental complexity [Code's Worst Enemy, Steve Yegge] [Mr. Yegge meets Mr. Brooks, Brian C Cunningham]. Early in the life of a language, the complexity of programs written in that language may appear to be essential, but as we learn more about software engineering and programming languages, we find patterns of complexity appearing in the code that can be eliminated by improved languages.

A good example of this is garbage collection. In C and C++, memory management is a pervasive concern. Smart pointers and destructors help, but they do not significantly reduce the complexity of memory management. In languages with garbage collection, most of the complexity of memory management is assumed by the implementation of the language. Most languages that have been introduced in the past ten years support garbage collection.

Another example is concurrency. The threads-and-locks-and-semaphores primitives of Java enable parallel programming, but require that programmers express concurrency at a fairly low level. This has been "good enough" for some time, as most programs are not deployed on highly concurrent hardware. But that is changing [The Free Lunch Is Over, Herb Sutter]. Libraries such as java.util.concurrent and Doug Lea's fork-join framework help somewhat, but in many cases they introduce complexities of their own. Other languages that support closures, such as Scala make fork-join-like libraries much easier to use. Scala supports control abstraction [Crowl and LeBlanc], which allows the libraries to manage much of the complexity associated with concurrency [Debasish Ghosh]. Support for the Actors model [Haller and Odersky], for example, can be expressed cleanly as a library in Scala

Besides raising the level of abstraction of concurrent code, control abstraction also raises the level of abstraction for sequential code by eliminating whole categories of boilerplate, which can instead be moved into common library code. This kind of boilerplate cannot be significantly reduced by adding one or two custom statements to the language, because such built-in forms necessarily make assumptions about the use cases that narrow their applicability. For example, ARM blocks don't document how they handle exceptions arising from the close() method. One example in the proposal suggests they are silently swallowed at runtime, which may work for many cases involving I/O streams, but another example given is a transactional API, in which ignoring such exceptions is precisely wrong. Without a specification for the syntax and semantics, the reader is welcome to imagine the most favorable treatment of each use case. But an attempt to reconcile these and other conflicting requirements may show the approach cannot be salvaged. Perhaps that is why no progress has been made since mid 2006.

What about Java? Is it a living language, or a legacy language like Cobol? This question underlies much of the debate about how to move the Java programming language forward, if at all. Carl Quinn asked at the December 14, 2007, JavaPolis Future of Computing Panel (to be published on http://www.parleys.com): "How can we address the issue of evolving the [Java] platform, language, and libraries without breaking things?"

Neal Gafter: "If you don't want to change the meaning of anything ever, you have no choice but to not do anything. The trick is to minimize the effect of the changes while enabling as much as possible. I think there's still a lot of room for adding functionality without breaking existing stuff..."
Josh Bloch: "My view of what really happens is a little bit morbid. I think that languages and platforms age by getting larger and clunkier until they fall over of their own weight and die very very slowly, like over ... well, they're all still alive (though not many are programming Cobol anymore). I think it's a great thing, I really love it. I think it's marvelous. It's the cycle of birth, and growth, and death. I remember James saying to me [...] eight years ago 'It's really great when you get to hit the reset button every once and a while.'"

Josh may well be right. If so, we should place Java on life support and move our development to new languages such as Scala. The fork-join framework itself is an example of higher-order functional programming, which Josh argues is a style that we should neither encourage nor support in Java. Is it really time to move on?

Personally, I believe rumors of Java's demise are greatly exaggerated. We should think of Java as a living language, and strive to eliminate much of the accidental complexity of Java programs. I believe it is worth adding support for closures and control abstraction, to reduce such complexity of both the sequential and concurrent aspects of our programs. At the same time, for completely new code bases, we should also consider (and continue to develop) newer languages such as Scala, which benefit from the lessons of Java.