On one level yes. On the other hand it should hopefully reduce the FUD around alternatives with better type systems (Scala), if the type system is turing-complete in any case.
The problems with richer type systems (they're not "better", just more expressive, just as Dart's unsound, covariant type system is not better or or worse, just different) has nothing to do with them being Turing complete or not. And the "FUD" is nothing more than the very real reality that many teams find those language far too clever for their taste. I like Java, but others may not; you like Scala, but others may not. That's perfectly fine and normal, and doesn't make it FUD. As long as no one has good empirical evidence supporting their claims of their preferred language's superiority, these discussions are all about personal preference anyway.
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.
We don't yet have a lot of solid empirical evidence, sure - mostly because we don't have a good measure of productivity. But the fact that we can't measure it well doesn't mean it doesn't exist. There is a fact of the matter to be found, it's more than just personal preference.
> 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.
> But the fact that we can't measure it well doesn't mean it doesn't exist. There is a fact of the matter to be found, it's more than just personal preference.
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. ;)
Eh, turing complete is a really low bar to clear. In practice it just means the compiler can't be proved to halt on valid code.
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)
Turing complete is by now means a 'low bar'.. Turing complete means that a language, system can compute anything. Anything that can be computed, can be computed with java generics. Thats what it says. Its quite a important term.
I believe the intent is that it is not a very informative bar to cross. Consider that Conway's Game of Life is Turing complete. Doesn't really increase the usage of it for solving problems.
Basically, the expressiveness of a language is unrelated to it being Turing complete. And is much more relevant for most conversations.
Basically all you need for turing completeness is the ability to jump/branch/ignore-some-bits on some criteria see SUBLEQ or cyclical tag systems or XY combinators.
it's actually very hard to avoid in a system that is supposed to do some sort of computation.
It's a low bar in that it's easy to achieve. So easy that it's often done by accident, as is the case here and in many other cases (C++ templates are another example). It often takes more work to define a useful language not to be Turing complete.
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.
It very much is necessary. Take the linux kernel's eBPF interpreter for example. Since the code runs in kernel scope and in many critical places (system call interception, network processing) it's important that the code executes within a bounded amount of time.
I think its restrictions are that it only allows forward jumps and a limited amount of instructions.
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.
The only reason some regular expression engines take exponential time to match strings is because those implementations don't use regular expressions at all. Features like backreferences break them out of that class of languages. When you take the "regular" bit seriously you can guarantee linear time matching. See the engines used in Rust and Go.
I agree that it is not necessary in practice, no, and it is of limited theoretical use because a halting algorithm might still take ten years to do so.
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.
Note that e.g. Agda uses unary representation of natural numbers during typechecking, but then compiles them to `BigInt`s to avoid exactly this kind of slowdown due to a representation that is well-suited for proofs.
Accidental Turing completeness brings to mind the Zuse Z3 computer discussed recently. This 1941 system only supported fixed sequences without branching, but it was shown [1] to be Turing complete if you use weird tricks. The point is that it is fairly easy for systems to unexpectedly be Turing complete.
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.
Too bad they are still pretty much useless in many cases because of type erasure, and many type constraints cannot be expressed (eg. constructor constraints)...
Still way better than no generics at all, but if you are used to better implementations these random limitations can be really painful..
Don't know why this is being downvoted. I've been on a Java project for the last ~6 months after ~8 years of doing C#/.NET development. My brain hurts from seeing all the stupid workarounds in Java because it doesn't have real generics.
I guess it's one thing to claim Java generics have problems and another to claim they are "useless".
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?
Last week wanted to use GSON in a generic method to deserialize to a type which is generic type parameter, in a method of the abstract base class. Unfortunately it could not be done without the implementations passing up their .class via super constructor.
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 :)
> I guess it's one thing to claim Java generics have problems and another to claim they are "useless".
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.
In C# you can often avoid reflection using dynamic.
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.
Yeah, if I wouldn't care for type safety, and I'd want to write validation code for stuff currently handled by GSON instead of me, by the class definitions.
We use Groovy for some things, but here I'd stick to Java.
Apache Groovy's dynamic typing, closures, and shorthand collection syntax (from v. 1.0) are great for testing and playing with Java classes, and I'm told Groovy's Ruby-like MOP is useful for Grails developers, but the stuff added later (like generics in v. 1.5) made the language klunky and overdone. Best switch to Java (or Scala/Kotlin/etc) once you need anything to be typed, let alone with generics.
Out of curiosity, what would you say is missing? My one big beef with templates is lack of constraints and somewhat ugly syntax, but hopefully concepts will remedy the former.
My one big beef with generics is that they only work through constraints! With templates, the compiler checks the validity of a template only when applying it to a concrete type; a generic, OTOH, needs to be valid as an independent entity. And this means that you are stuck with the limitations of the constraints: you cannot specify that your type has a constructor accepting parameters, you cannot specify static methods, nor operators.
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:
That's not inherent to generics - see Swift, which absolutely allows protocols to require static/class members or initializers. Swift 3 also adds generic type aliases (typedefs).
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.
What exactly is missing, or better in C#? I'm surprised because I've been programming in OCaml for a long time, and never missed "real" (i.e. non-erased) generics.
Exactly - except it's gross and results in a lot of annoying boilerplate code for what should be considered a simple operation.
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.
Erasure is not the problem here, you need to be able to specify a type constraint that will allow you to invoke T(). What if T is an interface? An abstract class? A class with no default constructor?
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.
Yes, good point. But if you did have a type constraint, like C#, you would still need the runtime type info to know what to create. This is legal in C#:
public static T Factory<T>() where T:new()
{
return new T();
}
Unboxed values; this primarily affects performance, particularly memory performance, but with a sufficiently smart compiler could be worked around. It's unlikely any very clever JVM optimizations would see wins here though, as the assumption that everything is boxed is idiomatic.
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).
I downvoted it because it is not relevant to the submitted article. I would rather see discussion on the actual result - Java generics are not decidable. A discussion on whether or not Java generics are good is, in comparison, boring.
- 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.
You mean aside from it providing better and easier generics support? I personally like Kotlin, but I think Gosu has some advantages that should not be overlooked e.g,. Generics support.
Or my personal favorite where intrinsic types aren't objects so I can't use them in generics with makes the whole Lambda/Stream api a complete disaster.
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.
Eh, there are hacks around both of those... Gratuitous "typedef classes" for the first problem (instead of using a List<Sandwich> when you want runtime to be able to sniff the type, declare a class/interface SandwichList extends List<Sandwich> and use that) and just wrapping static factory methods around constructors to handle the generic part (to handle the second problem)
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...
I actually think type erasure might even be the right way to implement generics. You see, the point of static types is to provide compilation time check. That is, types are compilation time concern. So yeah, type erasure did introduce some inconvenience but if you need to access them during runtime, either you did it wrong or the compiler fails at some point.
So, claiming type erasure making generic useless is a false argument.
Not to mention, having type erasure made them backwards compatible with non-generic implementations. I don't think it would have been acceptable to break old Java code or introduce an entirely new collections library like .Net did.
I think that the problem with Java is not that they are implemented with type erasure (many functional languages do the same and monomorphize only when optimizing), the issue is that this implementation detail is a leaky abstraction in Java.
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.
If it weren't for type erasure we wouldn't have the abundance of dynamic languages on the JVM. Developing dynamic languages on runtimes that have reified generics is a PIA.
IronPython, IronRuby, and plenty more dynamic languages on the CLR which has reified generics certainly disagree with your opinion here.
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.
Java generics are cumbersome for sure, but if they're useless, it's not because of type erasure. As a Scala programmer, I've very rarely run into a situation where type-erasure has been an inconvenience.
You don't need reified generics with a more powerful type system. You might be feeling that C# .NET is better than Java, but it's actually a symptom that C#'s type-system also sucks. Basically whenever you have to do an `instanceof` check, that's a sign that the type system isn't strong enough and hence needs to be solved at runtime.
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.
What it means in practice is that any Java typechecker is either (a) incomplete - it will disallow some programs that should typecheck; (b) unsound - it will allow some programs that shouldn't typecheck; or (c) non-total - it will fail to terminate on some programs. (Or some combination of these three.)
No, that's in theory. In practice not much: any practical sufficiently complex type system quickly gets extended to become Turing complete; from time to time the compiler might get yell at you when you hit the maximum allowed type recursion limit, but usually everything just work.
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.
>I believe that type checking in unextended Hindley-Milner type systems is exponential
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.
"the compiler might get yell at you when you hit the maximum allowed type recursion limit" is an example of the compiler failing to compile something which is valid in the type system.
Right, but that's precisely how Turing completeness works. It's possible to write a program that can figure out whether most real world code terminates. All Turing completeness tells you is that no matter how clever that program gets, there will be some programs (not necessarily 'real world programs') which it won't be able to process. The fact that the Java programming language is Turing complete doesn't prevent us being able to write programs in it (indeed it means we can write interesting programs in it!). It is just a result that tells us there are some things we can't say about ALL Java programs.
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.
// 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.
whine whine whine. go make a language of your own then, that way it's going to be perfect and have no flaws. oh wait, you wouldn't even know where to start.
You don't need to be a good chef to know a meal is awful. A lot of people use the language (me included) and make it work, doesn't mean it's without fault.
DING! This is what always happens. If you want to get useful manipulations done to code you'll eventually creep up to a Turing complete system. Now you have a type system where you could calculate any program transformation, albeit in really really convoluted, slow and ugly way.
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).
Yes. Otherwise you'll end up with an ugly Python or Perl script that generates code, coupled with ugly build system hacks to make it work. This will happen sooner or later.
The Java culture fetishizes code generation. It's like somebody decreed "We want to take over the world, and one measure of that is lines of code. So there should be a hell of a lot more Java code: start writing Java code that generates more Java code!"
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.
AST Macros are useful for AST level manipulation, but the problem is that usually at that point you do not have full type information (name resolution or even type inference hasn't run yet), so they can't take decision depending on types which is sometime what you want.
C++-like templates can be seen as type level macros (or rewrite rules) and run with full type information.
I very much disagree. Even if they've ended up accidentally Turing complete (which, don't get me wrong, is bad), they at least guide you towards the right thing to be doing at compile time: simple, declarative transformations. I have never seen a macro system that wasn't immediately abused for unmaintainable clever-clever things, whereas this has only emerged ~10 years after Java introduced generics.
What if, there was a in standard "macro" library of the language
* 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.
C++ is a spectacularly bad example, it's not always like that.
> * 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).
An GWT engineer and former Scala guy, Lex Spoon, proved a similar theorem several years ago when we were upgrading generics for GWT-RPC. The GWT RPC code generator has to reason about return types of methods and which classes could possibly be deserialized say if you return a List of Foo.
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.
103 comments
[ 4.3 ms ] story [ 171 ms ] threadBeware of the Turing tar-pit in which everything is possible but nothing of interest is easy.
Alan Perlis
https://en.wikipedia.org/wiki/Turing_tarpit
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,
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/...
[2] https://en.bitcoin.it/wiki/Script
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:
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.
https://kotlinlang.org/docs/reference/generics.html
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.
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.
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?
comment from the source:
https://news.ycombinator.com/newsguidelines.html
We detached this subthread from https://news.ycombinator.com/item?id=11777678 and marked it off-topic.
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).
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.
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.