Control flow is one of those things that may seem simple at first, but later one may see the messy bits. Or not, because pretty much everything in programming is controversial.
Newbie (or simply less careful) programmers tend to write the code that would only work in perfect conditions at best, since covering all the possibilities seems to be overwhelming and excessive. And then things don't work as they should.
Programming languages and libraries tend to encourage this: usually the most straightforward way to define a program is to define its behaviour in the most desired/expected/perfect conditions, optionally covering the others. In dependently typed languages, there are attempts to assist verification, though not necessarily to make it easier to define correct programs than incorrect ones.
Some methods to handle the situations that are different from the most desirable path in a flowchart:
exit
call with an
appropriate code and a message in stderr
would be more
suitable for that.
NULL
, -1
,
Maybe a
, Either a b
,
(err, a)
, Bool
. They may force a programmer to
do something meaningful with the result, but that only works as long as
a return value can't be ignored altogether. When it comes to I/O
actions, which are the most error-prone, many of them are executed
solely for side effects, and not to get any return value – so a
programmer could still easily ignore misbehaviour, though Rust would
warn if a Result
value is not handled, and GHC
with -Wall
would warn if it's not discarded explicitly. And
as with the Except monad, no way to throw them to a different thread, or
do other magic things (though maybe it's rather good than bad).
assert
, panic
goto
. I think it can be
quite similar if it's sprinkled inside nested branches, or in some
branches and not in others. It can help to reduce the code, but the same
can be said about goto
. It is unnecessary and can be
confusing, so perhaps better to avoid.
An unpleasant thing in languages with built-in exceptions is that those
exceptions could pop up just anywhere. Even if one prefers a different
approach (such as ExceptT
), chances are that other approaches
are used in the used libraries, so one ends up dealing with every
available kind of branching, possibly wrapping it into something unified
along the way. There even are libraries to assist that, such as errors.
Probably one of the reasons of this rather unpleasant situation is that a correct control flow can be represented with a directed graph, a flowchart, while it is tempting/easier to write and see programs as mere sequences of actions; even in dot it's not that handy to describe flowcharts. State machines (and Mealy machines in particular) may be more suitable to describe control flow precisely, while being relatively handy to edit as text. Event-driven architectures seem to be nicer in general, though they may lead to relatively verbose and/or error-prone code.
Flowcharts and state diagrams are nice tools for control flow visualisation; I rather like plain ASCII ones (similar to those used in RFCs), which can be easily embedded in comments and any other common kind of software documentation. Unfortunately the resulting graphs for non-trivial programs are not planar, so those diagrams become tricky to follow.