
The “Bug-O” Notation - danabramov
https://overreacted.io/the-bug-o-notation/
======
jonahx
> The problem with this code isn’t that it’s “ugly”. We’re not talking about
> aesthetics. The problem is that if there is a bug in this code, I don’t know
> where to start looking.

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.

~~~
rzwitserloot
Ugly, given that it is an emotional word, applies to any code that you don't
like.

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").

~~~
cloverich
In a sense those are all finer grained extensions of the same concept: How
hard it is to reason about the code. On the highest level are easily
quantifiable things like wrapping several error producing segments in a single
try / catch block, obscuring the source of an error. Use of interfaces makes
documentation and refactoring easy. Then the lowest level stuff is pure style
-- much more subjective, but meaningful in a team environment. When people
write code in the same style, you spend less time parsing style differences
and more times parsing the actual semantics.

------
userbinator
I'm not familiar with the language, and yet I remain unconvinced because I
could still understand what the code in the first example is doing, and more
importantly, _see it all at a glance_. I don't have to jump around between
functions to follow the logic. More verbose code means more opportunity for
errors and more code to look through and debug.

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)
         }
        }

~~~
chriswarbo
Thanks for sticking your neck out and providing code to back up your argument!
I'm going to refute it quite heavily below, and just wanted to clarify that
it's all down to the particular semantics of Javascript and browser form APIs.
Since you say you're not familiar with the language, it's absolutely fine that
you wouldn't have known this, so this isn't a personal attack against you;
just a clarification for you and hopefully others about why this wouldn't work
:)

    
    
        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 ;)

~~~
rcfox
You could have been more generous and assumed they meant `succ = await
submitForm();`

~~~
chriswarbo
I've actually never used any async/await stuff, so I didn't know that's what
it's for. All that stuff appeared in JS (and elsewhere) after I stopped doing
Web dev. These days I mostly write Haskell, which AFAIK doesn't need all of
that async/await/promise stuff because its already covered by laziness.

~~~
rcfox
`await` is essentially syntactic sugar that means "wrap everything after this
in a callback to be executed after this function does something
asynchronously."

~~~
chriswarbo
I see. That would presumably still have the problem of multiple spinners, as I
mentioned at the end of my original comment?

~~~
rcfox
Yes. You'd want to guard against this running more then once, probably by
disabling the button until the submit finished.

------
qlk1123
This is a good attempt to kickstart the discussion of some quantitative metric
about bugs and the time one needs to deal with it, but I wonder if there
wasn't any software engineering theories or practices to address this.

If anyone know other related topics, please share them, thanks!

~~~
jakear
This is somewhat similar to the concept of “Cyclomatic Complexity”, which is
the number of independent paths though a piece of code, and can be statically
analyzed.

[https://en.m.wikipedia.org/wiki/Cyclomatic_complexity](https://en.m.wikipedia.org/wiki/Cyclomatic_complexity)

~~~
jerf
I like this concept better than cyclomatic complexity, precisely because this
is what cyclomatic complexity is trying to poorly approximate. But it can't be
automatically derived in the general case, or even terribly easily in non-
general cases, either, unlike cyclomatic complexity.

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.

~~~
sigil
Does gometalinter report absolute cyclomatic complexity numbers, or does it
report density (cyclomatic complexity / sloc)?

------
pjc50
So what I think this points to is the importance of what I'm going to call
"psychological reversibility" in programming: the ability for the developer
(with the aid of their tools) to answer three kinds of question:

\- 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.

------
balena
[https://en.m.wikipedia.org/wiki/Cyclomatic_complexity](https://en.m.wikipedia.org/wiki/Cyclomatic_complexity)

~~~
bcaa7f3a8bbc
There is a GCC plugin that can automatically calculate the cyclomatic
complexity for programmers, and it has been built into the Linux kernel source
tree. I don't know if there is any public statistics, but it would sound great
for code reviewers.

------
PezzaDev
After seeing callbacks cause a lot of "hard to debug" scenarios for me and for
others, I'm starting to believe they are gotos in disguise.

~~~
pjc50
Even worse: it's a "come from", a statement normally only seen in INTERCAL.

~~~
rbanffy
At least "come from" states clearly where the code is coming from.

Now... When your callbacks are names and not inline you can't really make that
assumption.

------
jkravitz61
This notation is already taken in the computer science world and is used in
robotics planning algorithms.
[https://www.cs.cmu.edu/~motionplanning/lecture/Chap2-Bug-
Alg...](https://www.cs.cmu.edu/~motionplanning/lecture/Chap2-Bug-
Alg_howie.pdf)

------
lioeters
Otherwise known as cyclomatic complexity?

~~~
danabramov
Cyclomatic complexity refers to a specific code snippet.

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)._

~~~
beatgammit
Really, all of this can be pared down to: reduce mutable state. That's what
React does, and it's why code written in functional languages like Haskell
tend to be much easier to reason about.

Basically, the fewer things you need to know about in a given context, the
less likely you are too mess it up.

------
j2kun
Cute, but it's still just asymptotic complexity... You can use asymptotic
notation (big-O) to describe _anything_ that grows. It doesn't have to be code
runtime.

------
8lall0
tl;dr avoid too much nesting, prevent DRY and make small but useful functions.

An advice that is valid since forever.

~~~
danabramov
You can split the first example in small functions and it'll still be a mess.
The post is not about duplication, function size, or nesting. We gained
predictability by preventing _accumulation_ of errors — which we got by always
restarting rendering from scratch.

~~~
dyarosla
Well- the React example doesn’t really restart rendering from scratch (as you
yourself mentioned). And in larger applications you’d still prefer to not
‘redraw from scratch’ in React either and build on top of a previous setup.

Really what you’re doing is preferring more declarative programming practices
over procedural. And as a consequence, more declarative APIs like React.

~~~
danabramov
For the app developer, React code _does_ restart rendering from scratch. You
have no access to the past rendered state from the render method (at least if
you don’t intentionally escape the declarative model). This isn’t true only
for bugs in 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”.

------
titanix2
Funny how the post is about bug complexity, says that "Some API and language
designs make a whole class of mistakes impossible. Some create endless
problems." and then proceed to an example in Javascript... for which a whole
language (Typescript) was invented precisely because it does not scales well.

~~~
danabramov
My audience is JavaScript developers.

