All Programming Languages are Wrong (2018)
users.rcn.com
users.rcn.com
1. Make over the top statement in title.
2. Vaguely address the over the top statement in content.
For example the author hasn't shown us why "All" programming languages are "wrong". They haven't shown how they would fix them to make them "right". Their premises (e.g. "computation is almost always free") are flawed.
I wonder, if the title was called "A rant about some popular programming language paradigms" would it be as popular?
Isn't that the opposite of the goal?
the author is very vague about why exactly numbers don't fit in the type system, and in which languages. first class types for numbers exist in some languages. is he advocating for default overflow checks, which bloat code size?
No, he’s quite specific: “hardware details such as the number of bits supported by an integer add instruction show through in the language semantics.”
Now, it's also true that there are quite a lot of languages where this is not true, e.g., Scheme, Haskell (IIRC with the default numeric settings), Ruby, Python, for instance, and many more have numeric types in core language or stdlib that don't face this problem but don't make them the default for simple numeric literals.
Numbers are hard!
Consider something simple:
9999999999999999.0 - 9999999999999998.0
In discussing what is Right™ we are making a choice between all the possible Good™ ways we can deal with that expression. Here are some of the most obvious:1. atof() could fail since the string representation doesn’t match the resulting float, but then what should happen for 0.3? I've seen this only occasionally.
2. We could use a hypothetical atonumber() which uses decimal or big float or some other representation, but what does this do to performance? This gets tried a lot.
3. We can ignore the issue and blame the programmer for not constraining the input domain to that of our function. This is what most people do.
I don't personally like any of these; I've seen and experienced some of the problems with each of the approaches, so I wonder if there's a Good™ fourth option (or a fifth). Maybe it's only because I have my own ideas of what a fourth option might look like that I'm not very quick to consider this a "solved" problem to the point that someone (anyone) knows the Right™ answer, but maybe it's worth you (and others!) thinking about this too.
I'm extremely interested in suggestions here.
Then clicking next you get jumped into a new language implementation : /
If we're limiting what we mean by "computation" to a few extra non-branching instructions on data we already have, I would probably even endorse it outside particularly unusual settings.
Taken that we've more or less stalled with regards single core performance, I'd argue that computers are now getting slower (because software is bloating faster than single core performance is increasing).
In the vast majority of cases, it is. He's talking about the ALUs actually performing work, as opposed to waiting on memory. He rightly points out that cache misses make the biggest performance impact on modern systems. Most languages these days do not address this, there's tons of indirection by design.
> Taken that we've more or less stalled with regards single core performance, I'd argue that computers are now getting slower (because software is bloating faster than single core performance is increasing).
Again, if you can actually give your ALUs enough work to saturate them, your problem is most likely embarrassingly parallel, in which case single-core performance is not the bottleneck. Otherwise, you will want to optimize for memory access.
The script was re-written and they got it right into 10 min exec time when he couldn't do it in 3 months anymore
https://www.youtube.com/watch?v=fX2W3nNjJIo&feature=youtu.be...
> Today's languages contain far too many features that do almost the same thing but have slightly different performance characteristics, for example regular and virtual functions in C++.
Odd example. Virtual functions help enable certain designs, but avoiding virtual functions is commonly recommended when writing cache friendly code.
Virtual functions hurt performance (generally not significantly) but allow for OOP design, in theory to make the program structure more clear/organized. Exactly what the author is supposedly advocating.
> It does not reflect the realities of modern hardware, where computation is almost free, memory size is almost unlimited ...
Well, no, this certainly doesn't apply to all problems!
> ... because they seem to be in love with the idea of "object" or "abstract data type." ...
Again, there are many different paradigms of programming, OOP being one of them with its own pros and cons.
1) null
2) exception based error handling
such that, when you call foo() where foo is
String foo(){ blabla }
you can get a String, null OR an exception (!!!) and most compilers happily let you treat it as if it only ever returns a String.
I hope some day null is no longer a thing, and that Functional Programming types like Option, Either, Try etc in the native libraries is the new default.
* Java also has unchecked exceptions, and, incredibly, there's still no consensus as to which exceptions should be used for what and when, though these days the most common approach is to simply stick only to RuntimeExceptions, which is terrible.
* Functional error handling using Option, Either, Try etc are arguably much simpler, safer and more powerful than exceptions.
Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.
Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)
Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.
It could be that, when learning Java, Python and any other language, we learn that methods return objects... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.
When lists and booleans also implement map, flatMap etc, you can actually reduce syntax of languages even further- there's no need for looping syntax like for, while etc, and no need for if else either. This is probably too extreme for most people, but think about it!
Maybe flatMap would return true only if all chained booleans are true?
Bimap would allow you to provide functions for what to do in true and false case?
Fold would allow you to turn true or false into a single type?
I'm sure someone more experienced in FP can weigh in with something more correct! :p
map :: Bool -> (() -> b) -> Maybe b
map True f = Just (f ())
map False _ = Nothing
Or, in Haskell because you've got laziness you could go with: map :: Bool -> b -> Maybe b
Which already exists as a combination of Control.Monad.guard and Data.Functor.(<$). map tf b = b <$ guard tfIn fact I'd say checked exceptions are a richer formalism since they essentially provide something akin to intersection types for errors.
The fact that there are also RuntimeExceptions comes up a lot as an argument against Java/checked exceptions but that doesn't convince me. Haskell also has runtime errors that can happen (division by 0) that are outside the Either or Option/Maybe world. The problem is maybe rather that people are lazy and the trend became to decide that everything should be a RuntimeException because who needs all that noise and checked exception handling, right? I'm fairly certain that people who rejected checked exceptions would also reject the more functional approaches.
My argument is that no consensus around using checked exceptions correctly ever developed and so they never really had a chance to shine.
Having said that, sure I think having more usage of idioms we find in FP for error handling/propagation or early aborting is great. It's better than just raising runtime exceptions. However we are still in a world where no widely known language actually enforces (!) error handling in that fashion. It's all convention in the end.
And so my point was that if you want a language that tries to encourage error handling, with known failure modes communicates in the method signature then Java's checked exceptions sound pretty close.
Sure they are not perfect but the question is whether it's worth exploring better formalisms along the lines of first class errors with dedicates language support or piggybacking on the existing type system features and introduce conventions? The latter sort if works but I find it less ...exciting.
stream.map(f).collect(…)
is where checked exceptions failed. #map is a generic method that should obviously throw each of the checked exceptions f can throw, but without some kind of generic sum type Java can't express that. Everyone is forced to choose between "can throw any exception" (e.g., Callable<T>) and "can only throw unchecked exceptions" (e.g., Function<T, U>), each of which spreads virally and punishes anyone using specific throws clauses.You need generics for exceptions. If the inner function can throw some exception, then it should be possible to declare the stream method that it will throw the same exception.
I believe that the JVM doesn't actually support this, so the reasons were got the horrible situation we're in is because they didn't want to sacrifice backward compatibility.
And with my .NET hat, I hate having to dig into outdated documentation to discover what exceptions might come my way, so one ends up catching any kind of exception, just in case it might blow, in critical code paths.
The problem you describe may be an issue with checked exceptions but it would equally be one in any language with the aforementioned feature.
> We're not going to handle each of those cases differently, if we were going to we could always catch the specific exception type explicitly.
But you don't have to handle them differently. You can though and your method signatures will tell you what type of errors you might have to deal with. Some make sense to handle on the spot others don't.
You don't get that with runtime only exceptions. With those you really don't know and can get all kinds of errors and so you expect all and then just catch all and deal with it or blow up/abort whatever is going on (which let's be honest is what most web services do). Is that good? Yeah, sometimes. It certainly is easier to do that instead of thinking about possible failure modes when you know you'll only end up logging the error and give up anyway. I'm not trying to be cheeky, it really is overkill to make full use of checked exception in some cases.
Granted I haven't seen many great examples of checked exception usages so far but I wouldn't dismiss them because they don't mesh with what you're used to or with what's common in a given domain.
So this means, you will have a whole bunch of re-throws, or exceptions wrapped in "higher design level" exceptions which you then throw to finally communicate the error condition to the proper component, polluting all signatures on the way with "throws".
Or you catch it as early as possible, wrap it in a RuntimeException, re-throw it unchecked and let the high level component properly handle it, effectively bypassing the "checked exceptions" system :D
Just my two cents, I know this might be a controversial position.
For instance, Java's checked exceptions break higher-order functions. A map function should be able to say "I can throw anything my argument can throw". Some wrapper functions might say "anything my argument can throw, minus FooException because I catch it, plus BarException because I might raise that myself."
If we're going to dig into it, first we need to call out the occasional need to interrupt running pure code (eg. "the user hit ctrl-c"). As it can happen at any point, I think it's uncontroversial to say that return values are inappropriate? These are Haskell's "asynchronous exceptions", I think they do their job fairly well, and I don't know of a compelling exception-free alternative (although I'd be very interested to learn).
So now we can restrict ourselves to "synchronous exceptions" - those raised because of the function we've called.
I agree that we don't need them. They bring to the table early termination and transparent error propagation. There are contexts where these are desirable - if you disagree on that point let me know and we can dig in. In sufficiently expressive languages, we can do it ourselves - so that's plenty to show we don't need them.
But we may want them.
When we do it ourselves, we usually lack a way of telling the compiler which paths to privilege, and we often find some overhead compared with native exceptions.
When exceptions are provided in the language, we get special syntax to distinguish them. You describe this as a drawback, but nonuniform syntax can help with legibility - if different things look different, I can more easily figure out what I'm looking at. Obviously there are tradeoffs here.
Putting exceptions in the language also provides a distinction to the compiler in type checking, allowing different rules to apply. For instance, Java methods throw some set of exceptions, which get unioned (&c) in ways you can't speak about sets of types otherwise. Simply applying the ordinary value-level rules would be problematic (I hit this in Haskell sometimes). Ideally you can broaden your type rules to support what you need for all of it in one system, but it's not always clear they combine well. Even if it all can be made to fit, I could easily see a situation where assumptions need to be made for inference to work, and the most ergonomic assumptions are different for most values vs. bags of errors.
It's just so much better and easier to understand than exceptions! Doesn't require any special syntax or mental gymnastics to understand the execution path either.
Say you have a function f1 returning an Either a b value then another method f2 which calls f1 but doesn't handle the error case will simply flatMap or bind or whatever the language of choice is calling it and return yet another Either a b value until at some point up the call hierarchy someone actually decides to unwrap and if necessary do something with the error case. The function signatures have the same problem as the parent comment mentioned.
flatMapping over an Either and returning the result is no different than adding "throws SomeException".
Then Option types (since they were mentioned as well somewhere) are a bit of a different story all together. They make sense for describing absence/presence if values/results and are definitely useful. However they don't compete with Exceptions in any real way since they don't really work for error handling beyond the most basic cases (i.e. where handling the error somewhere doesn't matter/need to happen).
Some of his views might be controversial, but he has plenty of Lisp implementation experience and clearly knows his stuff.
And from what I've read so far, he writes excellent documentation. I'd recommend anyone to at least skim the whole thing [0].
Even in more typical use cases, it matters: for example performance or memory use can mean the difference between a small or large EC2 instance which has very real cost implications. Or it affects battery life of my phone app. Or between my application feeling fluid or laggy.
All of there’s things have real implications for users.
I very much care when I need a top of the line computer to run some shitty application that should be able to run on 20 year old hardware.
Yet I also understand programmer ergonomics and recently implemented an interpreter for a domain specific language in an already not the fastest language because productivity mattered more to me and correct easy to write domain code mattered more to me. But the point is that it was a conscious decision made after weighing the costs and benefits, not just because I think compute and memory are free. They’re not, this has a real cost for me being able to run it, in terms of servers, but with the benefit that I can replace complex buggy code that took me months to write, with simple easy to understand code that I could write in a few hours (and a thoroughly unit tested parser and interpreter that took a week to write)
The amount of code where the cost of actual computation isn't vanishingly small relative to memory access is so tiny, it arguably should not dominate the design of a general purpose language.
This wasn't true forty-plus years ago, when these languages were designed, and it's not true on some microcontrollers, but it's generally true today.
The author is completely right in that general purpose languages do virtually nothing to optimize memory access (which is a hard problem), as opposed to optimizing ALU usage (which is relatively easy).
https://www.h2o.ai/blog/a-brief-conversation-with-david-moon...
They need a language that is easy to work with, and share with others, and there are few that really do that.
But of course, there are. Go, Python, Julia and others I'm sure, all prioritize comfort over performance. Although, as compilers improve, they might one day actually become faster -- since the user writes more abstractly, there is a wider search space for optimizations.
Your statements are contradictory.
> they might one day actually become faster
faster compared to C/C++ ? Of course, Performance is improving day by day. What's the point?
Yes, faster than C++. Better abstractions can, sometimes, give the compiler more flexibility to optimize.
The proposed solution is the language he designed:
Instructions executed is not the only metric to consider! It doesn't makes sense to have loads and stores in the program with all of them having a cache miss.
Does the author mean that the memory hasn't caught up with the computation here? I don't think so.
> For example, one cache miss might take as much time as a hundred add instructions.
This is why compiler must work hard!
> Today's languages contain far too many features that do almost the same thing but have slightly different performance characteristics, for example regular and virtual functions in C++. This just encourages programmers to waste time on micro-optimization when they could instead invest time understanding the large-scale behavior of their program and optimizing that.
Not all programs need to worry about insane performance. But most do. It is totally upto the developers to choose the features of the language which they need. Choosing the right features/language for the right kind of programs is an important skill. Efficient code generation for all possible hardwares and all possible workloads with a restricted language features is not going to work or else Python would have been one of the very few languages to survive. Elon Musk wouldn't have tweeted that he needs C/C++ engineers ;-)
It seems the article suggests that neither the hard working compiler nor the hardware friendly language is required to get good performance!
Different kinds of problems are solved by different languages. Simply rejecting them without stating the details doesn't fly!
Dot makes it easy to chain things left to write, and also almost always dot based syntax is more readable than stuffing all arguments in parenthesis.
xs.delete(x)
vs
delete(xs,x)
And virtual / regular function distinction exists in performance centered languages for a good reason. Either you have to go out of your way to prevent dynamic dispatch or use clunky function pointer syntax and manage vtables manually. Similar can be said to many features he complains about.
There exist languages with arbitrary precision arithmetic, e.g. Python or languages with much sophisticated polymorphism e.g. Haskell, Scala or Rust.
Floating point numbers are implemented using double in C.
All bets on their precision are off unless you happen to
know the machine you are working with.
https://docs.python.org/2.4/lib/typesnumeric.htmlI actually would like more control in many of the current languages.
BCPL was from the late 60s, most people were introduced to it in the 70s, C++ from the mid-80s
https://en.wikipedia.org/wiki/CPL_(programming_language)
In an alternative universe, had the CPL project not suffered management issues and C would never had happened.
The time of the design of the language is different from actual implementations being made available. C++ compilers were pretty much unusable until the nineties.