The “Bug-O” Notation
overreacted.io
overreacted.io
But this is precisely what people mean when they say it's ugly.
"Ugly" is what this gets translated as by your limbic system which registers in an instant: "fuck this big mess and the work it's going to be to reason about."
Aesthetics and usability are deeply entangled here, and the problem with that code is absolutely an aesthetic one. I want to say, more so. Because I experience it that way first, and only later do I enumerate the measurable reasons it's likely to be more buggy because of it.
WHY do you not like some code? To avoid pointless discussions about taste and gut feelings, I'd bring it down to falsifiable statements.
"This is hard to maintain, for reasons X and Y" is such a statement. As is: "This is hard to test for reason: It needlessly engages in combinatorial explosion" is another.
Yes, you could shorten that to 'ugly'. Good for you.
Nevertheless, I've seen tons of discussions referring to 'ugly' code where the reasons were solid but completely different from maintainability (for example: "It is longer than needed", "It does is not idiomatic for this language"), or even not particularly solid ("It is ugly because it isn't functional", "It is ugly because the style guide says all APIs should always be interfaces", "It is ugly because I see tabs, not spaces").
> Or it could look “clean” but have a bad Bug-O.
I think this is possible but in practice rare. I'd be curious to see an example of code you think falls into this bucket.
That said, I'd probably write something closer to this (once again, not sure if this is even valid, but you can just treat it as pseudocode):
// don't create these every time, we're just changing their visibility
let retryButton = createRetryButton(); // calls trySubmit() when clicked...
let successMessage = createSuccessMessage();
let spinner = createSpinner();
let errorMessage = createErrorMessage();
...
function trySubmit() {
// assume these don't cause errors if the child isn't already there
formStatus.removeChild(errorMessage);
formStatus.removeChild(retryButton);
formStatus.appendChild(spinner);
succ = submitForm();
formStatus.removeChild(spinner);
if(succ)
formStatus.appendChild(successMessage);
else {
errorMessage.setMessage(error);
formStatus.appendChild(errorMessage);
formStatus.appendChild(retryButton)
}
}My second example (that doesn’t have this problem) is pretty much what you wrote.
This isn't because it's good code. It's because it's code that fits in a blog post, so of course you understand it quickly. If you didn't understand it quickly, the author's point couldn't be conveyed properly.
The question isn't really what happens when you have 10 lines of dodgy code. The question is, what happens when you have 10,000 lines of dodgy code?
(In a classroom, on a chalkboard, bubble sort isn't any slower than any other sort, and somewhat easier to draw, honestly. Quicksort is rather a lot of stuff to draw. But unless your code solely deals with lists small enough to fit comfortably on a blackboard with work room to spare, you are well advised not to use bubblesort.)
Imagine adding an additional state to the Bug(n!) version of the author's trySumbit() function, perhaps splitting the error state in to two different error messages based on what goes wrong. You would have to pay close attention to how you got to the state you are in in order to make sure you manipulated the DOM properly, and it would be hard to know you got it right. You would have to consider all the state changes into and out of your new state, figure out which ones are impossible, and account for all the ones that could happen.
If you want to add a state change to the Bug(n) example, all you have to do is add an an entry to the switch case without worrying about how you got there or how you leave.
formStatus.appendChild(spinner);
succ = submitForm();
formStatus.removeChild(spinner);
You're solving an easier problem: synchronous form submission.The code in the article (which is Javascript, the "J" in "AJAX"; although there's some stuff in the final example which I assume is "JSX") is solving asynchronous form submission (the "A" in "AJAX").
In general, Javascript programs work by attaching handlers to events. Events happen asynchronously, i.e. triggering an event like `submitForm` will push that event (or, equivalently, its handlers) to a queue, then carry on with the next instruction. Blocks of Javascript code, like event handlers, are also guaranteed to run from start to finish before anything else happens.
This means that your algorithm cannot work, even as pseudocode. Calling `submitForm()` will simply queue up that event. We won't be given the result of the form submission (which you call `succ`) since, according to the semantics, the form hasn't actually been submitted yet. This breaks all of the subsequent code.
What about faking it? We could try setting `succ = null`, and use an event handler on the form submission to overwrite this value. Then we can put a loop like `while (succ == null) { sleep(1); }` to wait until that handler gets run. Except Javascript doesn't have `sleep`, so we'd need to use a busy-loop instead like `while (succ == null) {}`. That will use 100% CPU while we're waiting, which is pretty horrible. Yet it also won't work! Remember that Javascript executes one block of code (like an event handler) before it starts on another (this is important to prevent horrible race conditions). In this case, the value of `succ` will never get overwritten, since the event handler can't be run until the current block has finished; yet the current block will never finish, since it's running an infinite loop!
Also, when I say that nothing else happens until a code block has finished, this includes interactions with the page. Hence even if it were possible to implement your algorithm in Javascript (which, as detailed above, I think is impossible), the entire page would freeze while we're waiting for the form submission to take place (possibly including the spinner, which defeats the whole point of having one). In browsers which don't use separate processes per tab, this might make the whole browser freeze.
Also note that your code still doesn't deal with the possibility of multiple executions, which is what the whole post was about. Even if it were possible to make your algorithm run in a browser, and even if that didn't cause the whole browser to hang, clicking the button multiple times would still cause multiple spinners to appear, which is exactly the problem that the original post was trying to avoid ;)
[1] https://en.wikipedia.org/wiki/Continuation-passing_style
If anyone know other related topics, please share them, thanks!
At least for me.
I use a descendant of gometalinter on all my Go code, and one of the things it ships with is cyclomatic complexity, and I always turn it off. Precisely because I tend to think like this, it often gives my code terribly wrong ratings because it can't see that there's actually only a finite set of paths through the code that are possible, and also as the blog post demonstrates, sometimes adding more code actually simplifies the conceptual flow. But cyclomatic complexity will generally say the "more code" is even more cyclomatically complex.
There's also some Go-specific elements to my distaste for the measure, though; since in Go right now handling an error is automatically an if statement, that'll hit you right in the metrics. But in many cases,
something, err := GetSomething()
if err != nil {
return errwrap.Wrapf("while trying to get something: {{err}}",
err)
}
Officially that's an if statement; unofficially, it ought to be a cyclomatic complexity of zero. Now, it isn't technically a free if clause, in the sense that a bug could live there, but if you're going to count the exception-based equivalent as zero (and it has the same callout; bugs can lie in your exception handling exactly the same way), then this ought to be practically zero too. Consequently, Go functions tend to get smacked with much higher complexity numbers than exception-equivalent code. Yeah, it's probably not a one-to-one, but it's certainly not as lopsided as the metric makes it seem.I unfortunately can't find it, but I remember there being a study (by Microsoft?) of defect density for projects with different methodologies (scrum, TDD, whatever), which found lines of code is more correlated with defect rate than everything else they tried. They took that as a failure; I've always taken that to mean that reducing lines of code is the most important first step (or probably more accurately reducing tokens of executable code; one liners don't help and type annotations don't hurt).
While attempting to find that study, I found a study [1] that claims to have found an empirical sweet spot in defect density by module size - apparently 400 lines for assembly modules or 200 lines for ADA modules. Those are some weird languages to have numbers on, but maybe something else in that paper's family tree has something more relatable.
- how did this (wrong) control flow get here? Normally simple with stack traces, may be more complex with event systems. Part of the original rationale against GOTO.
- how did this (wrong) data item or variable get here?
- how did this piece of state get into this (wrong) state? This is often very hard to answer if the state is outside the program. DOMs are a large piece of global state that everyone working in the browser environment has to deal with.
Now... When your callbacks are names and not inline you can't really make that assumption.
I am talking about inherent characteristics of a particular API and how the patterns it encourages influences debugging.
For example:
>In fact, if any value looks wrong in the DOM of a React app, you can trace where it comes from by looking at the code of components above it in the React tree one by one. No matter the app size, tracing a rendered value is (tree height).
Basically, the fewer things you need to know about in a given context, the less likely you are too mess it up.
An advice that is valid since forever.
Really what you’re doing is preferring more declarative programming practices over procedural. And as a consequence, more declarative APIs like React.
What happens internally in React is another question. But the point of React abstraction is to let _you_ think in terms of “render from scratch”.
I tried to write software like that 10 years ago already, but I always failed at performance.
React was just what I was missing.
How many states can a selection of random, non-expert humans interacting with your code across a bevy of platforms encounter? The answer is always: Infinite.
Hand your application to a bunch of people that are completely ignorant of your intentions and it will reveal a seemingly endless bevy of code paths you didn't think we're possible! It will also reveal entire categories of code paths that have yet to exist but should!
In my TODO list I have a book idea... All I have is the title, "ERROR: This should never happen."
If I were going to write an article about maddening complexity in code that seems like it should be straightforward I too would choose JavaScript for my examples!