Summary
In which I argue that (a) Generics have done egregious harm to both the elegance and readability of the Java language and,
(b) they prove by example that static type checking is a linguistic dead-end. Are you persuaded? Do you agree? Read on...
Advertisement
I confess. I'm a Ruby hacker, at heart. Ruby's Perl-isms can't go away
fast enough to suit me, but what's left after removing them is a thing of utter beauty and precision. Not to mention power.
But long before I became enamored of Ruby, I was a Java hacker.
I've recently come back to Java, and was rather surprised by the strength
of my reaction--mostly as a result of generics.
I admit to being mildly annoyed by all the semicolons, braces, and parentheses
that the compiler forces me to supply. It makes me feel like a slave, working
for a not-very-bright master. I mean, the compiler could be smart enough to
know what's missing. It just isn't. It prefers to give me an error, instead.
And of course, I miss Ruby's functional coding style, closures, and the flexibility
provided by duck typing, open classes, and dynamic hooks--things that give the
language it's expressiveness and power.
Don't get me wrong. I continue to admire Generics as a tour-de-force of
implementation genius. Getting them to work in a backwards-compatible way
was nothing less than brilliant.
At the same time, I decry their very existence. They are the ultimate
condemnation of static type checking--because their very addition
has virtually destroyed both the readability and elegance of the language.
To make my case, let's start with a quasi-mathematical definition of
"elegance". In my view, elegance is a function of power and simplicity.
Power is measured by how much you can do. Simplicity is the inverse of
the number of characters it takes to achieve the result. So more power
with fewer characters equals "elegance", in my book.
Of course, by that measure APL is a very elegant language! And it is.
But it's famously unreadable. Somewhere, there is a quote from a very
smart guy who spent 4 days figuring out what a 4 line program did--
a program that he himself had written a few months earlier. That has to
set some kind of standard for unreadability.
So "elegance" has to be tempered with the equally important notion of "readability".
Readability is also a function of the number of characters on the page, but
it has a bell curve: Too few characters, and the code is indecipherable.
But with too many characters, the code is effectively obfuscated. It becomes
impossible to see the forest for the trees. At that point, the code is obscure.
Between "indecipherable" and "obscure", lies the domain of "readable".
That realm is the sweet spot. It's one that Java has been pretty darn good
about occupying.
Until generics.
With the advent of generics, elegance has obviously been impaired (more characters,
same power). At the same time, I claim that generics have pushed the language into
the realm of obscurity.
That is an entirely subjective assessment, I admit. People have different levels of
tolerance for that kind of thing. I mean, there will always be folks who prefer
SALES MINUS COSTS DIVIDED BY YEAR
to
(sales - costs) / years
To each their own. But there is no doubt that with the advent of generics,
Java code is less readable (and therefore more obscure) than it once was.
The result is a two-fold loss--a loss of both elegance and readability--that to
my mind is nothing short of a travesty. It represents the virtual destruction of
a good language at the hands of something that undoubtedly seemed like a good idea
at the time.
When I look at code I've written using generics, and mentally strip out the
generic portions, I can't say that the code is any better or worse than it
was before. Generics just don't seem to be adding any real value.
Of course, generics create greater "type safety" at compile time. And that,
I think, is the ultimate condemnation of static type checking: They show how
much work you really have to do to get it. And what is your return for that
investment? Not much, really. You get a small percentage increase in the
number of real errors found at compile-time.
But to get that marginal increase, you spend more time adding required
syntax, watching the compiler find silly omissions and typos, and making changes
solely to satisfy the compiler. In return, you get code that is less readable.
If all that work produced code that was truly bulletproof, it could easily be
worth the investment of effort. But it doesn't. You still have to do an enormous
amount of testing to get any real quality. And by the time you've done all that
testing, static type checking winds up being superfluous.
Sure, in some super-critical applications like the Mars mission or a stock
exchange, the tradeoff may be acceptable. Even a tiny improvement in quality
is worth a significant investment, because the cost of a defect is so high.
But for day to day programming, the tradeoff just isn't worth it, in my book.
I'm a big fan of the "foreach" concept. A phrase like this one clearly
communicates the code's intent:
foreach item in List {
...
}
In Ruby, the equivalent mechanism is even cooler, once you get used it:
List.each { |item|
...
}
It's cooler, because "each" is a method that can be applied to any
collection.
Sidebar: Going Crazy with Functional Expressions
Since every block is an expression, and every expression
returns a value, if you want to go nuts, you could even apply
a dot-operator to the block:
List.each { |i| ... }.doSomething()
Crazy, huh? You wouldn't do that often, but there are times...
Want to iterate over a Map, processing each key/value pair
in turn? Here's the syntax:
Map.each { |key, value|
...
}
And now here's the equivalent expression in Java, with generics:
for(Map.Entry<K, V> e : map.entrySet()) {
...
}
Sorry, but that's an expression that only a mother could love.
Without generics, it's not too bad. With generics and the
other syntactic elements to required to make it legal, the
statement verges on unreadable.
In general, the trick to reading such loops is to translate the litte
colon to "in" and, once you see the colon, to mentally convert the "for"
in the expression to "for each". So with a simple, "unsafe" loop, when you see
this:
for (item : List) {
}
You read:
for (each) item (in) List ...
That's not bad. But lets look at the Map example again. This time,
adding the statements needed to get values to work on.
With the generic declarations added in, plus the need to invoke a
method on the map, and all of the parentheses and angle brackets
that are required, the colon is hard to see. It is effectively lost
amid the noise. You have to peer at the expression carefully to
discern its intent--especially when you come across it in the context
of a larger program.
In fact, it was after copying that very expression from the web that
I became disenchanted with generified Java. For me, Java's
huge advantage from day one has always been it's readability. (The
Web interface library didn't hurt, either. But this is about the language.)
With Java, there was only one way to do anything. There were no #ifdefs
or home-grown macros, so no matter whose code you read, you always knew
exactly what it was doing. So it easy to read. In consequence it was
(relatively) easy to learn.
Ruby is more like Perl, in that regard. There are so many ways to do
things that, to read anyone else's code fluently, you first have to
master the particular set of idioms they employed.
So score one for Java in the realm of readability. Until generics.
A subtle syntax element like ":" can't stand up against the deadly
incursion of ubiquitous type-declaration syntax. To even begin to
compete, it would have to employ COBOL-like syntax--in all caps, at that:
for(Map.Entry<K, V> e IN map.entrySet()) {
...
}
That, at least, would be a little closer to readable. But of course
it would be even less elegant.
Pretty cool, huh? You merely invoke an intermediate sort() method
to get the result you want. It's even more readable because parentheses
on method invocations are only required when they are absolutely
necessary.
Here is the equivalent operation in generified Java:
Map<String,Object> map = getInfo();
Map<String,Object> sortedMap = new TreeMap<String,Object>(map);
for(Map.Entry<K, V> e : sortedMap.entrySet()) {
String key = e.getKey();
Object value = e.getValue();
...
}
Readable? I think not. It takes more than a quick glance to tell
what that code is doing.
Generics force the programmer to do at something the computer is
entirely capable of doing at run time (type verification). And if
we have learned anything at all about computers, it's to let them
do what they're good at!
When you add generics to the requirements for parentheses and semicolons
(other things the compiler could easily supply), the scales tip quickly
away from readability and elegance.
Of course, runtime type checking makes your code more "unsafe", so you
have to do more testing. But when you're not spending all of your time
adding syntactic "sugar" to make the compiler swallow your code,
you have a lot more time to make sure your code is doing the right thing.
Note:
That's right. I just called generics "syntactic sugar". In this case,
the sugar makes the code more palatable to the compiler, not the coder.
You have to pour it on, but you don't get to enjoy it.
Equally horrendous, to some, is the fact that runtime type checking slows
things down. But computers are getting faster, the number of programs that
needs to be written is exploding, and the number of programmers available
to write them is shrinking in comparison.
Given that state of affairs, it's hard to see the point of sacrificing
readability and elegance, all in the same breath.
There are productivity as well as quality benefits to catching errors at compile time rather than runtime. But I agree with you about the readability issue with Java, especially when you throw generics into the mix.
You should check out Scala. It is type safe but the compiler is smart enough to infer type and other semantic information without requiring the extra syntax. Readability was a primary goal in the design. And runs on the JVM with full interoperability with Java code, so you can leverage the huge Java ecosystem.
Being a fan of both Ruby and Groovy I have been very pleased with Scala.
One is that I think you are blaming static typing for something that's more a fault of Java than static typing. It is like concluding that because Perl code is obscure and Perl is a dynamically typed language, that therefore dynamic typing leads to obscure code.
My other comment is that where I find the benefit of type errors being found by the compiler really pays off is when refactoring mature programs. It isn't so much when I'm writing something for the first time, because I'll be focused on the new functionality then and making sure it works, but when I make changes to it later that affects many things and I may not be aware of all that I'm affecting. Yes tests are helpful there too, but in practice I find static type checking very useful at that time.
I agree with some of your points and disagree with others.
Yes, the generic syntax makes Java harder to read.
Yes, the Ruby closure syntax is great.
Where I think that Ruby falls flat:
Duck typing: evil and dangerous, avoid it [1]
Dynamic typing: great for quick prototypes, terrible for large software shared by large teams. Dynamically typed code is harder to maintain and most of the automatic refactorings are impossible to achieve [2]
At the end of the day, I still find myself extremely productive in Java because of the exceptional tooling that accompanies it, some of which simply cannot be done in dynamically typed languages.
I'm sorry, but I found this entire article to be grossly misleading and ill-informed. The only valid point you've made is that -- as you mention with semicolons, braces, and parentheses -- requiring the programmer to manually apply type arguments is usually a pointless exercise in pedantry. As other commenters have pointed out, both Scala and an upcoming version of Java relax this requirement with some pretty basic local type inference.
You argue that the introduction of generics added unnecessary syntax and bloat to Java code, but you never show the translation of Java 1.5 into Java 1.4. Namely, you neglect to mention the ubiquitous downcasts that must be performed in 1.4. Effectively, in your motivating loop code the type arguments on Map.Entry, <K, V> (sic), would mosey on down to typecasts on the accessor calls, like (String) e.getKey() and (Object) e.getValue(). Where is the extra bloat? Sure the colon might be harder to see, but the character count is roughly the same and you now have to make downcasts that could possibly throw exceptions.
The Ruby example looks cleaner first and foremost due to the closure. The sorted version of the loop is shorter in Ruby because of a convenient method in the collection libraries (surely there are almost-as-convenient methods in third party Java libraries like the PLT Utilities); admittedly there is an unnecessarily verbose type argument application when creating the TreeMap, but this would be moot with a sort method.
You also suggest that Java 1.4 was (is?) appropriate for day-to-day programming and not just for secure, type safe, thread safe software, while Java 1.5 is not -- due to generics. Maybe if 1.4 had closures and pattern matching and 1.5 gutted those, then I could see there being such a transition away from its utility in daily programming tasks.
It's interesting that the person who built Generic Java (which was incorporated, essentially unchanged, into the official Java language version 5) is the person who designed Scala: Martin Odersky.
1. All examples used in critics on generics compare collections processing between Java and Ruby (specially Maps, which get two type parameters). Ruby has collection processing built deeply into its syntax and APIs, just like all these 'functional-ish' languages. In many of these languages objects are basically, glorified maps. If you compare this specific feature, it's *obvious* that Java will lose every time.
2. In Java, most of this kind of code would be encapsulated/isolated (in utility classes), so that it won't mess with readability of the meaningful code. Well, unless it's crappy code, that you can write in any language.
3. Generics are OPTIONAL!!! If you think that in a particular block of code the type-safety is not worth the readability loss, don't declare the type parameters! You may then want to add a @SuppressWarnings("unchecked") annotation to the method/class, though. I actually do it a lot with Wicket code, because most of the time the type parameters on components (since 1.4) give me nothing but verbosity (the binding is done by reflection, implicitly, so there's no parameter for the type parameter to match with).
This kind of critics always ignore the power that tools, only viable due to static typing, provide. I'm doing a lot of Groovy code nowadays, and it's really frustrating not being able to refactor and navigate through code like I can do with Java. This is also the very reason I still just can't jump into Scala right now.
I've read somewhere, "It's just text". Well, it's not "just text", it's a model, and having, and knowing how to use, the tools allow me to manipulate this model effectively.
> It's interesting that the person who built Generic Java > (which was incorporated, essentially unchanged, into the > official Java language version 5) is the person who > designed Scala: Martin Odersky.
He couldn't do better with Java due to, among other things, the backwards compatibility constraint. And that frustration is what led him creating Scala :)
Eric, I don't think many are going to be able to reply to your post because it's fractally wrong. Pretty much everything in it is wrong including the premises, each and every argument and the conclusion. But to correctly reply to it would need a complete deconstruction taking hours, and I would guess most people don't have the time, drive and money to do that.
Instead, I will just say the following: Haskell is statically typed (with generic types of course) and is often terser than Ruby. What you demonstrate here is not that generic types or static types are bad, just that Java is a crummy language which has been completely unable to evolve gracefully.
> Generics force the programmer to do at something the computer is entirely capable of doing at run time (type verification).
No. That makes no sense. Generics let the compiler have a much deeper understanding of what happens and check types at compile time instead of doing them at runtime. They're a prime example of failing early, and in Java or C# avoid having to fill the program with type casts.
> Of course, runtime type checking makes your code more "unsafe", so you have to do more testing. But when you're not spending all of your time adding syntactic "sugar" to make the compiler swallow your code, you have a lot more time to make sure your code is doing the right thing.
So instead of spending some time specifying types and then let the compiler ensure you're not doing things that make no sense, you take that time and spend it writing tests which do the exact same verifications?
I understand your frustration with generics. Every time I have to type in type parameters while coding it seems to put the brakes on my chain of thought and create a small mental disturbance akin to a "... not that again". But claiming that "generics kill Java" is exaggerated (proven by the fact that 6 years after generics, Java is still the most widely used language).
First, as another user pointed out, you don't have to use generics. You can code on as if they never existed. Also as another user pointed out, generics basically only replace the type cast by a compile time check. Java is a statically typed language, there is no way around that.
I think what your post boils down to is statically vs. dynamically typed languages and that you think that the latter are inherently superior.
Of course dynamically typed languages are nice. I have worked with Python for a while and I practically never experienced the missing type checking as a problem. It was also a very liberating experience. After all, if I -the programmer- know that there are only Strings in that list, why do I have to cast them? Can't the compiler just trust me on this?
And the answer is no. An interpreter for dynamic languages can, but not a compiler. Distrusting the code is its job.
Dynamically and statically typed languages occupy different niches and they each have their specific point and purpose. If you're writing short, concise programs all by yourself, then by all means, go dynamic and forget about types. The responsibility to do the right thing lies with you alone.
But if you're working with 20 people on a big project you don't always know what other codes intended. Coding this without static type checking is asking for trouble. Also, static type checking makes it possible to use tools and methods, such as static analysis, that are impossible to apply when working with a dynamic language.
I would say that your "elegance=power/characters" formula is a little one-dimensional, because elegance is not the only important thing in a PL. That is the whole point. Other important factors are "scalability", "ability to work in team", "tool support", "safety"... in all of these a statically typed language will be superior. There is a reason why dynamic languages are mainly used for scripting, usually embedded into a statically compiled language.
> strings.sort().each( { s -> out.println( s ); } );
>
I think that Java needs to be careful not to cater to the dynamic community too much, especially as it is unnecessary, since many dynamic languages run under the Java VM. I think that Java is feeling the competition and is trying to hard to appeal, thereby introducing new syntactic sugar that is however "unpure". For example, I particularly dislike the automatic type boxing feature:
Integer i = 12; or int i = new Integer(12);
Yes, it's shorter . More "elegant" than Integer.valueOf(12) according to Erics formula, but it's also wrong and confusing to newcomers. 12 is a primitive type and Integer is an object, they are not equal. This can cause some subtle errors that are hard to notice. For example every time you have an overloaded method that accepts a primitive type and an object, such as in ArrayList: remove(Object o); remove(int index);
I had an Integer object "i" and was calling list.remove(i), expecting to remove the object at a specific index (thanks to autoboxing). Of course, the actual method being called was remove(Object) and nothing was being removed, because my Integer object wasn't in the list.
And this is the point. Autoboxing fools you into thinking that 12 and "new Integer(12)" are the same, whereas they are something entirely different. I hope Java won't introduce any more of these "fuzzy" constructs.
> I think that Java needs to be careful not to cater to the > dynamic community too much
Where does it do that so far? Since when is introducing first-class anonymous functions "cater[ing] to the dynamic community" at all, let alone too much?
> I think that Java is feeling the competition and > is trying to hard to appeal, thereby introducing new > syntactic sugar that is however "unpure". For example, I > particularly dislike the automatic type boxing feature:
That's because it's crummy, as usual in Java.
> Integer i = 12; or > int i = new Integer(12); > > Yes, it's shorter . More "elegant" than > Integer.valueOf(12) according to Erics formula, but it's > also wrong and confusing to newcomers. 12 is a primitive > type and Integer is an object, they are not equal.
And that is very specifically one of Java's worst misfeatures. `int` and `Integer` should be aliases. And should always have been.
> And this is the point. Autoboxing fools you into thinking > that 12 and "new Integer(12)" are the same
Which they should be.
> I hope Java won't > introduce any more of these "fuzzy" constructs.
And I hope Java deprecates and removes all the gunk in the language. As far as I am concerned there is no reason for having both int and Integer, everything should just be `int` and Java does its boxing when needed (method call on an int or whatnot)
Flat View: This topic has 62 replies
on 5 pages
[
12345
|
»
]