Maybe We Can Have Nice Things
noncombatant.org
noncombatant.org
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!)
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!
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.
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.
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.)
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.
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.
- 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.
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.
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.
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.
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.
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 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?
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)
> I don't think anything will change that perspective.
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!
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++?
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.
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.
GC may not be the fundamental feature of Java the language, but it is certainly a fundamental feature of JVM.
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)