Java Generics Are Turing Complete
arxiv.org
arxiv.org
interface Z {}
interface N<x> {}
interface L<x> {}
interface Qlr<x> {}
interface Qrl<x> {}
interface E<x> extends
Qlr<N<?super Qr<?super E<?super E<?super x>>>>>,
Qrl<N<?super Ql<?super E<?super E<?super x>>>>> {}
interface Ql<x> extends
L<N<?super Ql<?super L<?super N<?super x>>>>>,
E<Qlr<?super N<?super x>>> {}
interface Qr<x> extends
L<N<?super Qr<?super L<?super N<?super x>>>>>,
E<Qrl<?super N<?super x>>> {}
class Main {
L<?super N<?super
L<?super N<?super
L<?super N<?super
E<?super E<?super Z>>>>>>>>
doit(
Qr<? super E<? super E<? super Z>>> v
){
return v;
} }
You can make your compiler:1. Bark out an odd error. (Loop for a while and then stop due to an internal stack overflow.)
2. Compile something that gives a runtime type error.
It would have been SO much better to give the language proper macros to begin with. I mean, a library that's actually meant for doing AST manipulations, and a way to tell the compiler "apply this transformation to that code before doing anything else with it", instead of another compiler-only interpreted language inside the original language. People would not have to learn 2 different syntaxes, and compilation would probably be faster (well I'm thinking more of heavily templating dependant C++ libraries here).
I'm just sad that Java's accidental compute system managed to be uglier (and apparently far less useful) than C++'s. The language could use some accidental features to counter the lack of intentional ones.
C++-like templates can be seen as type level macros (or rewrite rules) and run with full type information.
* easy and standard way of doing the most often used things (like replacing type identifier with another)
* automatic handling of what-stuff-came-from-which-line of code, to facilitate better error reporting by that macro library
* honest to god stack trace from the macro library if it falls flat on it's face
I mean, if you've ever seen errors that C++ compilers gurgle out from heavily templated code, I don't imagine macro library being any worse off a priori. Basically, when the compiler puts out a 3 screen error message, it's almost always much faster to just eye which lines it's referring to and read the code, than trying to decipher what the error message is actually trying to say...
Trying to avoid Turing completeness ends up introducing new concept/syntax to the language every time there is a need to do something that could not be done before. It ends up bloating the base language making it hard to reason about, and remember how every nook and cranny work/interact. That's why the "simple core language + macros" -kind of approach holds appeal to me. I'd rather see a comprehensive standard library as a cure for lack of standardization (not being able to read other peoples code) than base language that just keeps getting more and more complex with time.
Then again, maybe I'm just naive, I've only worked in small teams or alone.
> * easy and standard way of doing the most often used things (like replacing type identifier with another)
I think you're talking about a conventional type system, or something close to it.
> I mean, if you've ever seen errors that C++ compilers gurgle out from heavily templated code, I don't imagine macro library being any worse off a priori. Basically, when the compiler puts out a 3 screen error message, it's almost always much faster to just eye which lines it's referring to and read the code, than trying to decipher what the error message is actually trying to say...
Honestly I think the biggest problem with macros is the compiler/tooling side - if you allow compilation to just run arbitrary code that does whatever, there's no structure for the tools to make use of.
I would agree that compilers need to make error handling a lot more first-class and offer good support for that kind of use case. Rust is a positive example, with a lot of emphasis on good compiler errors - needed because they know how hard it is to satisfy their borrow checker. It would be great to apply that same level of support to errors from custom code (and really there's no reason Rust-style ARM couldn't be ordinary library functions in a language with good high-level constructs - you can use monadic regions to express the same behaviour).
Lex showed how to construct the peano arithmetic in Java generics and how to formulate problems such that solving the question of what types could be returned from An Rpc call with List would amount to deciding these small theorems or problems and therefore the GWT SerializationTypeOracleBuilder class would have to solve the halting theorem.
That's probably a wrong paraphrasing and maybe lex will see this and chime in but I think the original discussion was lost forever ironically when Wave, which was built with GWT, was shut down.
comment from the source:
// Encodes a Turing machine that counts in binary, and then halts.
// Gives stack overflow by default.
// If you increase the stack size (javac -J-Xss100M Main.java),
// then it type-checks, but it takes a lot of time.I believe that type checking in unextended Hindley-Milner type systems is exponential, so, even if the algo is guaranteed to terminate in theory it might not do so before the heat death of the universe. Again, in practice outside of test cases demonstrating this 'feature' it doesn't matter.
Yes it recursively solves for function typing parameters though the call graph. This isn't guaranteed to exit.
If the language you are using annotates function argument's with types[1] then this behavior is avoided and Hindly-Milner becomes effectively linear. This is why type-inference is starting to become a common feature in most languages.
[1] Nearly every language does this, but inline clojure are sometimes type inferred. Depends on language/language family.
This is a similar result. The fact that the Java type system is turing complete doesn't stop us expressing things with it. It just tells us there are some things we can't say about ALL Java type declarations.
To summarize, Theorem 1 has two practical implications:
1. If one intends to implement a formally verified type checker for Java, it is necessary to choose a subset of the type system that is decidable.
2. If reified generics are added to Java, then one needs to find some solution for not turning instanceof into a security problem.
Preferably one that you don't have to be at a university to read?
Beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.
Alan Perlis
Being Turing complete or not is an intrinsic quality of the type system which has not been shown to have any effect whatsoever on the effectiveness of a language. Similarly, being more or less expressive, being sound or not, supporting subtyping or not, are intrinsic qualities that have not been shown to have either a positive or a negative effect. Those are all true properties -- like Coke cans being red -- but they cannot be assigned good/bad judgment other than in people's personal preferences -- until, that is, someone actually starts collecting data.
> And the "FUD" is nothing more than the very real reality that many teams find those language far too clever for their taste.
I've heard people literally say "we shouldn't use Scala because its type system is Turing complete" (in an organization that was already using Java). I call that FUD.
I agree, but we still don't know which side has the advantage and how big the effect is. I believe that if we somehow control for all other factors, the effect of linguistic features is very small, but that "simpler" languages like Java/Kotlin would have the (small) advantage over "complex" ones like Scala. I can support this from personal experience (and also for theoretical reasons), but I can't prove that. You likely have a different experience (and other theoretical justifications), but can't prove your conjecture, either.
> I call that FUD.
I call that silly. It's just as silly as using intrinsic features as a positive. We shouldn't use Scala for a hundred reasons, none of them having to do anything with the formal language acceptable by its type system. ;)
Avoiding turing completeness requires either very very careful design OR (trivially) a hard limit on size/iterations the compiler is allowed to make (a sufficiently small limit, obviously we call many things turing complete colloquially that have very low limits anyway like 8-bit cpus with ~kb of memory)
Basically, the expressiveness of a language is unrelated to it being Turing complete. And is much more relevant for most conversations.
it's actually very hard to avoid in a system that is supposed to do some sort of computation.
Which is a terrible outcome, and isn't a problem for nearly any other language save C++.
Avoiding? Why would you want to avoid it? Guaranteeing that a compiler halts on any input is not something that is necessary in practice, and it restricts the amount of processing that can be done on the code during compilation.
I think its restrictions are that it only allows forward jumps and a limited amount of instructions.
So the property "not Turing complete" does not give you anything practical.
A better approach is to have a scheduler which is fair, and which gives the compiler the cycles it deserves, even if it needs many of them.
Secondly, the eBPF constraints put the language even lower in the hierarchy than regular expressions.
The code is executed inline when filtering packets, instrumenting system calls and to prefilter kernel probes for user space. It has to be really fast, i.e. execute in a bounded amount of time.
However, Turing-incomplete sublanguages can be very helpful in practice, for the same reason that a `for` loop is very helpful even though it is not essential and we can solve all of our problems with `goto` which is ultimately what our compilers generate anyway. Why do most people prefer `for` to `goto`? because with the former, you pick up a writer's discipline which makes it harder to create infinite loops and to accidentally miss parts of the data structure.
Well, in any Turing-incomplete subset, it is impossible to make an infinite loop and usually very hard to miss parts of the data structure (since there is usually only a few ways to enter that data structure that the computer can prove won't halt) -- and, if you want, you can build tooling which checks your algorithms to guarantee these things. It guarantees that even any recursive handling you do with the data structure is not going to be infinite.
With that said, there are some examples of perfectly valid algorithms which don't nicely support that sort of structural recursion, e.g. if you can "jump down" to a much smaller (and sometimes hardly-related) data structure rather than incrementally descending into it. A great example of this is the GCD algorithm,
function gcd(x, y) {
if (y < x) {
return gcd(y, x);
}
return x == 0 ? y : gcd(y % x, x);
}
We can easily prove that this halts for positive integers by simply looking at the second parameter; each recursive call makes it strictly closer to 0, which means that it must hit 0 and stop recursing. But a Turing-incomplete subset would probably balk at both of the recursive calls, if, say, we tried to encode integers as lists of booleans to get a "binary"-ish representation: it will say "hey, you can't recurse on gcd(y, x) or gcd(y % x, x), because you're supposed to be going in gcd(x, y) from x to a substructure of x, but y and y % x are not substructures of x!"You might be able to do it by encoding everything as a list of nulls and then first invoking some sort of decrement-increment step to swap x and y, then doing something similar for the modulus, but it means that your runtime might be something like min(x, y) * max(x, y) or something -- ugly.
On the other hand, the language built into Bitcoin [2] was deliberately designed not to be Turing complete. This avoids problems in deciding if a transaction is valid or not.
[1] http://www.inf.fu-berlin.de/inst/ag-ki/rojas_home/documents/...
Still way better than no generics at all, but if you are used to better implementations these random limitations can be really painful..
They are a big improvement over not having generics and, in my experience, one seldom sees those workarounds you mentioned. Sure, when they are necessary they are ugly... but how often are they needed in standard application code?
IMHO deserializing JSON is quite a common application code.
If you are really interrested I'd create example code, but (as being lazy, and the mentioned code not being my property) I'd hope to take my word for granted :)
edit: I wrote useless in many cases!
https://github.com/jhalterman/typetools
...but proper support of reified generics certainly would have been better.
I guess that's why the original comment said useless in many cases. Not completely useless. The next sentence also said they are way better than no generics.
Java generics problems: "primitive types" cannot be used as type parameters. Type erasure makes type parameters unavailable at runtime, which sometimes causes many problems.
When you see java code passing a .class as a constructor/method parameter, that is often a workaround.
Try Stream.toArray(). Type erasure makes it a pain and forces you to provide an array Supplier.
Other Example: you cannot have overloads of a method one taking Function<T,U>, other taking Supplier<T> because their erased signatures are equivalent.
Disclaimer: I still prefer Java when C# is not an option, yet I reserve the right to criticize its shortcomings.
We use Groovy for some things, but here I'd stick to Java.
Also Groovy has limited IDE support.
Yes you can use ArrayList but you'd think that something as fundamental as an array would work with generics.
https://docs.oracle.com/javase/tutorial/java/generics/restri...
interface List<T> { T[] toArray(T[] item); }
You need to pass in a T[] because arrays keep their types at runtime but the generics don't.
Now, I've never worked with Java, but if you say that java generics make you miss C# generics, they must be really horrible :-)
With generics, you can only pass complete types as generic parameters; templates accepts types, templates and integral constants.
You can't have partial specializations with templates.
Typedef. This is the most annoying limitation I found in C#. You cannot type:
typedef vector<MyClass> MyList;
and have it valid in the whole project.Having worked extensively with both I think Swift learned a lot from C# (and many other languages).
As for Concepts... No one seems to be able to agree on them so I'm not holding my breath.
Tons of generic APIs essentially do the same thing by passing in the class object in the constructor, e.g. http://stackoverflow.com/a/1090488/1075909 . The question for people not that familiar with the Java type system is often "Why do I have to pass in the class object when I just declared the type parameter in my instance?"
That's not the only thing you need to workaround because of type erasure (the "TypeToken" stuff in a lot of deserialization libraries is another one that comes to mind frequently), but it's probably the first annoying example people hit.
The only reasonable way to do this is to accept a lambda that tells the compiler exactly what call is legal to create an instance of T.
And that's called a factory.
public static T Factory<T>() where T:new() { return new T(); }
Having a type argument available; as adevine mentions, performing type-level things rather than just instance-level things. A smarter language than Java could handle this with hidden arguments, or passing along a dispatch table for type-specific actions when constructing a generic type. Things get trickier when you're e.g. storing a reference to a static generic method.
In reality, there's a continuum between C++-style generics-as-parameterized-syntax-trees and Java's generics-as-ignorable-type-checker-façade. Going full C++ isn't what you want; the error messages on failure to instantiate are unpleasant. Going full Java leaves you with a few holes. C# is probably a small bit too far in the C++ direction - instantiations can add up, despite .NET reusing instantiations where the type arguments are object references (that is, the CLR will effectively erase types if your type arguments are reference types, but you can't observe this without escaping the type system).
Need to pass in a class type, but it's of a list?
List<YourType> list = new ArrayList<>(); yourfunc(list.getClass());
C# got all this junk right.
- Reification
- Declaration-site generics
- Default covariance when variance not specified
- Variance inference!
In practice default covariance is usually the most suitable; it's the pragmatic approach. But you can make a generic type super sound, if that's what you need. And with variance inference your generic type can safely inherit from generic Java types.http://gosu-lang.github.io/2015/11/22/threading-the-needle.h...
Sure, I could use Integer/etc, but now each stage of my transform is creating a ton of garbage and I no longer get data locality.
Admittedly this is a little annoying, possibly even "really painful" so this doesn't really contradict your view... I don't find it really painful, just a little annoying, but that could be because I haven't used C# or another language people say has better generics so I don't know what I'm missing...
So, claiming type erasure making generic useless is a false argument.
void func(List<ClassA> list);
void func(List<ClassB> list);
Using type erasure, I cannot specify functions that are overloaded based on the generic type of an argument. To the runtime, both of these accept an argument of type List<Object>, and so it is ambiguous which one to call.In C++, which does not use type erasure, this sort of function overloading works as expected.
http://stackoverflow.com/questions/20918650/what-are-the-ben...
If anything, my opinion is that the abundance of so many dynamic languages on the JVM, and the active use of those dynamic languages in JVM projects, has much more to do with how no one really wants to work in Java than anything to do with how generics work (or don't work) in the JVM.
One such thing missing from .NET for example are higher-kinded types. Basically people end up with the need for `instanceOf` because they aren't able to write generic code that abstracts over containers such as List[T].
And this holds back language development on top of .NET actually. There are good reasons for why alternative languages flourished on top of the JVM and not on top of .NET, in spite of its original marketing, which is a little ironic. Not the only reason mind you, other reasons where availability on Linux, having an easier time to generate bytecode along with debugging symbols, more mature tooling, etc, but having reified generics in the runtime either means that you have to limit your language (e.g. C#'s type-system with a different syntax), or it means that you have to do the type erasure yourself and always have performance problems and be considered a second class citizen. Lack of higher-kinded types is actually what's holding F# back as an FP language, because in FP there is a lot of opportunity to abstract over M[T] types, that are just not doable in a static language without HKT.
And don't get me wrong, I like dynamic typing as well, but the problem with languages like Java and C# are that they are neither here, nor there, in many ways being the worst of both worlds.
And the only advantage that .NET's reified generics have over Java is actually the specialization it is doing for primitives. Which is nice for performance reasons, since you avoid boxing and so on, but doing specialization doesn't necessarily need to be a runtime feature, when it can very well be a feature of the language's compiler. And we weren't talking about performance anyway.
https://news.ycombinator.com/newsguidelines.html
We detached this subthread from https://news.ycombinator.com/item?id=11777678 and marked it off-topic.