Pretty neat. Three things that occurred to me while reading it.
(1) Makes it pretty clear why exceptions are even more of a control-flow nightmare than goto. What a messy graph.
(2) The 'defer' diagram is a big WTF. Couldn't even figure out which box is supposed to represent which statement in the example.
(3) I'd love to see a graph of the control flow when you throw in a few spurious lambdas like "advanced" C++ programmers tend to do (and similar in a few other languages). Might even make exceptions look good.
My takeaway was the exact opposite of yours. For example the goto graph seems very intuitive and simple. But it is much more complex to reason about compared to exceptions. My takeaway was that simple graphs like these can't really explain the pros and cons of high level programming constructs.
The tradeoff in goto-vs-exception is that a goto needs an explicit label, while an exception allows the destination to be unnamed, constrained only by the callstack at the site where it's raised.
That makes exceptions fall more towards the "easy-to-write, hard-to-read" side of things; implied side-effects make your code slim in the present, treacherous as combinatorial elements increase. With error codes you pay a linear cost for every error, which implicitly discourages letting things get out of hand, but adds a hard restriction on flow. With goto, because so little is assumed, there are costs both ways: boilerplate to specify the destination, and unconstrained possibilities for flow.
Jumping backwards is the primary sin associated with goto, since it immediately makes the code's past and future behaviors interdependent. There are definitely cases where exceptions feel necessary, but I believe most uses could be replaced with a "only jump forward" restricted goto.
It's even clearer when you consider higher-order functions. These diagrams are only able to represent functions of order at most 2, i.e. what is called "dependency injection".
My impression was that the article made the exceptions graph much more complicated than it needed to be. Each function doesn’t need three boxes, and they don’t need to each throw exceptions twice. IMO exceptions are much cleaner in practice and are great for things like “kill this script” or “throw a 500 error”.
RE (2): Defer. The diagram has an extra box in the control flow which is unnecessary. You can label the big boxes from top to bottom like so: A E B <unnecessary> D C.
A good use case for understanding the control flow of 'defer' is when cleaning up resources.
mutex.lock();
defer { mutex.unlock(); }
val f = File.open(filename);
defer { f.close(); }
// write to file
It's a nice construct because it keeps construction and destruction close together instead of far apart
mutex.lock();
val f = File.open(filename);
// write to file
f.close();
mutex.unlock();
Unfortunately, many do. When that includes popular libraries and frameworks, "abuse" becomes idiomatic or even strictly necessary. When it's in the language itself, such as Python's use of KeyError or StopIteration, that's even more true. Since better alternatives exist for most exception use cases, I'm of the opinion that direct language support was a bad idea. A more general and explicit cousin of signal handlers would suffice for those few "stop the world" cases, and can be done via library functions. Optional types, multiple return values, or plain old error codes (I like Zig's "non-ignorable error code" approach) suffice for the rest.
This sort of formalism is bad for handling multiple returns and thus exceptions. For instance, they become very simple in continuation passing style whereas GOTO is difficult.
Flowcharts feel like a simple and universal way of talking about, well, control flow, but they imply a certain kind of bias toward a kind of reasoning which isn't necessarily all that better than code itself.
> (2) The 'defer' diagram is a big WTF. Couldn't even figure out which box is supposed to represent which statement in the example.
I found the defer diagram very accurate and understandable, but I already knew what "defer" does.
From the preceding examples, I understand that the little box is an instruction the developer didn't write, but was added automatically. From the graph, I understand that the instructions are executed in a different order from what it's written by the developer (and exactly in reverse order).
There's actually a fairly natural way of representing Result-like types (including exceptions) in a flow chart. Draw multiple "exit" terminals for the function, one for "success" and one for each error case. Then a function invocation can have multiple flowlines leading from it, one for each case. And one can "rethrow" the exception quite naturally, by having a flowline for a "rethrown" exception lead to an exit terminal of its own. This also applies by analogy to the case of multiple entry terminals, which come up most naturally when desugaring async functions.
(It's actually even simpler than that when accounting for the functor and monad properties of Result<T, Err>, but the graphical representation for a "functor" or "monad" is a bit more complex; these would be drawn as labeled regions, and functor and monad properties would describe how such regions can be "merged" or "combined".)
What are you even talking about? Are you replying to the wrong post? I don't even know how to play Go. I prefer Othello. It's simple and convenient. Anyway, didn't Dijkstra write a paper warning "Go Considered Harmful"?
(1) Makes it pretty clear why exceptions are even more of a control-flow nightmare than goto. What a messy graph.
(2) The 'defer' diagram is a big WTF. Couldn't even figure out which box is supposed to represent which statement in the example.
(3) I'd love to see a graph of the control flow when you throw in a few spurious lambdas like "advanced" C++ programmers tend to do (and similar in a few other languages). Might even make exceptions look good.