The Java type system is broken
wouter.coekaerts.be
wouter.coekaerts.be
This is the correct conclusion. The people getting outraged over this are the same that will hold long and boring monologues about how everybody does REST wrong.
Required a SOAP message, that contained no XML after the message tag, but instead the entire message body was to be a JSON, and responded with a JSON if something was wrong, and a fully formed SOAP message if things were Ok.
That and the guys that handle your money use the Http status code as their personal error messages. Except when the error is on their end, in which case they return 200 and a JSON with an error message.
There are plenty of ways to do REST wrong, so let's not lump that into the people that make blog posts complaining about X because X is broken, and working just fine...
To be fair, I'm not sure there's any other way.
> Java’s type system has taken a beating by the desire for backward compatibility and the interaction of its growing list of features. It needs some love to get it back in shape.
If Java had generics in the beginning, it would look a lot different and the type system would be more powerful and safe.
For what it's worth, Brian Goetz doesn't see type-erasure as a such a failure and defends the design choice[0].
Erasure is the safest way to implement generics for a long list of reasons. Reification comes with a lot of downsides, which is why most languages that support parametric polymorphism use erasure.
Aside from the video I posted from Brian Goetz most of what I've read is just comments about how much people like reification in C# and how annoying those edge cases in Java are (i.e. stories from practitioners not language designers.)
I'd love to read more.
1. Erasure keeps you honest and prevents you from second guessing the compiler by limiting the introspection you can perform on your code.
2. Reification puts up a very high barrier to interop. Erasure systems are much more welcoming to implementing multiple languages and multiple type systems on top of them. For example, the scala.net project was abandoned because it was impossible to represent Scala's type system on .net's reified platform.
3. Reification imposes overhead because of the multitude of extra runtime checks that need to be performed.
4. Most the benefits you get from reification can be emulated on erased systems (some are admittedly more hackish on an erased runtime, but they should be rare).
> Erasure keeps you honest and prevents you from second guessing the compiler by limiting the introspection you can perform on your code
But the title of the article under discussion is called "The Java type system is broken", after all :)
It would have been possible to implement reified generics while preserving backward compatibility, Neal Gafter had actually a proposal to do just that.
In the end, erasure won simply because it's the superior solution.
Erasure gives you a little less flexibility to express certain constructs than reification allows, but from a type safety standpoint, the two approaches are equivalent.
If you want widen the meaning of "type safe" a bit, I would argue that reification is "less" type safe in the sense that it allows you to perform additional runtime type checks / type casts, which basically means you are second guessing the compiler and invalidating all the type soundness that it has provided you by accepting to compile your code.
[0]: http://mattwarren.org/2018/03/02/How-generics-were-added-to-...
Take a look at C# which added it without many issues (because they were willing to break back compat of their bytecode). Go fits this model a whole lot better.
In Java's case, old bytecode using Map would correctly work with change to Map <K, V>. C# introduced a new set of collections I believe.
When there is a C# class that both has and doesn't have type information, they are actually completely different classes that just happen to share a base name.
This really made it painful when they first added generics. If you used generic containers but wanted to call into a library that predated it you hand to transform your data in/out of all the calls to the library. In Java land you didn't have to do anything other then a blind cast on the out side which doesn't even have a runtime penalty.
Long term the C# way was probably better but then they didn't have nearly as robust of a library ecosystem when they added generics.
To the 2nd - The concrete types are separate, but they fit into a common interface hierarchy, so, at a source code level, they're plenty interoperable. For example, the (non-generic) IEnumerable interface has a Cast<T>() extension method that will convert it to a (generic) IEnumerable<T>. IEnumerable<T>, for its part, simply inherits IEnumerable.
In general, I actually like using those conversion methods for downcasting, because it gives a clearer indication of when I'm in a danger zone - for example, converting a non-generic list to a generic list might fail if the non-generic list contains a mix of different types of object. Java's situation feels less predictable to me, as this article illustrates fairly well. A few keystrokes for the sake of safer code is a dandy tradeoff in my book.
That's not true. (Mass shared hoster kinda guy here) when we pushed a bunch of customers code compiled for .NET 1.0/1.1 over to hosting environments with only CLR 2 available, their code ran just fine. These were .NET 1.0/1.1 assemblies.
There are some edge cases where stuff could break, say when doing reflection or emitting your own IL, but that for most LOB web hosted apps was pretty rare.
I did in fact marvel how backwards compatible CLR 2 was when it first shipped.
They just decided to release 1.0 without generics, instead of waiting for it to be 100% ready.
As Don clearly describes on his blog.
In fact, I'm a big fan of how Microsoft does versioning, supporting previous versions, but releasing major versions with backwards incompatible changes. You see this in DirectX, and IMO, that's why we're talking about DirectX and Vulkan, rather than AZDO OpenGL these days. It really gives you an opportunity to clean up the cruft.
Metal uses Objective-C/Swift, with shaders in C++14. The Objective-C runtime can be used as FFI.
DirectX, uses COM with HLSL (a C++ subset). Likewise any COM or .NET (via RCW) aware language can talk to it.
One of the reasons OpenCL lost to CUDA was being stuck with C, while CUDA offered C, C++, Fortran and any additional language that could target PTX.
Which was what eventually made them come up with SPIR and later SPIR-V.
NVN and LibCGNM are also based on C++ and C++ inspired shader languages.
All modern OO, with nice SDKs that handle font, texture, materials, maths, GPGPU debugging.
Except for the fact that all CLR languages must adopt the same variance model, enforced by the runtime (or choose not to interoperate well).
I wouldn't say "without many issues." It took a major surgery in the CLR codebase to add generics. The difference between CLR 1.1 without generics and CLR 2.0 with generics was huge. No area of code was untouched.
I was thinking of more fundamental changes to Java idiom and the standard library had generics been part of the design (e.g., how arrays work).
And conceivably a good deal faster, too. My understanding is that the JVM is required to constantly perform runtime type checks.
I remember Java before generics: tons of casting from Object. The compiler is doing the same thing.
No. That's the point.
[0] Interfaces can contain other interfaces, but I think that is still different because you can't do
var a []subinterface = []superinterface{...}For dynamic or tag-free languages, type indicators could be used to parse-check scalar (base) values to see if they are interpretable as the intended type:
function foo(int a, date b) {...}
This would be equivalent to: function foo(a, b) {
if (! parsableAsInt(a)) throwTypeError(...);
if (! parsableAsDate(b)) throwTypeError(...);
}
And don't overload operators, such as how some languages use "+" to mean both arithmetic addition and string concatenation. Use a different symbol for concatenation like PHP does.Having used reflection in C# with both nullable types and non-, I'm curious what you mean by "scavenger hunt".
You have to reason about types in any language because passing the wrong type to a function can result in errors. You have to do all of that mental work by yourself in a dynamic language because there isn't a system to do it for you.
The purpose of types is to introduce errors in the first place - at compile time, if you messed up.
Wrong code causes errors regardless. Annotating with types just moves this event from runtime to compile time.
I also read, more generally, that adding reified generics to the JVM might make it hard or impossible to implement some dynamic languages on top of the JVM.
So yes, erasure has its drawbacks, but it's not all bad.
Maybe it's why the JVM has a larger ecosystem of new languages?
HN is less prone to toxicity, but this is reminiscent of Reddit and it drives away the people that want to help.
As a piece of advice, some type theory never harmed anyone ;-)
It's pretty dramatic so it gets met with preemptive defensiveness.
From the end of article: Everything is broken. Everything is fine.
In practice, there are ranges of brokenness. If you have to go hunting for the problem, and it takes a combination of 5 obscure features which nobody would ever use together, it's not that big a deal. If it's the sort of thing that you encounter in any non-trivial program, well....
... your language dies.
Part of the reason so many people in this thread can be cavalier about type soundness issues is precisely that all the languages they use, being the languages that did not die because they have a crappy type system, are pretty sound, within the model the language is operating under, so, yeah, of course it doesn't look like a problem to a normal programmer, because it was solved a long time ago. Doesn't mean it's a bad idea, or that people should just sneer contemptuously at the issues and snidely dismiss them as academic frippery, because then these people go on to write new languages that have these sorts of issues.
(Like the recent wave of "event based" programming environments that just threw away the benefits of structured programming, because they were so thoroughly immersed in a structured programming world they didn't even realize how thoroughly they'd internalized it, had no idea what the preconditions were for it, and not only had no idea they were throwing it away but snidely berated people who pointed it out. And then spent 5+ years rediscovering it all over again....)
To apply a formal method to your work requires to acknowledge that your intuition might me wrong, your past decisions incorrect, and your knowledge of the subject area deficient. Often you have to step back, rethink, and rework.
This is a normal mindset for a scientist, and a pretty common mindset for an experienced engineer.
But this view is not automatically acquired with an engineering diploma, with obvious coding skills, or even with a couple successful open-source projects. It has to be nurtured consciously—or rammed down your throat by unforgiving reality; the latter is pretty traumatic.
When one is a polyglot developer, we are like mercenaries, a bag full of tools, each with its caveats, and we care about the solution less how we got there juggling those tools.
Developer X tend to keep searching for external confirmation that they did the right choice. Any attack on tool X triggers defensive attacks, as if they would be a personal attack.
Of course I am exaggerating here, just extrapolating from those that I know on my circle.
https://raw.githubusercontent.com/namin/unsound/master/doc/u...
If I can serialize to String, and then deserialize a String to any type of class, I can effectively "cast" anything to anything.
In Haskell you can certainly achieve the same thing with various common extensions ( http://okmij.org/ftp/Haskell/impredicativity-bites.html ). Possibly not in vanilla Haskell '98, at the price of being not a very nice language to work in. (A total language like Idris should permit GADT-like functionality without allowing this kind of issue).
null can be considered bottom (if it were not for primitives)
Haskell can be made to be unsound using 'unsafeCoerce', but as you'll note, there is a giant unsafe in front of it.
It's still safe, though, as it diverges at run time. Much like the ClassCastException coming from the JVM prevents the unsoundness in the original article from being a safety error.
'unsafeCoerce' (and things that can implement it, like creating a polymorphic mutable cell with 'unsafePerformIO') are worse than unsound - they are also unsafe.
data EqualityProof a b where
Refl :: (a ~ b) => EqualityProof a b
-- compiler error because we don't check that the equality proof is actually Refl
coerce :: EqualityProof a b -> a -> b
coerce _ a = a
-- this works
coerce Refl a = a
-- here the Refl pattern match catches the undefined
-- this is like forcing the programmer to do a null check before the equality is in scope
-- and throwing a runtime exception when it's null
coerce undefinded 3 :: StringI make analogies to taxonomy because before I learned software a biology teacher told me that taxonomy is also broken, so when I learned type theory it felt like a similar kind of broken.
We think putting wings to a mammal is adding behavior, when in fact it is subtracting it. Adding wings on a mammal means you have a bat, or a flying squirrel.
You have narrowed the potential of this type, not enhanced it. Anything else a mammal might do that bats can't? You've taken all of that away. Mammals with fly() aren't the fastest land animals. They don't have the record for holding their breath, or hibernation time. They can't dive(), speak() or useTool(). Really they're kinda useless except for vermin control.
Java is not perfect, but this looks to me like a broken code indeed ".get(0).get(0).get(0)".
Could we rephrase the message of the article as "If you write really broken code in Java, you might be not guarded by the type system". And this is, well, uhm, well... acceptable?
I kinda feel like humanity has repeatedly decided the answer to this question is "yes".
"Being valuable" isn't binary. There are relative amounts of value, I think.
Saying things like "do better easily" makes sense in the context of fixing something that's broken.
But for anyone who just sees it just as a limitation this comes off as presumptuous. What do you want to change? How do you want to overcome the limitation? At what cost?
Several new languages (eg Typescript, Dart) have optional type systems that still bring significant value to the table.
Sure - all other things being equal, I'd much rather have a type system that is completely sound. Ceylon's type system is beautiful and I'm happy to see its union + intersection types being adopted by Typescript. But honestly, I'm not going to lose much sleep over these particular issues in Java. Bad code gets refactored.
A bigger complaint should be made that generic type erasure makes libraries hard (eg, the number of places you have to pass around Class<?> or TypeReference objects to let the runtime know what to do). But that was a clearly a compromise decision and everyone knows about it.
fyi "Dart’s type system is now sound" and "…types are mandatory".
I think it's too early to say that this approach has proven to be successful - indeed another reply says that Dart has switched to a stronger type system with required types, which I'd take as significant evidence that optional typing is not a good tradeoff. Certainly my experience of the checkers framework in Java was that optional types are the worst of both worlds.
> But honestly, I'm not going to lose much sleep over these particular issues in Java. Bad code gets refactored.
I'm worried (or rather, I would be worried if I hadn't seen the bug reports, which seem to be being taken seriously and fixed in the compiler). If they can happen in bad code, who's to say they can't happen in good code.
> A bigger complaint should be made that generic type erasure makes libraries hard (eg, the number of places you have to pass around Class<?> or TypeReference objects to let the runtime know what to do). But that was a clearly a compromise decision and everyone knows about it.
I'd argue that the only cases where you need that are when you're doing something you shouldn't (reflection). But I guess in a language without typeclasses there are problems that have no good solution.
It is not. The only purpose of a type system is to prevent broken code from compiling. When it fails to do so, it is broken.
[1]: https://www.typescriptlang.org/docs/handbook/type-compatibil...
A language like rust can prevent writing code like that, and thus prevent that sort of error, while a language like javascript is even more lax. Both have their tradeoffs wrt productivity and performance.
Either way, it is hard to reason about factors like that numerically when we've not qualified which kinds of programs we're expecting the type system to actually work for.
The very idea of the type system is that it makes "broken code" of a certain type a compile error.
This article gives a series of examples that break that promise.
f.get(0).get(0).get(0)
typically is the way to write want in languages that allow operator overloading is written as: f[0][0][0]
You will rarely see that with three constants, but f[x][y][z]
isn’t uncommon.If we take the view that these are bugs in the Java compiler, then of course a bug that's rarely triggered is less bad than a bug that's triggered all the time. But both are bugs all the same.
It would be nice of you to compare Java generics with equivalents instead of just criticizing it. It doesn't help anyone writing "this is broken" not offering any alternatives or suggesting ways to fix the problem.
Clearly stating and detailing problems is absolutely helpful. We can't solve problems if we don't understand them.
Actually identifying a problem is of very much help, even without a solution.
For one, it lets people know of the problem, so they can get to an eventual solution.
Second, it lets people know of the problem, and avoid those areas, even if there's no solution available.
Hmm. If I knew your coolant was leaking, but had no idea how to fix it, would you prefer I didn't tell you? Just want to keep driving that car around with that coolant leaking?
At the very least, this is educational and helps better understand Java for those of us who work with it.
As the tweet shows, you still get (some) type safety from the runtime checks.
Object[] objects = new Integer[] {1, 2, 3};
objects[0] = "hello";
Also compiles but fails at runtime.Pollution is the proliferation of superfluous objects creating some sort of undesirable mixture. For instance "namespace pollution".
An inconsistency between the run-time type of an object in the heap, and the type of the expression in the program which refers to that object, isn't a good fit for the word "pollution".
Quit trying to pollute the computing lexicon with nonsense.
Specifically you can do (forgive syntax it’s also been a long time since I wrote java)
X = new String[10] Ovject[] o = X
o[0] = 1; // this will auto box, and then fail at runtime
Note that this isn’t a “bug” as it predates any version of generics so collection types especially are much saber if you support this. That said it does result in a logical error in which you can’t assign something that looks like it should be fine.
.Net unfortunately picked up this design decision in order to support java on their vm.
Find something that bothers you, complain publicly, do absolutely nothing after that, repeat.