3 ms·
Even without branching or loops, it's still a problem. Every line in a code block shares a namespace and state-space with each one another. If there's one very
by dcolkitt 7y ago
Even without branching or loops, it's still a problem. Every line in a code block shares a namespace and state-space with each one another. If there's one very consistent lesson in the history of software engineering, it's that state is a necessary evil to be minimized. Stateful code almost always is less reliable and harder to reason about than pure code. Small functions keep the surface area of stateful interactions strictly delineated by the arguments and return parameters.
The more lines of code that exist in a block, even with linear control flow, the exponentially higher chance that one line inadvertently clobbers something that subsequent line depends on. Consider a function like
def foo (msg: str):
head_index = find_header(msg)
... # 50 lines of code
log(msg, head_index)
... # 50 lines of code
return msg[head_index]
Now say somebody adds some chunk of logic in the middle
def foo (msg: str):
head_index = find_header(msg)
...
head_index += offset_digest(...) # Needed for log formatting
log(msg, head_index)
...
return msg[head_index]
A seemingly innocuous change to the function, has inadvertently clobbered the value of a variable we're relying on in a slightly different context. With 50 lines of code separating each reference to the variable, it's very easy for this bug to fly under the radar unnoticed.
An engineering culture that emphasized small functions avoids this problem, because the change would instead move the logic to its own self-contained function. That avoids the problem of new code accidentally clobbering the old code.
def foo (msg: str):
head_index = find_header(msg)
...
log_offset(msg, head_index)
... # 50 lines of code
return msg[head_index]
def log_offset (msg, head_index):
head_index += offset_digest(...)
log(msg, head_index)
- Silhouette 7y agoThe problem you describe is also fixed by any language that is explicit about stateful behaviour and effects. It's not an inherent property of having a long function, just of having insufficient control over mutable state (or poor discipline in using the available controls).
- dcolkitt 7y agoEven if you have strong mutability controls built into the type system, reasoning about correctly applying those constraints is far easier using small code blocks. If a variable is declared at the beginning of a block, its use has to be audited throughout the entire block. It's a lot easier to confirm that a variable is never mutated across 4 lines than it is across 200. When the size of a code block exceeds the capacity of working memory (about 8 chunks), then devs will skip the cognitive effort and fall back to the most permissive types by default. Not always, but bugs mostly come from the codebase at its worse, not its best. Considering your point from the other direction, the most mutability disciplined codebases tend to naturally bias towards short functions. In my experience most long functions are long, because the locally-scoped variables are being used to haphazardly track statefulness across the control flow. To contrast, Haskell's immutability and purity strongly restricts that pattern. Unsurprisingly, one of the most striking features of Haskell codebases are how short the functions tend to be compared to something like Java.
- Silhouette 7y agoIf a variable is declared at the beginning of a block, its use has to be audited throughout the entire block. Well, if the variable is declared at the beginning of the block and is scoped to the entire block, we have two possibilities. One is that the variable really is relevant throughout the block, or close to it. In that case, full mutability makes it more difficult to understand the behaviour, but if the variable is immutable or can only be mutated using explicitly controlled tools then the audit you mention is trivial. The other is that the variable is only relevant to some relatively small part of the block. In that case, it is possible that the function is trying to do more than one thing and that part of it could beneficially be separated into its own function, taking any variables that are only relevant to that part with it. It is also possible that the variable is being declared prematurely and starting its lifetime earlier than necessary, in which case it may be better to move the declaration later so its lifetime is shorter and the area where it is in scope is reduced. The latter cases do tend to lead to shorter functions, or at least reduced scopes, and for good reasons. However, what you can't do is magically take a mutable variable that is genuinely relevant throughout some algorithm that really is complicated enough to require 200 lines of code to describe it, say the current state of some fundamentally complicated state machine, and make that complexity go away just by factoring out smaller functions. You're still going to have to pass the variable or some equivalent around, and you're still going to need to mutate the state in lots of places. But now your logic for doing so is scattered across 50 4-line functions, and to establish the true behaviour you don't just have to audit one of them, you have to audit them all and all the code that connects them.