Terrible programming features from the past
medium.com
medium.com
For those not in the know, AOP is basically the Intercal COME FROM statement (itself a pun on goto, ie it's a jump but in the opposite direction) (think that through a bit). Yes, that's a totally nuts idea, but it turns out that with a bit of effort you can formulate it in such a way that you can do conference talks about it and people nod and think "o wow nice I gotta tell the team".
More concretely, AOP is usually a bit of compiler infrastructure that lets you inject code into other people's functions/methods etc and make arbitrary changes. It's nice for eg inserting logging code at the beginning of each method, with the method name and argument. You'd be able to eg define a query somewhere and say "for all methods with annotation X, inject code Y at the beginning of the method".
But naturally, teams that adopted it always had this one team member who thought they knew better and started using it for injecting all kind of behavior-altering code in methods defined far, far away. When reading such a method's source, there was absolutely no way to tell that code was going to get injected. If the AOP crowd was well behaved there'd be an annotation/attribute/etc that would trigger the behavior, but that's not a requirement. I've seen people inject code that sanitized data, which was very surprising if said function didn't actually take user data and suddenly while debugging a string's content had changed and there was no indication whatsoever what had caused it.
While it can be misused (by making unclear what aspects are being applied, as you mention) it can be very useful and avoid a lot of boilerplate code and repeated logic.
The concept is so appealing, “Hey I’ll just fire this message off and whoever needs to know about it will do what they need”.
It’s great to have that ability to focus on just the component or module you’re working on, but the lack of visibility on exactly what is about to happen, and what has happened, has caused many late nights of debugging and frustration.
It’s just how you described “there’s absolutely no way to tell the code that was going to get injected”
I’m guilty of contributing to this problem as well. Once you become familiar with the event system you’re working in, windows, DOM, whatever, you can make some pretty good assumptions about how other components are implemented. When you’re faced with a bug in code you don’t have access to, or a behaviour you don’t want, often the solution you’re left with is asking “okay, so what combination of events and timing do I need to force this component into the state I need?”
For example I remember doing this with some third party grid controls. We really really wanted the TAB key to insert a new row at the end. We had to orchestrate exactly the right mix of events and method calls in the right order, with a few BeginInvokes to get the job done.
I wish I had a solution to all of this and other frustrations. It’s all trade offs and a balancing act.
On a project where "logging" was budgeted as a short, separate task that could be delegated to a junior engineer at the end, I used Spring's AOP to inject performance and usage logs across entire chunks of the program. It worked great and took way less time than scheduled.
I was convinced there'd be other uses for AOP, and kept an eye out for other opportunities for months. Given that I completely forgot this was a thing until now, I think you can guess how often that happened.
Other than logging and profiling I've used it in ejb3 app calling old plsql business logic - we had problems with some plsql code handling exceptions and transactions in non-standard way (basically - comiting or rolling back in pl/sql code) which messed up our ejb3 container-managed transactions. We had a template for pl/sql code that should be followed - every function should start with a savepoint, never commit, and only rollback to that savepoint in case of error. But there was a lot of plsql code and some of it didn't followed that rule.
So I added aspect in java that wrapped around any plsql call, created a savepoint before it, and checked after the call if it's still there - if not it would a special exception. That allowed us to quickly find all the places that weren't handling transactions properly in PL/SQL and fix them.
We also used AOP for integration testing with our java business logic and plsql - instead of preparing and using up data during tests we simply wrapped transactions around testing methods and rolled back everything afterwards.
I don't think AOP is bad, even if it's truly COME FROM with a different branding.
There was the guy who used AOP to inject logging and perf, and set it to "instrument every method".
_Every method_.
It was Ok in test, but when the production load rose to the daily peak, the volume of logging data was in itself sufficient to saturate a network and requests got dropped. Including health checks, which caused machines to be taken out of the load balancer, increasing load on the survivors. Autoscaling didn't help, new machines came up with the same issue.
The failure cascade brought down the entire service.
True story.
Game modding also demonstrates the big strengths and weaknesses of AOP. New versions of games tend to break some or all mods until the mods are updated, even if the game developer is trying to support the modding scene and avoid breaking things. When literally every function in your code base is a potential extension point, it becomes impossible to change anything without breaking something else, and you can get insane impossible to debug errors.
But as you said, devs find all kinds of mind melting creative ways of using it.
This clearly makes it pretty awkward to handle. Yet considering that the compiler manages to figure out where code needs to be injected, one can ask: why does the programmer not get to see this information?
This is not a fundamentally unsolvable problem. It is a tools problem. It's easy to imagine a programming environment where you can tell immediately which code gets injected where. Even in a dynamic system you should be able to see, on examining a function, what its current contents including injected code are.
Some programming language features require proper tools support to be useful. This requires languages that want to introduce such features to be opinionated on the matter of what tools you can use.
When developing a Spring project, you indeed start to quickly appreciate the introspection feature that advanced IDEs like IntelliJ IDEA provide. Inspecting the application container at runtime is also possible, but it's a clear sign that your app got waay too complicated and has to be refactored.
Good tooling takes the surprise out of nonlocal control flow, and can project it into local control flow on demand.
To take it back to skrebbel's complaint, "methods defined far, far away", "absolutely no way to tell", "very surprising", "no indication whatsoever" -- all things that good tooling fixes because it can show you what gets injected where and why, when you need it (and show you a minimally intrusive reminder when you don't).
ALTER SELECT-PATH TO PROCEED TO PATH-2.
That stores into the code.In the early days of computing, programs did not have a call stack and code was not usually re-entrant. No recursion. So, finding a place to store state was tough. This is still seen in some embedded code.
It's all Von Neumann's fault. The program is stored in main memory, and he makes a big point that this means the program can modify itself. So early programming languages, operating systems, and CPUs tended to have support for that. Von Neumann didn't think of index registers. Array access involved code modification.
It took a while for index registers to become a standard computer feature. The first one was in the Manchester Mark I, but they were, for too long, a "high end" feature. The IBM 650, IBM 1401, and the Intel 8080 lacked them.[1] Building in an extra adder cost money.
This is maybe common in similar machines?
Thought PDP-8 was used extensively for lisp at various locations.
Was recursion made possible by allocating memory elsewhere and using spare word as pointer?
[1] https://collections.museumsvictoria.com.au/content/media/38/...
Stacks were, for a long time, controversial. What if you ran out of memory? Some rare compilers, such as Modula 1 for the PDP-11, computed the stack size for each task. If you wanted to recurse, you had to give a recursion limit. Async, the early years.
Stacks work well now because we now have lots of memory and address space. Early machines lacked that luxury.
There was a tradeoff. By nixing recursion the compiler could perform variable folding optimizations that would be impossible with recursion. That's because without recursion the call tree is an acyclic graph. The result is variables are held in registers and a tiny scratchpad instead of being pushed on a stack.
I don't think this is a real tradeoff. Your optimization clearly depends on whole-program compilation anyway, so programs that do not involve non-tail recursion can still be optimized as such.
It's worth reading with this understanding, and of course, it is a must to follow through with Dr. Knuth's equally seminal "Structured Programming with go to Statements", in which Djikstra is briefly quoted to qualify and clarify his position on the matter.
Dijkstra was Dutch, not Deutsch.
But I'm able to construct the German form from memory, and have found myself unable to produce a Dutch equivalent even from reference.
When writing it, I imagined Meneer Djikstra rolling his eyes a bit. Which... fits.
On Error Resume Next wasn't anything really terrible either. You just enable it before a risky call so your program won't crash, do the call, disable it and validate the result of the call before using it. This way you just don't have to clutter your code with endless try-catch-finally clauses.
It exists in tcl/tk as "update" and/or "update idletasks".
How is that in any way better than try/catch? It would be just as mush code, but without the syntax ensuring you disable it.
Yes, they are bad ideas. But back when those old COBOL people were on the job, it is what they had in their tool kit.
It is easy to make light of decades old ideas from a modern lamp. But wait, oh contemporary programmer, your day awaits to be mocked by someone who thinks they have the tiger by the tail.
https://eli.thegreenplace.net/2012/07/12/computed-goto-for-e...
On error resume next:
This can be scoped. So I’m reality it’s was used in small sub routines where errors needed to be discarded. I’m not suggesting it’s a particularly great idiom but this comes from an era before try / catch blocks.
DoEvents:
This was vital back when your language runtime was event driven but also single threaded. It was a way of allowing the application not to lock up (and thus the user force closing it) when there was computationally heavy workload. These days you’d stick that workload in a new thread for async await but you’d have all the same problems of ensuring that you’d disabled UI elements first as the article describes with DoEvents. And actually DoEvents is rather more clever because you’re telling a single threaded application that was typically running on a single core/CPU system exactly where the safe points are to prioritise other processes.
These days we have green threads, async/await and other concepts for lazy multitasking. But back when DoEvents was created there was no such thing and using real OS threads was very difficult (in fact it was officially unsupported in the languages that had DoEvents, though there were some unofficial jacks) so it was a real life saver in some scenarios.
Your “in reality” sounds a lot nicer than mine. In my experience “On Error Resume Next” was slapped right at the top of every single lovecraftian horror that was a VB6 file, like a prayer to gods who had clearly abandoned humanity.
Engineering inventory management? On error resume next. Departmental budget management? On error resume next. Telecom billing rectification? On error resume next.
Was there some sane way to use it? Sure, probably there was a team, somewhere, somehow, using this language and making safe, sane, and well-reasoned decisions on how to best take advantage of its features while avoiding its sharpest edges.
But the first thing I learned in this industry is that if you try to teach the average VB programmer to fish, they’ll have burnt down the village and strangled themselves in the line by lunchtime.
not just the application. with cooperative multitasking (eg windows 3.1) the whole computer would be unresponsive if you didn't give the event loop a chance to run.
Does this mean that this feature should not exist? Or that it may be misused?
It seems just a kind of rough Java 'try-final'. If you know that an access to disk, e.g., may fail 'resume on next' just guaratees that the rest of code is executed.
This would not be so wise in a language like C++ were you may have corrupted the stack. But, for basic it had its uses.
But often, if an error happens, the rest of the code will also be errors (or just be incorrect), so continuing to the next statement just masks the error, it doesn't do anything to handle it.
I don't remember what happens in case of a file access failure: is there a way to detect that there was an error? (Other comment below suggests yes, but it sounds rather brittle to have to check the Err object after every statement) But if so, why not just check for errors without 'on error resume next'.
At least with try-finally, you get to choose the scope of it, so you isolate the part where an error might occur that you wish to resume after.
The equivalent to try/catch in VB was On Error Goto <ErrorHandlerLabel> which when used properly was more or less fine.
Obviously there are some cases where it makes sense to ignore errors, but doing it unconditionally for all errors is horrible.
Here's how it might look in Python:
try:
f = open("foo.txt")
s = f.read()
condition FileNotExists as c:
handle UseStream(io.StringIO(""))
where when open fails to find a file, Python will look for the first FileNotExists condition handler, pass it a FileNotExists condition object, and then the handler can choose to handle the condition.On the open side, it could be something like this:
def open(filename):
restartable:
... low-level stuff to open the file ...
if failed:
signal FileNotExists(filename)
...
restart UseStream as r:
return r.streamIf you write on error resume next, you should revert that soon, but the language doesn’t prevent you from forgetting to do that. Neither, AFAIK, do typical editors. A good editor, IMO, would indent code inside a “on error resume next”, but I haven’t seen such editors.
It does help that its scope ends at function/procedure return (I think), but of course, that means seemingly benign refactoring can change program behavior.
It isn’t too bad when you write new code, but once you start maintaining a larger program, it can bite you.
A very simple startup or autoexec script for example, where it might be useful for the rest of the tasks to be done even if one of them fails. It assumes no dependencies between the tasks.
The "on error" construct existed in QuickBasic, but it took a goto line number or label as its operand. "ON ERROR RESUME n" would give you a relative line, allowing you to jump n lines down. Since Visual Basic didn't care about line numbers, "on error" took a goto label instead. Visual Basic 3.0 introduced the "on error resume next" construct in 1993 so that the QuickBasic usage could be imitated.
Meanwhile, already Haskell had Maybe/Option types and monadic notation in 1992.
Wat
> scheduled for removal.
Wat?
> scheduled for removal.
COBOL is still being developed?
- Implicit decay from arrays into pointers
- No checked arithmetic by default
- bounds checking placed on the shoulder of developers themselves
- strings composed of a pointer and a null terminator, that might exist, with luck
- implicit conversation from integer into nonexistent enumeration values
Unisys keeps selling Burroughs as ClearPath MCP, not only because of legacy mainframes, rather it is an originally written platform in 1961 with system programming languages (ESPOL/NEWP) that take security seriously (the first set of languages with unsafe code blocks and instrisics instead of Assembly), so there are still customers around that are willing to pay for that extra security.
https://www.unisys.com/offerings/clearpath-forward/clearpath...
And then there is the whole experience from ALGOL compilers in production,
"Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980, language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law."
-- C.A.R Hoare on his Turing award speech in 1981.
Circa 1998 ish I had to take a "Software Engineering" course in college. We had to use Visual Basic for our silly application, and only one of the 6 people in the group had any experience with VB at all.
The project grading rules dictated that each program crash (which typically was some modal dialog with a system error and an OK button) during final demo was an entire letter-grade reduction (!!).
Enter On Error Resume Next. No crashes, just keep going! Software doesn't exactly work properly/to specs? Whatever, that "costs less" points than crashing =)
> The problem is that you never know exactly what DoEvents() is going to do. Other parts of your application may receive Windows messages (say, if the user clicks somewhere else), and they can start running their code in the space that you create when you call DoEvents(). This sounds bad, but it’s actually a lot of fun (if you like late-night debugging), because the result is just a little bit different on every computer!
Oh, but if instead you introduce threads, you won't have any such problems, right?
Maybe you're supposed to change the state of your program to "calculation running" so that when UI events are processed in the middle of DoEvents, this state is observed.
If the calculation ran in a thread, you'd probably track that; the UI would know that the calculation is running and certain actions would be disabled or handled differently.
C's solution was to define int as at least 16 bits long (but assumed to be the native word size), and char as at least 8 bits long (though it might be stored in a 9-bit half-word on an 18-bit machine, or even a full word on another type of machine).
Certainly not. Any mirrors?
In Sinclair Basic though replacing the constant with the variable was potentially a significant efficiency win as the 10000 wasn't just stored as five bytes but rather 5 bytes plus six further bytes containing a floating point or integer representation of 10000.
Using GOTO X thus saved ten bytes in this case - less the assignment to X. If there were lots of GOTOs (or more likely GOSUBs) to a particular point in the code this was worth doing. On a machine with less than 10k of free memory (16k Spectrum) this was a worthwhile saving and so was used quite a bit in practice.
I assume it was only still in VB6 for backwards compatibility.
The author (and pretty much everyone else who never used Visual Basic) misunderstands how it's used. The next line is expected to check the Err object to see if there was an error. It was never expected to be used for just ignoring errors; it's a way to switch from "exception style" to "error code style" error handling.
It's true that it's a footgun because you can forget to check, but people misunderstand the intended usage.
I think this comes down to how you think about languages and features. Is a feature good if its intended use is that it be used carefully in limited circumstances and with careful error checking?
Or is it a terrible feature if in practice, the largely low-skill userbase slapped it everywhere and anywhere they could with absolute abandon, because it made the confusing error messages go away?
I think the author (and I) just fall in the latter camp. It doesn’t really matter that you weren’t “supposed” to use on error resume next except in limited scopes, and you were “supposed” to rigorously check the Err global afterwards.
In practice, it was abused non-stop as a crutch by people who largely didn’t know what they were doing, and it made touching a VB6 codebase hell on earth.
I’m not looking at the language in the abstract, but from the experience of having been handed maintainership of a few LOB VB6 apps early in my career. On error resume next’s consequences, how people actually used and abused it, are what matter, not whatever hypothetical nice way to use it existed in the docs and the maybe 1 in 50 VB programmers who approached the language with any knowledge or care.
It was a godawful feature that vanishingly few VB programmers ever used correctly, and it made working with VB projects an absolute nightmare.
I really don't think the author understands this, though; the quote I provided was pretty unequivocal. The author makes it pretty clear that he doesn't think there's any other way of using the feature. If he does, he made absolutely no mention of it. It's pretty misleading.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equa...
On Error Resume Next is _exactly_ how real life situations should be responded to. Instead of On Error Share On Socials.
Goto has some very valid use cases, and sometimes I want to throw a goto into my code to say, "Yes. I did that on purpose."
* Computed GOTO
* Arithmetic IF
* Indentation based syntax
* M-exprs
* The Glasgow Haskell Compiler (I was going to write "The only thing that hurts more than the Glasgow razor gangs", but that's too cheesy).
- Indentation-based syntax isn't a programming feature, it's a lexical/parsing style religion. In my religion, it looks much cleaner than extra punctuation, superfluous keywords, and the visual noise of curly braces. What's worse is special column flags.
C F90 sucks
C F95 still sucks
C F18 probably still sucksLooks great on the surface. Ride through transient failures. Easier load balancing. Even out load spikes. But underneath, you're adding bufferbloat to your system.
I've worked on systems where every service communicates with event queues. Debugging these things is hell. Problems with eventual consistency everywhere. Duplicate requests chilling in the queues where you can't see or get rid of them. Ordering issues. Bad messages/events clogging up queues. Upgrade nightmares whenever endpoints change.
Adding a message queue between your services instead of doing things synchronously is a great way to sabotage a project.
- Impure macros
- Macros so pure you can't do token pasting and templating expressively
https://en.wikipedia.org/wiki/Duff%27s_device
Please excuse me while I go barf.