40 comments

[ 4.4 ms ] story [ 56.9 ms ] thread
"throw" is a nice thing in my mind and not at all like goto.

Before there was "throw" people wrote C programs with no error handling at all, or globals used to pass error conditions, or something like "Optional" with no syntactic sugar and 250% more coding errors.

Error handling is ultimately a non-local concern (e.g. this subroutine failed because somebody cut a fiber in Nebraska.)

Error handling is an everywhere concern. In general, recoverable errors are more local, and unrecoverable errors should crash/throw.
This doesn't feel like it translates into anything actionable though.

For example: I've never had a problem with Python's approach of defaulting to exception handling because it seems to bridge that gap nicely: either you can handle it in your local catch block, or you want to send it higher up to see if something with hopefully more context knows what to do.

Most error handling where "error's are values" is still ultimately doing this exact thing somehow it's just a question of with what implementation.

"Recoverable" means different things.

Where I work we would rather the app crash than display incorrect data -- accuracy is critical to us.

The engine control unit in your car has multiple degraded modes it operates in when parts like the oxygen sensor fail -- in that business the "show must go on" but so does the "check engine" light.

Fly-by-wire airliners like the A320 and B777 also have a number of degraded modes that pilots are aware of and there is a huge database of experience that proves the concept.

The 737 MAX hack tried to bolt a primitive form of flight envelope protection onto the old hydraulic control system but didn't consider that the plane would sometimes encounter degraded conditions such as one of two AoA vanes failed.

Absolutely, which is one of the reasons good error handling is difficult. The context of use for the current code determines how errors can be handled.
(comment deleted)
Here are a bunch of error-handling strategies that I'm aware of (which one is valid depends on the situation).

- Die immediately / assume the error won't happen, but still code defensively.

- Throw an exception (in languages that support this). setjmp() / longjmp() also falls in this category, although it is hard to do correctly.

- Return an error code to the calling function

- Contain the error in a data structure.

The last is a truly object-oriented approach. In larger systems this is the one that scales. Its advantage is that it decouples the error detection from the handling (temporal decoupling) and allows the error to be handled asynchronously. This is a property that exceptions don't have.

Exceptions are, in my experience, not nice to use. If you actually want to handle errors (i.e. not use them only as a glorified die() function) the code gets really ugly - you tend to use them like error return values, but with more verbose syntax (having two return mechanism is bad since it reduces compatibility of code).

I know a situation where exceptions can be useful: when interpreting a chunk of code in a sandbox in a synchronous fashion - i.e. you call the interpreter function of that virtual process, and that process either completes successfully or terminates with an exception. The virtual process is then cleaned up. There is no possibility to interrupt the interpretation and resume some time later.

This is more of a toy setting, though. You don't write complex systems like that.

> If you actually want to handle errors (i.e. not use them only as a glorified die() function) the code gets really ugly

And conversely, if you do want a glorified die() function, you want a actual glorified die() function, not something that encourages the caller to 'recover', pick up your undefined, probably-corrupted data structures, and continue trying to use them.

I wonder if something like supply-chain exists for npm, looks super useful.
I love when I read about languages and they completely ignore .NET and C#. Like it hasn't advanced in leaps and bounds.
And everything that has existed outside Bell Labs and UNIX.
.NET will always be looked at as enterprisey, and old school. I agree that it has advanced a lot, but I don't think anything will change that perspective.
And Windows-only. I know that Mono has made some good strides, but relying on it for the language itself adds a much bigger risk factor.
This is exactly the mindset of a lot of developers that don't even bother looking at .NET. If you did. You would know that .NET is far beyond offering Mono. .NET and Mono merged back in 2016 to form .NET Core. Which was supported on all major OS platforms and was open sourced. .NET 5 was released in 2020 which merges the rest of .NET, including WinForms. Now all open sourced. .NET 6 has already been announced with a cross platform UI framework. So enterprisey and old, it is not.
Yup .NET Core being cross-platform and open-source is a huge deal, and Blazor looks really cool, but I still don't think the way people look at C# is going to change.
Makes sense, and thank you for the updated information. I was looking into C# about a decade ago, and decided against it for the reasons I mentioned. I'm glad to hear that it continued to progress, though I'm a bit too invested in C++ at the moment to revisit that earlier decision.
Wow, that's just your particular bubble, but you write as though you're speaking for everyone .. it's exactly that kind of myopia the parent comment was pointing out.
I'm not speaking for everyone, I am just stating my opinion:

