I don't understand how lines 4 and 15 don't get caught by the compiler-- a String cannot extend an Integer?
I'm not even sure how you would be able to write this in Scala without violating rule number 1 of Scala, which is "never write the word null in your code".
I wish Java had done more backwards comparability breakage. I wonder if we'd see the same issues if Java eliminated null in exchange for a Option/Maybe style monad and if generic types were saved instead of erased at compile time.
Java 9 is not fully compatible with Java 8. So we will soon see first hand if doing more backward compat breakage, for good they told, will be accepted or not. I predict there will be an internet outrage when Java 9 will be out.
If I understand correctly, a constraint "? extends T" is checked at runtime, although in this case it could be checked at compile time (and, yes, it's easy to tell at compile time that String doesn't extend Number). Also the runtime check is performed on the actual value passed, which in this case is null, and therefore the check "can this cast be performed?" succeeds, but the actual cast fails.
So, right now, nobody can show an exploit. But security researchers often manage to find exploits given this much to start with.
Type constraint checking is done locally, and primarily relies on inductive reasoning to get soundness.
Whenever you instantiate a generic instance, you need to provide a concrete type--be it a known class name, or a named type variable. This applies both to instantiation via new and to the inference of generic method arguments (you can't say Unsound.<?, ?>coerce(), for example). Since named type variables come from outer generic instances that need to be instantiated themselves, this provides the inductive proof.
The first fly in the ointment is wildcard types. Wildcards are only legal as the type of a variable. They also don't work like you expect--each variable gets a different "capture" of wildcard types. Effectively, you use the capture conversion process to build up a complete set of constraints that a wildcard type must satisfy, and then you have to prove that it can be satisfied.
The actual problem comes when we are asked to provide proof that there exists a type that satisfies the constraints. Normally, we would derive the value from something that forces an instantiation of a type. Instead, the example passes in literally a null proof: the null value, or I have no proof that such a type exists. Even then, it's not so bad, except we borrowed the "proof" provided by the null instantiation in a different context to prove that we could go from an Integer to a String.
Put another way, the flow of the program works like this. The Constrain class is used to carry around in the compiler the notion the subclass constraint, and to provide the thing where wildcards get used. The upcast method is used to borrow the proof-of-existence from Constrain to actually do the cast. (The compiler doesn't complain here because it's all in terms of type variables--if there's a problem, it will be caught at the calling site).
Since the coerce method declares no bounds on the type variables, there's no reason for line 15 to miscompile--all of the constraints between T and U have to work under the assumption that there is no axioms about their relationship. Line 10 compiles cleanly because all we're doing is stating that, to use this variable (store anything in it), there needs to exist a type between T and U. The use of null means we defer it to later. Line 11 has no problems whatsoever. In Line 12, we use the fact that ∃x: T ≤ x ≤ U to capture that x and so cast from type T to x to U without any problems.
Fixing this is not easy. You somehow have to modify the type system to reflect the fact that a null value doesn't prove the existential qualifier, and it's not clear to me that there's a clean way to retrofit that into the way type checking is currently done in Java.
Interesting that Ross Tate (the author) is an adivisor to the Ceylon and Kotlin teams. When this was brought up before on HN [1], I found that Kotlin handled these type issues. I can't be 100% confident of that though as I did struggle to fully understand the issue.
I've been developing in Scala for years, but lately I've really grown sour at the TypeSafe/Lightbend libraries and tools (poorly documented, half-implemented libraries with major breakage on each release with little to no migration documentation).
People like Paul Philips (yes yes, I know he's ranty and rubs people wrong and is kinda intense, but he makes good points) show that you can build a sane language on top of the JVM, but Scala makes a lot of the same concessions and refuses to fix a lot of implementation bugs in favor of backwards comparability and enterprise support.
I wrote Kotlin off originally as a late into the game pull by Jetbrains, but now I really want to take another look at it and see what type of library and JVM support is has.
I don't have many good things to say about Scala but "concessions" to backwards compatibility and enterprise support are almost always the right choice. These things are much more important to more people than "doing the right thing".
"So had history been a little different, either with Sun deciding to throw out backwards compatibility after adding generics, or with Sun adding generics into the JVM, or with Java first being released with generics, then this exception would not be thrown"
It returns a ClassCastException because underneath it all, in the Java bytecode the Java compiler stores generics as casts. Therefore it will complain because it can't cast. If they had baked generics in at the beginning of Java then it wouldn't have compiled.
It would be the equivalent of writing String one = 1;
There are a lot of young developers that have never seen the <1.4 Java code and understand what's happening underneath with type erasures until this bites them.
I hope professors today teach the 1.4 vs 1.5+ equivalents and explain what happens in the underlying type system.
A lot of these problems with the JVM are exacerbated in the Scala world. With such a heavy dependence on type matching in Scala, things like
a match { case List[MyCustomType] =>{}; case List[SomeOtherType] => ...
become impossible since the underlying types are removed at compile time.
And you may not discover the type erasure problems until runtime. I once found an "impossible" bug in production that came of someone accidentally swapping two values passed to a database function. We had separate DocumentId and UserId types for the values -- how could the bad line of code even compile?! Oh, type erasure; they were both Longs "underneath". (This weakness also mandates more basic testing for Scala code than you might expect for a language whose flagship company was called Typesafe.)
Yes, the Scala docs do explain that type aliases are not really distinct types, so this sort of bug could have been expected in advance. I came to Scala with no prior Java experience so I got to discover the ways the JVM leaks through on the job. I thought that I could reuse my previous knowledge about types in program design in obvious ways and just get the obviously (to me, anyway) desired behavior. Mea culpa!
That has nothing to do with type erasure. Type aliases are not distinct from the underlying type, even at compile time. You needed value classes, not aliases.
example:
object Example {
type IdA = Long
type IdB = Long
def usesA(i: IdA) = "it's an A"
case class IdC(i: Long) extends AnyVal
case class IdD(i: Long) extends AnyVal
def usesC(i: IdC) = "it's a C"
val a: IdA = 1L
val b: IdB = 2L
val c = IdC(3L)
val d = IdD(4L)
usesA(a)
usesA(b)
usesC(c)
// these won't compile
// usesC(d)
// usesC(a)
}
How embarrassing! Thanks for correcting my mistake. I inherited someone else's code base and I thought that the type aliases were there to enforce correctness.
> It returns a ClassCastException because underneath it all, in the Java bytecode the Java compiler stores generics as casts.
Right - the article already says that doesn't it?
> If they had baked generics in at the beginning of Java then it wouldn't have compiled
Why wouldn't it have compiled if the JVM had generics? It's an error in the design of the type system of the language isn't it? The type system is the same if we have reified generics or erased generics isn't it? And that's where the failure is - not at the JVM level.
I agree with Chris,
it would be a lot worse if generics were reified in Java because instead of having some spurious ClassCastExceptions, you will get memory corruptions instead.
As a C++ programmer but not a Java programmer, why is it reasonable for Java to consider nonsensical types (where constraints are violated) to be valid but just not instantiatable? Wouldn't it be better to reject the type entirely? Then you couldn't have a null object of the type since the type itself is ill-formed.
That's a rather different issue. Abominable types are annoying and mostly worthless, but there's nothing inherently wrong with code containing a type variable that refers to the type. Java's screwy generic instantiations are types that violate constraints by merely being named.
I'm not saying that C++'s type system is great. I'm just saying that Java's problem mystifies me.
The real implication here is that Java behaves funny because of backwards compatibility. We're quite a distance away from < jdk1.5 days... What's the weather like to update the Java memory model or type system to take care of these obscure cases? We managed to add a new opcode to the JVM (invokeDynamic) and the world survived!
It's the opposite actually. Here the erasure is a good thing otherwise the bug described in the blog will have break the VM. The type system is broken because it supposes you have a type that exists and in the code you send null instead. If generics were in the VM, this kind of bug would have break the VM but 'thanks' to the way generics are handled in order to be backward compatible, a cast is inserted so this bug in the type system do not break the VM.
It's much easier to write robust code in Java - with it's Optional type, and checked exceptions - than in C#.
You probably would never write this, and your colleagues would never write this. That reasoning is good to apply in many situations. I in fact do this all the time; it’s a huge part of my research agenda. But you have to be careful where you apply it. Soundness is a security guarantee, not a usability concern. What matters is if someone can write this, because then that someone can get around the security measures that people have placed their confidence in and do whatever they would like to do. In the case of soundness, it’s the malicious programmer you should be worried, not just you and your friends.
I really hate this attitude. Programming languages should be designed to be safe and strict by default, but with easy outs. I utterly reject the notion that we should design languages first and foremost to protect us from people can't program that well or aren't careful when they write code.
Do we always have to appeal to the lowest common denominator? Empower the developers that care rather than trying to turn this into an unskilled profession.
C# has Nullable<T> (aka T?)[0]. I'm not a Java expert, but they appear to be the same (represent a value or a null value, allow you to inspect for the presence of a value or null).
ex:
int? x = null;
int y = x ?? 0;
int z = x.HasValue ? x.Value : 0;
if(x.HasValue && x.Value == 42){}
C# definitely doesn't have checked exceptions.
edit:
For reference types, as compared to value types, c# has the null coalescing operator
List<string> y = someList() ?? new List<string>;
and the null conditional operator
int? length = customers?.Length; // null if customers is null
which are defintely not Optional, but are what I use.
I'm unfamiliar with Java's Optional type, but the .NET type Nullable can only be parameterized over value types. It is there to add the concept of null to value types, not allow you to more precisely characterize the nullability of reference types.
I didn't imply it was desired or wasn't possible...? Another reply to my comment clarified that Optional also only applies to value types, which makes it analogous to Nullable. There's no reason to port it (as opposed to any other implementation of an Optional type) over.
What is really necessary is an equivalent for reference types.
Yes, I was misreading the documentation. The Optional class itself is to be considered a value type, and reference equality on Optional objects shouldn't be done.
And in Java they're in the the standard library. Which is going to go down better - simply using something official that every java library program will already have, or trying to explain to your colleagues why all your methods are returning something defined in this obscure library you added to all your builds?
this is a different question than the premise you initially outlined "It's much easier to write robust code in Java - with it's Optional type... than in C#."
You seem to be making several arguments at once. My only point is that C# has no problem implementing java's Optional, which some other folks seemed confused about, and there are already libraries that do so. If there is a social/cultural issue too, I was not commenting on that, or commenting on the usefulness of checked exceptions.
Look how many people here are advocating for the nullable hack because they don't understand what an option is. With java the discussion could be side stepped entirely, since they added proper support and put it in the standard lib.
Only since Java 8.
The coalesce operator is in c# since the version 3.5 or 4 I think and c# 6 introduced the null conditional operator that, as I said before, is objectively nicer than the verbose Java optional.
Before c# 6 I used this simple extension method:
Or something similar (writing c# code on the iPad before sleeping is not exactly simple), inspired by some blog post ages ago.
Just adding these three line of codes in a static class in the solution will enable you to write something like this:
var id = order.IfNotNull(o => o.Customer).IfNotNull(c => c.Id) ?? "INVALID"
Still not as good as the null check operator but comparable with the Java optional (arguably better from my point of view)
Except that nullable types aren't the same thing as option types, so it's not doing the same thing. Nullables are much weaker than options. You can't do this for instance:
string? id = "Name";
While you can do stuff like this:
int x = 0;
int? y = x;
Again, F# has nullable types and options, and the fact that no one in F# uses nullable types should tell you something.
The null check operator is much more concise than my pre-c# 6 solution.
Moreover you can apply both my solution and the null check operator to any type, while for Java you have to specify that a field is optional (increasing the verbosity and moving the responsibility to who write the class) and you can't do it for types that you don't own.
So for these types the only way is to create a wrapper type just to add the optional fields, and this brings the amount of boilerplate code through the roof.
So, just to sum it up, contrary to what you say the c# solution works out of the box, is much shorter than the Java alternative and works for every type.
Honestly I can't really see why Java solution is superior for you.
edit: this is wrong, I was misreading the documentation, which states that the Optional class is a value type, not the contents of the Optional class.
According the documentation, Optional is only valid with value types[0]:
"This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of Optional may have unpredictable results and should be avoided."
Again, I have no experience, so if this is wrong, then additional corrective references would be awesome.
checked exceptions? They lead to very un-robust code like try{...}catch(Exception e){//ignore or log, continue along like everything is grand!} or (much better) every method declaring "throws Exception"
Sure, it may lead to that kind of code if you have mediocre programmers who don't care about robustness - absolutely. But in C# it's much harder to keep track of all the exceptions that might be thrown.
Now, if a programming language has error aware return types - using algebraic data types or multiple dispatch - that's a better solution than both. But in the absence of that, give me checked exceptions over unchecked ones any day.
Thanks god c# doesn't have checked exceptions, they are just an enormous pain, especially when heavily using actions and functions, and most of the time they are completely ignored, rendering them completely useless.
The option type in Java is extremely verbose and doesn't guarantee that you won't have null pointers.
In c# you can use the null conditional operator and the coalesce operator that at least are much less verbose and more elegant.
The best solution is obviously to use a functional language like f# where you can't have null pointers.
> The best solution is obviously to use a functional language like f# where you can't have null pointers.
You absolutely can:
Microsoft (R) F# Interactive version 14.0.23413.0
Copyright (c) Microsoft Corporation. All Rights Reserved.
For help type #help;;
> let s:string = null ;;
val s : string = null
> I really hate this attitude. Programming languages should be designed to be safe and strict by default, but with easy outs. I utterly reject the notion that we should design languages first and foremost to protect us from people can't program that well or aren't careful when they write code.
I don't think the author has a beef with easy outs, but with accidental outs. Specifically with regards to the JVM, too, there are some practical security ramifications, with regards to running plugins.
I would not call this "industry perspective". Java type system is good enough for "industrial use".
Most problems described in article are not for backward compatibility. But because java at runtime (JVM) is dynamically typed (Java compiler is statically typed). You can do much nastier tricks with reflection.
First, it calls unsound the fact that an exception gets thrown in the shown code. There is no data security issue, no possible JVM escape, no type system failure. You can get the same cast exception with straight-forward code. The claim of unsoundness is merely based on the fact that the problem is obfuscated. Using unsound here is misleading.
Second, the whole point relies on implicit type relations that are not checked at compile-time when nullptr is involved. Certainly, it's a weakness in the specification that can cause suprising run-time exceptions, but it's not a hole in the type system.
Third, it makes claim that the JVM was saved by its early choice of backward compatibility. This is a bad habit that I've seen elsewhere: making claim about unspecified, unknown alternate histories. In this case, had generic not erased type, then assignment from nullptr would not work. Surely, the nullptr would have acquired typed alternative (nullptr<T>: nullptr<String>, nullptr<Integer> ...) which would have letthe compiler type-check properly and catch the problem at compile-time.
It would be a security problem if you could cast any value to an int array without getting an exception (and without the resulting byte code containing any sort of check). And it sounds like there is no guarantee that the exception will be thrown? (Or maybe I misread) So that does seem very serious to me.
Your first point runs counter to my understanding of what the author said. Specifically:
"Here’s our “true” unsoundness example that has been making the rounds most recently. It assigns an Integer instance to a String variable. According to the Java specification, this program is well-typed, and according to the Java specification it should execute without exception (unlike the aforementioned backdoors) and actually assign the Integer instance to the String variable, violating the intended behavior of well-typed programs. Thus it illustrates that Java’s type system is unsound."
That is, the author's unsoundness claim is independent of the class cast exception.
On your second point, you may be in violent agreement with the author. What is the difference between "a weakness in the specification" and being "unsound"? The author's definition is that this is an unintentional hole in the type system, unlike the intentional ones he outlined.
This seems to be a set of examples that don't work, rather than a set of examples that prove a point. For example:
interface IFoo<T> { Number foo(T t); }
class Foo<T extends Number> implements IFoo<T> {
public Number foo(T t) { return t; }
}
Number bar(IFoo<String> foos) { return foos.foo("Nan"); }
This implies that something bad is going on. But the class in the middle is a red herring. It is not used in this example, nor can it be used as an argument to the `bar` method. Try instantiating a Foo<String> and it wont work. Try instantiating a Foo<Integer> and then passing that to bar(), which requires an IFoo<String>. Both are compile time errors.
By all means claim, correctly, that support of unchecked code and raw types means that the type system is not "sound", but the incorrect examples undermine the claim that this is a problem. (I don't personally think that its a problem, and the fact that someone claiming otherwise can only provide examples that fail to prove that its a problem isn't terribly convincing. But then I grew up with 6502 ASM, which had a fairly limited type system).
This example is not to show that Java is unsound. It's to teach you the difference between types that are invalid and types that would be unsafe but are allowed because you can never instantiate one.
Like you note:
> Try instantiating a Foo<Integer> and then passing that to bar(), which requires an IFoo<String>.
That's what Ross is trying to get you to see. He's saying that Java does not have to make the declaration of IFoo<T> an error because it can rely on the fact that your later instantiation of it with <String> will be an error instead.
As long as the bad code gets flagged as an error somehow before it gets used, you're safe.
The actual unsoundness bug is the "class Unsound" example in Josh Bloch's tweet. What it shows was that you can't safely rely on "we'll flag an error when you create an instance of this type, so we don't need to flag an error in its declaration." Because you can assign null to be an instance of the type and then you sidestep the later error.
Does not implement an unconstrained T. It implements "T extends Number". When the T appears in an implements, its not declaring a new type. You can't do, for example
class Foo implements IFoo<T> {}
java: cannot find symbol T
The mistake is believing that the T's, in the example above, are in any way related. It would be more appropriate to write:
interface Foo<TUnconstrained> {}
class Foo<TExtendsNumber extends Number> implements IFoo<TExtendsNumber> {...}
And then it becomes clear that the foos in bar will never be a Foo, because Foo does not implement IFoo<String> and never will. Yes you can nuke it unchecked code. No, I don't have a problem with that. But the example given is not a useful example.
And this example is in response to the previous one where he asks "Is this code safe? .. your gut instinct would say this is unsafe". No. My gut instinct is that it would not compile. And indeed:
java: type argument java.lang.String is not within bounds of type-variable T
> And then it becomes clear that the foos in bar will never be a Foo, because Foo does not implement IFoo<String> and never will.
The statement "Foo does not implement IFoo<String>" is not meaningful. "Foo" is not a type, it's a type constructor. To refer to a type, you need to say Foo of what. And it is the case that Foo<String> implements IFoo<String>.
Consider:
interface IFoo<TUnconstrained> {
Number foo(TUnconstrained t);
}
void useFoo(IFoo<String> foo) {
Number n = foo.foo("not a number");
}
This code compiles fine. Add this:
class Foo<TExtendsNumber extends Number> implements IFoo<TExtendsNumber> {
public Number foo(TExtendsNumber t) { return t; }
}
It still compiles fine. This is exactly what the author is saying. The reason it is fine for the compiler to permit the above code is because it is relying on the fact that you will not be able to instantiate Foo<String>.
This is one half of the puzzle of the actual unsoundness example. The other half is that you can then sidestep that restriction using a wildcard to define a type that hypothetically meets the required bound even though no such real type exists.
That would still be safe, since you've never be able to initialize that variable since no value of that type is possible. Except that Java lets you initialize it with null.
I tried to rewrite the bug into a simpler example to wrap my mind around it:
public interface Helper<T extends Number> {
}
public static <T extends Number> Number nice(Helper<T> helper, T value) { // (1)
return value;
}
public static <SUB_T> Number naughty(Helper<? super SUB_T> helper, SUB_T value) { // (2)
return nice(helper, value); // (3)
}
public static Number evil(String value) {
return naughty(null, value); // (4)
}
From what I understand, the following is happening:
1: Nothing unusual here. Just defining a type parameter with a constraint and referencing another type which happens to have the same constraint.
2: I think this is the actual bug (?) - SUB_T is permitted as the subtype of a constrained type, yet itself is not subject to the constraint. This compiles.
3: The compiler derives T from pattern matching (this is the sole reason why the helper is needed)
T is inferred as the set of hypothetical types that are both subtypes of Number and supertypes of SUB_T.
4: Because SUB_T is unconstrained, we can happily instantiate SUB_T such that the set of hypothetical types is empty (e.g. set it to String).
This does mean there is no way we could create a helper object as no type would satisfy the constraints - but we don't actually need a helper object, so we can simply pass null.
In short, this bug lets the compiler infer a type hierarchy of Number -> T -> SUB_T without ensuring that Number -> SUB_T holds. Usually, that wouldn't get you far as there is no type that could substitute for T - except that you don't need a type because the compiler will accept null even for "impossible" types.
It's interesting to note that the class cast exception occurs in (3), not in (1). From what I know, that is because Java's erasure takes constraints into account - the arguments of nice() become (Helper, Number) after erasure, not (Helper, Object).
So... you can assign null to a variable whose type is illegal to instantiate (but not to define), and code which has that variable in scope which will compile with the assumption that the type can be instantiated.
>Is it safe for Foo<String> to be a valid type?
Wrong question. Is it useful for Foo<String> to be a valid type? The answer is probably no. So why allow it? The below example does not come close to explaining why this should be a language feature.
The industry perspective is that nobody cares (and rightly so).
I was at the conference where this was presented. The paper is terrifically written, and it's good that someone is investigating this.
That being said, it's just not that big of a deal. In practice, this will never be a problem for anyone writing Java.
The Java wildcard system is insane and no-one is trying to build type systems based on these ideas, not even Scala and the new batch of JVM languages (Kotlin, Ceylon). They have their own problems, but this isn't it.
Agreed no one in practice will likely ever see this. Ability to compile vs warned about it are two different things. Those that know Java would quickly be aware that a) type erasure is a thing, it allows stuff like this b) even if they wrote some of this their IDE or compiler would generate a ton of warnings. Generally when you have a compiler - you learn to look at the output of it.
How many ClassCastExceptions or ArrayStoreExceptions are people seeing?
Meanwhile, javascript is winning.
From a practical perspective, static type systems have proven useful for tooling: to help us with code completion and refactoring. Beyond that, they appear mainly to hurt developer productivity.
72 comments
[ 4.6 ms ] story [ 83.6 ms ] threadI'm not even sure how you would be able to write this in Scala without violating rule number 1 of Scala, which is "never write the word null in your code".
Look for the "Thanks, NULL!" section near the bottom.
So, right now, nobody can show an exploit. But security researchers often manage to find exploits given this much to start with.
Whenever you instantiate a generic instance, you need to provide a concrete type--be it a known class name, or a named type variable. This applies both to instantiation via new and to the inference of generic method arguments (you can't say Unsound.<?, ?>coerce(), for example). Since named type variables come from outer generic instances that need to be instantiated themselves, this provides the inductive proof.
The first fly in the ointment is wildcard types. Wildcards are only legal as the type of a variable. They also don't work like you expect--each variable gets a different "capture" of wildcard types. Effectively, you use the capture conversion process to build up a complete set of constraints that a wildcard type must satisfy, and then you have to prove that it can be satisfied.
The actual problem comes when we are asked to provide proof that there exists a type that satisfies the constraints. Normally, we would derive the value from something that forces an instantiation of a type. Instead, the example passes in literally a null proof: the null value, or I have no proof that such a type exists. Even then, it's not so bad, except we borrowed the "proof" provided by the null instantiation in a different context to prove that we could go from an Integer to a String.
Put another way, the flow of the program works like this. The Constrain class is used to carry around in the compiler the notion the subclass constraint, and to provide the thing where wildcards get used. The upcast method is used to borrow the proof-of-existence from Constrain to actually do the cast. (The compiler doesn't complain here because it's all in terms of type variables--if there's a problem, it will be caught at the calling site).
Since the coerce method declares no bounds on the type variables, there's no reason for line 15 to miscompile--all of the constraints between T and U have to work under the assumption that there is no axioms about their relationship. Line 10 compiles cleanly because all we're doing is stating that, to use this variable (store anything in it), there needs to exist a type between T and U. The use of null means we defer it to later. Line 11 has no problems whatsoever. In Line 12, we use the fact that ∃x: T ≤ x ≤ U to capture that x and so cast from type T to x to U without any problems.
Fixing this is not easy. You somehow have to modify the type system to reflect the fact that a null value doesn't prove the existential qualifier, and it's not clear to me that there's a clean way to retrofit that into the way type checking is currently done in Java.
1. https://news.ycombinator.com/item?id=13050491
People like Paul Philips (yes yes, I know he's ranty and rubs people wrong and is kinda intense, but he makes good points) show that you can build a sane language on top of the JVM, but Scala makes a lot of the same concessions and refuses to fix a lot of implementation bugs in favor of backwards comparability and enterprise support.
I wrote Kotlin off originally as a late into the game pull by Jetbrains, but now I really want to take another look at it and see what type of library and JVM support is has.
Lately bigger players have started to take notice:
https://spring.io/blog/2017/01/04/introducing-kotlin-support...
https://blog.gradle.org/kotlin-meets-gradle
I'm still not sure where it will end up, but things look pretty good right now.
But wrt Type Erasure, Kotlin doesn't completely get rid of it; but it does offer some ability to retain Types in some generic situations.
It returns a ClassCastException because underneath it all, in the Java bytecode the Java compiler stores generics as casts. Therefore it will complain because it can't cast. If they had baked generics in at the beginning of Java then it wouldn't have compiled.
It would be the equivalent of writing String one = 1;
I hope professors today teach the 1.4 vs 1.5+ equivalents and explain what happens in the underlying type system.
A lot of these problems with the JVM are exacerbated in the Scala world. With such a heavy dependence on type matching in Scala, things like
become impossible since the underlying types are removed at compile time.Yes, the Scala docs do explain that type aliases are not really distinct types, so this sort of bug could have been expected in advance. I came to Scala with no prior Java experience so I got to discover the ways the JVM leaks through on the job. I thought that I could reuse my previous knowledge about types in program design in obvious ways and just get the obviously (to me, anyway) desired behavior. Mea culpa!
example:
Right - the article already says that doesn't it?
> If they had baked generics in at the beginning of Java then it wouldn't have compiled
Why wouldn't it have compiled if the JVM had generics? It's an error in the design of the type system of the language isn't it? The type system is the same if we have reified generics or erased generics isn't it? And that's where the failure is - not at the JVM level.
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p017...
[2] https://news.ycombinator.com/item?id=10935171
I'm not saying that C++'s type system is great. I'm just saying that Java's problem mystifies me.
There are other examples (e.g. see for Java 8 http://www.oracle.com/technetwork/java/javase/8-compatibilit...) where they've broken actual source backwards compatibility.
You probably would never write this, and your colleagues would never write this. That reasoning is good to apply in many situations. I in fact do this all the time; it’s a huge part of my research agenda. But you have to be careful where you apply it. Soundness is a security guarantee, not a usability concern. What matters is if someone can write this, because then that someone can get around the security measures that people have placed their confidence in and do whatever they would like to do. In the case of soundness, it’s the malicious programmer you should be worried, not just you and your friends.
I really hate this attitude. Programming languages should be designed to be safe and strict by default, but with easy outs. I utterly reject the notion that we should design languages first and foremost to protect us from people can't program that well or aren't careful when they write code.
Do we always have to appeal to the lowest common denominator? Empower the developers that care rather than trying to turn this into an unskilled profession.
C# has Nullable<T> (aka T?)[0]. I'm not a Java expert, but they appear to be the same (represent a value or a null value, allow you to inspect for the presence of a value or null).
ex:
C# definitely doesn't have checked exceptions.edit:
For reference types, as compared to value types, c# has the null coalescing operator
and the null conditional operator which are defintely not Optional, but are what I use.[0] - https://msdn.microsoft.com/en-us/library/1t3y8s4s.aspx
Checked Exceptions was a design choice, http://www.artima.com/intv/handcuffs.html
What is really necessary is an equivalent for reference types.
struct Maybe<T> where T : class {
no - Java's Optional applies to reference types.
Your quote also omitted checked exceptions, which is another reason why it's easier than me to write robust code in Java than in C#.
I think that was the point he was making, that it's always been possible, and now is simple.
According the documentation, Optional is only valid with value types[0]:
"This is a value-based class; use of identity-sensitive operations (including reference equality (==), identity hash code, or synchronization) on instances of Optional may have unpredictable results and should be avoided."
Again, I have no experience, so if this is wrong, then additional corrective references would be awesome.
[0] - https://docs.oracle.com/javase/8/docs/api/java/util/Optional...
http://fsharpforfunandprofit.com/posts/the-option-type/
Scroll down to "Option vs. Null vs. Nullable" for an explanation of the differences.
Now, if a programming language has error aware return types - using algebraic data types or multiple dispatch - that's a better solution than both. But in the absence of that, give me checked exceptions over unchecked ones any day.
You absolutely can:
I don't think the author has a beef with easy outs, but with accidental outs. Specifically with regards to the JVM, too, there are some practical security ramifications, with regards to running plugins.
Most problems described in article are not for backward compatibility. But because java at runtime (JVM) is dynamically typed (Java compiler is statically typed). You can do much nastier tricks with reflection.
Scala is not much better, I wrote some examples: http://www.mapdb.org/blog/scala_has_weakly_typed_syntax/
First, it calls unsound the fact that an exception gets thrown in the shown code. There is no data security issue, no possible JVM escape, no type system failure. You can get the same cast exception with straight-forward code. The claim of unsoundness is merely based on the fact that the problem is obfuscated. Using unsound here is misleading.
Second, the whole point relies on implicit type relations that are not checked at compile-time when nullptr is involved. Certainly, it's a weakness in the specification that can cause suprising run-time exceptions, but it's not a hole in the type system.
Third, it makes claim that the JVM was saved by its early choice of backward compatibility. This is a bad habit that I've seen elsewhere: making claim about unspecified, unknown alternate histories. In this case, had generic not erased type, then assignment from nullptr would not work. Surely, the nullptr would have acquired typed alternative (nullptr<T>: nullptr<String>, nullptr<Integer> ...) which would have letthe compiler type-check properly and catch the problem at compile-time.
"Here’s our “true” unsoundness example that has been making the rounds most recently. It assigns an Integer instance to a String variable. According to the Java specification, this program is well-typed, and according to the Java specification it should execute without exception (unlike the aforementioned backdoors) and actually assign the Integer instance to the String variable, violating the intended behavior of well-typed programs. Thus it illustrates that Java’s type system is unsound."
That is, the author's unsoundness claim is independent of the class cast exception.
On your second point, you may be in violent agreement with the author. What is the difference between "a weakness in the specification" and being "unsound"? The author's definition is that this is an unintentional hole in the type system, unlike the intentional ones he outlined.
By all means claim, correctly, that support of unchecked code and raw types means that the type system is not "sound", but the incorrect examples undermine the claim that this is a problem. (I don't personally think that its a problem, and the fact that someone claiming otherwise can only provide examples that fail to prove that its a problem isn't terribly convincing. But then I grew up with 6502 ASM, which had a fairly limited type system).
Like you note:
> Try instantiating a Foo<Integer> and then passing that to bar(), which requires an IFoo<String>.
That's what Ross is trying to get you to see. He's saying that Java does not have to make the declaration of IFoo<T> an error because it can rely on the fact that your later instantiation of it with <String> will be an error instead.
As long as the bad code gets flagged as an error somehow before it gets used, you're safe.
The actual unsoundness bug is the "class Unsound" example in Josh Bloch's tweet. What it shows was that you can't safely rely on "we'll flag an error when you create an instance of this type, so we don't need to flag an error in its declaration." Because you can assign null to be an instance of the type and then you sidestep the later error.
class Foo<T extends Number> implements IFoo<T> {}
Does not implement an unconstrained T. It implements "T extends Number". When the T appears in an implements, its not declaring a new type. You can't do, for example
The mistake is believing that the T's, in the example above, are in any way related. It would be more appropriate to write: And then it becomes clear that the foos in bar will never be a Foo, because Foo does not implement IFoo<String> and never will. Yes you can nuke it unchecked code. No, I don't have a problem with that. But the example given is not a useful example.And this example is in response to the previous one where he asks "Is this code safe? .. your gut instinct would say this is unsafe". No. My gut instinct is that it would not compile. And indeed:
I don't know what you're disagreeing with.
> And then it becomes clear that the foos in bar will never be a Foo, because Foo does not implement IFoo<String> and never will.
The statement "Foo does not implement IFoo<String>" is not meaningful. "Foo" is not a type, it's a type constructor. To refer to a type, you need to say Foo of what. And it is the case that Foo<String> implements IFoo<String>.
Consider:
This code compiles fine. Add this: It still compiles fine. This is exactly what the author is saying. The reason it is fine for the compiler to permit the above code is because it is relying on the fact that you will not be able to instantiate Foo<String>.This is one half of the puzzle of the actual unsoundness example. The other half is that you can then sidestep that restriction using a wildcard to define a type that hypothetically meets the required bound even though no such real type exists.
That would still be safe, since you've never be able to initialize that variable since no value of that type is possible. Except that Java lets you initialize it with null.
1: Nothing unusual here. Just defining a type parameter with a constraint and referencing another type which happens to have the same constraint.
2: I think this is the actual bug (?) - SUB_T is permitted as the subtype of a constrained type, yet itself is not subject to the constraint. This compiles.
3: The compiler derives T from pattern matching (this is the sole reason why the helper is needed)
T is inferred as the set of hypothetical types that are both subtypes of Number and supertypes of SUB_T.
4: Because SUB_T is unconstrained, we can happily instantiate SUB_T such that the set of hypothetical types is empty (e.g. set it to String).
This does mean there is no way we could create a helper object as no type would satisfy the constraints - but we don't actually need a helper object, so we can simply pass null.
In short, this bug lets the compiler infer a type hierarchy of Number -> T -> SUB_T without ensuring that Number -> SUB_T holds. Usually, that wouldn't get you far as there is no type that could substitute for T - except that you don't need a type because the compiler will accept null even for "impossible" types.
It's interesting to note that the class cast exception occurs in (3), not in (1). From what I know, that is because Java's erasure takes constraints into account - the arguments of nice() become (Helper, Number) after erasure, not (Helper, Object).
>Is it safe for Foo<String> to be a valid type?
Wrong question. Is it useful for Foo<String> to be a valid type? The answer is probably no. So why allow it? The below example does not come close to explaining why this should be a language feature.
I was at the conference where this was presented. The paper is terrifically written, and it's good that someone is investigating this.
That being said, it's just not that big of a deal. In practice, this will never be a problem for anyone writing Java.
The Java wildcard system is insane and no-one is trying to build type systems based on these ideas, not even Scala and the new batch of JVM languages (Kotlin, Ceylon). They have their own problems, but this isn't it.
David Byrne repeating "Same as it ever was" comes to mind ... ( https://www.youtube.com/watch?v=I1wg1DNHbNU )
Meanwhile, javascript is winning.
From a practical perspective, static type systems have proven useful for tooling: to help us with code completion and refactoring. Beyond that, they appear mainly to hurt developer productivity.