Annoyance about C++ errors isn't only about the error occuring. With me, it is predominantly about the utter unusability of the error messages. C++ has postprocessors you can use to get your 20 page STL errors down to a few lines just by reversing the expansion the compiler did to show you mere mortal something that you might recognize as your code instead of template-cthulhu.
Haskell has such situations as well, but usually far less verbose. Getting something to typecheck because you wrote down something incompatible uninferable still sucks. But far less than C++.
I'm always puzzled by people who see this as somehow a good thing.
If the Rust compiler can figure out what the type should be, why doesn't it just do the cross-function inference, and leave the complicated nested implications which only obscure the intent and effect out of it?
If having the programmer specify types is an important check on the correctness of the code that is written, how is blindly copying, without understanding some 60+ character type specification string from an error message going to help demonstrate correctness? All it does is make two sections "consistent". It isn't something the programmer understands or specifies as a type check.
> why doesn't it just do the cross-function inference
It could! This is an explicit design choice. There are a few different reasons. They're all sort of connected...
In general, Rust takes the position that the type signature is the contract. If you inferred the types on function signatures, changing the body of your function could change the signature, which means that breaking changes are harder to detect. It also leads to "spooky action at a distance" errors; I could change a line of code in function A, but then the compiler complains about the body of some unrelated code in a totally different part of the codebase, because that changed the signature of function A, which changed the signature of function B, which is called in function C. My error shows C is wrong, but I made a mistake in the body of A. That's confusing. Much nicer to say "Hey you said the signature of A is X but the body is Y, something is wrong here."
I am gonna handwave this one slightly because I don't fully remember all of the details, but full program inference and subtyping is undecidable. Rust doesn't have subtyping in general for this and other reasons, but lifetimes do have subtyping. I am sure this would get ugly.
Speaking of getting ugly, Rust is already criticized often for compile times. Full program inference would make this much, much worse. Again with that changing signatures issue, cascading signature change would cause even more of your program to need to be recompiled, which means that bad error message is gonna take even longer to appear in the first place.
I think there might be more but those are the biggest ones off the top of my head.
> I am gonna handwave this one slightly because I don't fully remember all of the details, but full program inference and subtyping is undecidable. Rust doesn't have subtyping in general for this and other reasons, but lifetimes do have subtyping. I am sure this would get ugly.
Correct. Haskell is the only language I know of with globally decidable type inference, and uses the similar hindley-milner method as Rust... but no doubt some of Rust's language features can break global inference. In Haskell, many common language extensions can also break global inference.
I think if Haskell was written today they probably wouldn't pick global inference as a goal, Haskell "best practice" types the function boundaries in the same way that Rust enforces.
SML and OCaml also have global inference, and in fact Haskell initially got it from there.
A lot of the "weirder" parts of Haskell are there because early on Haskell was pretty much "LazyML" and then it started growing into something different.
Because the computer being right 90% of the time when something is ambiguous doesn't preclude the programmer from understanding it (while still providing them a best guess at their intent for those times they don't), but DOES mean that the program doesn't assume the wrong thing that remaining 10%.
I am not sure where you got the idea that Rust error message suggestions lead to blindly copying 60+ character type specifications. They tend to be much more localized and understandable in my experience.
I got the idea by following the Rust tutorials and then making 'simple' programs, seeing the errors, going to Rust resources and getting the advice 'just cut and paste the expected type'. The expected types generally had 6-8 ':'s, and three to four deep nested type specifiers.
If a significant part of those types was just something like `std::collections::` or similar then I'm not sure I see the problem.
Suggestions should probably trim redundant prefixes like that, but recognizing standard library namespaces shouldn't be a big obstacle to understanding either.
I haven't been back to rust since, so I don't have the specifics. But it was clear to me that this was not a helpful way to program.
It was also crystal clear that, like C++, Rust puts many barriers to true abstraction. You have to know many, many details of how a specific type is implemented, sometimes several levels deep, to correctly use it at a high level. The cognitive overhead is enormous.
That's not entirely true. It gives suggestions in the cases where the C++ compiler does too. There are more than a few very cryptic errors you can encounter with Rust. I like it though, just needs more work.
Depends on the compiler. In my very limited experience I've found that Clang is far superior to GCC in this matter (but rustc is better still, apart from iterator errors)
I am curious about which GCC, G++ version you have in mind. Clang definitely took the lead, but by GCC caught up and I prefer GCC's do Clang. But even I am behind the bleeding edge quite a bit so not sure how things stand now.
It's been a while since I used C++ but I have never seen suggestions of the same quality that the rust compiler produces. Do you have a link that shows the error messages you're talking about?
For sure though, not every error has a great message and some can be cryptic. But those cases are relatively rare these days IMO
I don't agree here. As someone who's only ever dabbled with Haskell, I found its error-messages pretty cryptic, no better than C++. Perhaps they make more sense to someone with significant Haskell experience.
For what it's worth, I believe modern C++ compilers give much better error messages than older ones.
Haskell error messages are far less verbose, but I find them hard to read. What makes it worse is that often the error message involves a lot of deeply nested types that were hidden from me before by library writers using an alias. So I’ve had a lot of “where did that come from?” moments.
I had this problem when being onboarded into a TypeScript codebase recently. Unfortunately I don't think there's a technical solution to it: the type system simply won't stop you from making types that are way too complicated (unless you use a language like Go where the type system is intentionally lobotomized, which obviously comes with its own problems).
We're used to avoiding complexity when it comes to logic, but maybe there's less awareness when it comes to types. Maybe what we need is to have a bigger conversation around that so people realize it's something that needs to be on their radar.
The thing is, these types aren't usually complex in the way that logic is. Complex logic generally means lots of loops and conditions and such, maybe shared mutable state. Linear code that just calls a series of independent functions one after another is not generally considered complex, even though it may ultimately result in the execution of lots of code. What generally leads to these confusing errors is just simple types composed end-to-end. The problem is that while compilers are good at picking out the specific part of a function that's causing problems, they generally can't do the same thing with types, so it's kind of like if every time you got an exception, the entire module source was pinpointed as the problem.
Type systems are really good for detecting contradictions; they're much less good at figuring out which piece should be different. For any given type error, there may be half a dozen different changes to the code that would resolve it in different ways. Type systems often have no real way of guessing the user-intent there. There are loose heuristics, like... an explicit return type is more likely to be intentional (and therefore correct) than the type of the local value that's being returned. But I'm not aware of any formalism around this "ranking" of which types are most likely to be unintentional. Maybe we need one.
The only immediate solution I can see is to keep types simple enough that the user can fit the entire relevant type-space in their head (and in the IDE dialog!), so that they themselves can determine which part is actually "wrong" (as opposed to just contradictory).
Not only do modern compilers give better error messages (with room for improvement still), it is possible to make use of enable_if and if constexpr/static_assert to give proper error messages for templates, and when C++20 gets widespread enough, concepts.
Modern clang (usually, there are edge cases) gives pretty good error messages over all. And like you point out, if you use constexpr you get even better messages. Hell, I watched some cppcon videos over the weekend and learned that clang can even detect out of scope access of temporaries when in a constexpr (but not otherwise sadly), and also that in many cases, a lot more of a program can probably be constexpr than we might think.
The talks were by Jason Turner, who has an ARM emulator implemented entirely as constexprs and a test suite that runs at compile time (so if it compiled, the tests passed). Obviously for actually interacting with it, its not running at compile time, but the logic has the ability to run at compile time, which is pretty cool.
I personally call it "old school" vs "new school".
"Old school" is asically the programming languages that originated in the 70s-80-90s. An incorrect but an illustrative way to describe error messages for them is "The programmer needs to suffer". They are any combination of cryptic, terse, complex, exposing internal machinery of the compilers and linkers etc. There are many reasons for this: computers were not powerful enough to afford better code analysis, parsing, backtracking etc; the users of the tools also knew the tools and could tinker with them etc.
"New school" is from late 2000s on. I usually say it started with Elm. Clear messages pinpointing the exact problem, solutions to problems inside error messages, error clarity as one of the priorities in language/compiler/tool design.
Haskell has such situations as well, but usually far less verbose. Getting something to typecheck because you wrote down something incompatible uninferable still sucks. But far less than C++.