> I don't think anything will change that perspective.

The programs Palmer is talking about already exist, and are already implemented in C and C++, languages without managed runtimes. Rust is interesting here because it is memory safe and also doesn't have a managed runtime.
A programmer's opinions of various technologies is highly trajectory-dependent. The problem of uneven distribution of lore is exacerbated by two startling factors: we tend to be ignorant of the history of our field; and we are fashion-driven to a fault. I suspect that's how you get educated intelligent people with, um, unusual ideas about things. (E.g. I've worked with fellows who did not know who Alan Kay was, or what Prolog is.)

In re: Rust, Graydon Hoare gave a talk on the history of compilers in which he doesn't mention Prolog. https://thenewstack.io/rust-creator-graydon-hoare-recounts-t... I suppose he just didn't think it was relevant, but it's possible even he is a little ignorant?

I see a ray of sunshine in the form of a nascent and inchoate new paradigm that could be called "Categorical Programming", Conal Elliott has the scent: http://conal.net/papers/compiling-to-categories/ CT provides a mathematical formalism for finding the most highly factored forms of various functions/programs, enabling a kind of "convergence" of code to its most efficient form. But that's a big ol' tangent.

My point is, it's often more informative to try to understand the context in which something someone says makes sense, than to just decry them as wrong. Oh, and to remind folks to read your history!

Cargo from Rust and go fmt from golang have been some of my favorite things these past few years. Cargo isn’t perfect, as the author points out, but wow is it great to have it as a major part of the language’s ecosystem.

I recently got access to GPT-3 and was playing around with it, which meant breaking out Python for the first time in a while, and I just don’t understand how anyone deals with it. Poetry seems much better than what I remember Python ever having, but it’s not official of course and not everyone uses it. I somehow was hitting dependency issues with packages stuck in Python 2 and I couldn’t believe that’s still happening — it’s been over a decade now. Even ran into some very minor Black vs PEP 8 things. Somehow every time I touch Python I find myself fighting the tooling, which may just be my ignorance or bad luck.

Very thankful for Cargo and go fmt so I can just get on with things!

(And also the author is right, the Rust community is super welcoming. Thanks for that as well!)

It's not your imagination. Python tooling is awful.

If I may be forgiven a bit of preaching, I'll point out that the fundamental problem with the Python ecosystem is that package definitions are executable python code. This means they have their own dependencies, and are subject to all the pitfalls of general purpose programs - the halting problem, non-determinism etc. The Python community keeps trying to fix this by writing a better installer, and it's never going to work.

NPM and Cargo, with their declarative package definitions, are built on a far more stable base!

Whatever Perl's other issues, through the introduction of `META.json` CPAN successfully augmented a system designed for executable install scripts like `Makefile.PL` with a declarative package metadata mechanism. Why can't Python do the same?
Python packaging tooling and infrastructure is exactly like this: https://xkcd.com/2347/

Everybody uses it, but the work of maintaining it is thankless and doesn't receive nearly as much attention as you would expect relative to the popularity of the language.

The unfortunate thing is that there's only a very small number of people actively maintaining, and they have so little time available that it's nearly impossible even to contribute something substantial.

I bore witness to an attempt by my colleagues to replace the legacy XML-RPC APIs (which have been publicly declared as deprecated for years with no replacement even today) which completely fell through because the maintainers didn't want to (or have time to) provide any feedback whatsoever. It was basically requested that a 100% implementation with 100% test coverage be provided before they would even spend the time to evaluate the API design concept and give feedback.

Not sure I agree with this diagnosis -- Ruby gemspecs (for components) and Gemfiles (for dependency lists) are both executable Ruby code, but packaging troubles at the Pythonesque level are rare.

The trouble I've had with Python packaging had a lot less to do with the specs being executable code than with there being tools with overlapping use cases which interpret them in different ways, none of which seems to cover all use cases, and whose documentation of what they expect in package specs is sometimes in conflict. If the specs turned to JSON-with-comments tomorrow, those problems wouldn't go away.

> However, for all of NPM’s problems, at least it is a package management system at all! It’s easy to pick on NPM (or predecessors like CPAN, or CTAN, or...), but even at its worst it’s a huge improvement over manually managing dependencies (such as by manually vendoring them into your source tree, or just telling the user to install such-and-such libraries before attempting to compile).

Isn't the labor-intensive nature of manually wrangling all of these resources a constraint that pushes people to minimize version conflicts and micro-dependencies? The reason that you see these problems in NPM (and other version control systems) is because the automation removes a constraint.

Yeah, good point. The reason NPM can have packages like left-pad is that it works so well. It's easy to publish a package, it's easy to find one, it's easy to install and use a package. So sure, why NOT make a package for one function that you use all over the place?

There's a downside to all this, but ultimately it's to NPM's credit: the tools are so easy to use that people... use them.

I feel the need to mention, whenever leftpad comes up, that it's impossible for that incident to ever happen again. Packages can no longer be unpublished after a brief window (48 hours I think?), so if you lock to specific patch numbers your dependencies are effectively immutable
"A key innovation of C++ was to introduce RAII, which essentially ‘piggybacks’ on the value of the stack and enriches it with a lot more power."

I haven't written C++ in two decades, but it was my impression that heap allocated resources are not on the stack.

The major problem with C++ isn't resources can be used used after freed, it's that they are never freed if you don't clean up when your RAII is popped off the stack via destructor or other "event handler methods".

Isn't RAII more of a pattern implemented atop the core constructor/destructor? It's kind of a stretch to call it a fundamental feature of C++ like GC is to java/jvm.

Has valgrind "solved" memory leaks as a primary concern/danger in C++?

RAII is a core fundamental feature of C++ and I would argue that constructors and destructors are the implementation.

With RAII (using vector, string, unique_ptr, etc), memory leaks are pretty much a solved problem in modern c++. The biggest memory danger in C++, in my opinion, is dangling references where you have a reference to an object (either on the stack or heap) that has been destructed.

The idea in RAII is you create a type that's solely responsible for acquiring the resource and freeing it (and little else). This idea is indeed fundamental to modern C++ design. For example, smart pointers.

When created, the pointer lives on the stack. The resource it points to lives elsewhere but that's no concern to RAII. When the stack variable goes out of scope the resource will automatically be freed (via the destructor)

One place where this can break down in C++ is in copy and moves. The acquire/release works simply enough using constructors/destructors but correctly handling copies and moves has turned out to be harder to get right. And when you add in other objects that may refer to the resource object... well things can quickly become even harder to handle unless you're very careful.

What's a fundamental feature for C++ then, if not RAII?

GC may not be the fundamental feature of Java the language, but it is certainly a fundamental feature of JVM.

You should look into modern c++ - at least C++11. While it is possible to write the code with all the problems you state, C++11 adds some neat features that mean in practice you rarely do. Sure I can put something on the heap and forget to free it - but in practice I don't put anything on the heap without first wrapping it in something on the stack so that when the thing on the stack goes away my thing on the heap does too. This can get really complex fast to deal with edge cases, but most of the time the complexity is hidden and so doing the right thing that always works is just as easy as putting something on the heap that I have to remember to free latter.

Most people use address sanitizer not valgrind for the few times they have a memory leak. In general though the problem is solved not by tools but by using the modern C++ features. Valgrind is really only useful when you need to deal with C interfaces. (note interfaces, the code on either side need not be written C)

>Programming languages advance by introducing new constraints.

Among other things.

>throw is a little bit too goto-y for my taste

Conceptually, throw is not at all like GOTO. It's not a control structure, but a communication tool. Throw interrupts execution of logic in a subsystem that encounters abnormal conditions and communicates those conditions to the containing system. The second part is the most important one.

>However, for all of NPM’s problems, at least it is a package management system at all! It’s easy to pick on NPM [...], but even at its worst it’s a huge improvement over manually managing dependencies

This really depends. In many ways NPM made introduction of new dependencies easier, while making handling dependencies over long periods of time a much more hairy problem. I don't like that most devs today assume that versioned packages is the only way you can do things. Many also don't understand that "dependencies" is not a real thing, it's an aggregate label that is applied to a set of problems that happen to be solved by package managers. Problems like discovery, deprecation and compatibility checking.

> throw is not at all like GOTO

They are the same thing.

To see it clearly, replace each exception with a label, each "try" by a no-op, each "catch" by a label statement, and each "throw" by a GOTO statement.

    try:
        foo()
    catch BarException
        baz()
    

    def foo():
        ...
        throw BarException
        ...
Is like:

    CALL foo
    GOTO end
    LABEL bar
    CALL baz
    LABEL end


    LABEL foo
    ...
    GOTO bar
    ...
    RET
Either you have relatively simple exceptions and the equivalent GOTOs would also be simple enough to be not "considered harmful"; -or- you have complicated exceptions, which is really an ad hoc state machine, and you might be better off making that explicit rather than hiding it in a tangle of try/catch/throw control-flow logic, eh?
I had taken for granted that RAII was a good and useful pattern, from my dabbling in rust and what I've overheard in C++. So I was surprised and interested that Zig rejects that, in favor of a `defer` construct somewhat a la Go to free resources. I'm not sure I agree, but it shows that "advances" in PL design aren't universally agreed as such.

I guess what I'm getting at is I agree with the article that it's great that programming languages develop new ideas and constructs, but it's not always obvious which ones are "winners". How do you even really judge that? The quantity and quality of software in the language? Polls of developers?

There are still people who disagree that the world is round and a very very large portion of the populace is religious when it's abundantly clear that religion is bullshit. Go is an outlier with a huge media machine behind it and it will disappear back into the abyss as soon as Google stops pushing it with no contributions to programming language research at large.
There isn't a good answer. RAII is a great thing that I use all the time. However it requires deterministic destruction which in turn means some form of reference counting garbage collection. Garbage collection research has found non-reference counting garbage collection has a lot of advantages. So as a language designer you need to choose, better garbage collection at the expensive of non-deterministic destruction, or worse garbage collection but you get RAII. There isn't a clear winner.

For about 80% of all garbage this doesn't matter because it turns out the reference count is always one until it goes to zero so you don't need to track it, and in turn this means the better garbage collection algorithms I mentioned above are not better in this case. However this rule of thumb comes from C++ where training (based on real performance needs of the type of code you might write in C++) keeps people from doing the types of operations that would increment the reference count. Which is to say even if Java could figure which cases can use this rule (this appears like variant of the halting problem to me), they would probably discover that the number there is enough less as to not make it worth adding.

Worse, in C++ there is a tiny number of data structures with circular references that you cannot use reference counting to deal with. Thus as a C++ programmer will sometimes have to write some other garbage collector for the edge cases, and these don't get RAII deterministic destruction.

Note that I didn't state the advantages of better garbage collectors. I am not an expert here, so I'm leaving it as an exercise to the reader. Just know that there are advantages to them that make them a great choice for some problems despite losing RAII! If you are designing a language you should probably look into this early. (I was going to say a hybrid is the worst of both worlds - but maybe I'm wrong so I'll let you figure that out)