LOC is often a rough approximation for complexity. We once had an intern who made some useful things, but he didn’t know how to break anything down. One of them was a 1,000 line perl all as one function. I asked if it could be broken down into something more maintainable and he said no. There were several projects like this.
Knowing At a high level what needed to happen to accomplish what the code did, I know for a fact it could have and should have been broken down more. However, because of the complexity of what he wrote, no one had the patience to go through it. When he left, the code got thrown away.
While 10 LOC vs 50 LOC doesn’t matter, when commas enter the number, it’s a safe bet that things have gone off the rails.
After having this discussion in so many professional settings, I'm starting to think that this is just something that divides people. Maybe our brains are wired differently. What's important for me is that the call graph is simple. Second most important is that data structures are easy, or at least fits the problem. The amount of lines in a function is a distant third.
It could have been me behind one of those thousand line functions. Some tings lends itself naturally to this style, such as interpreter-like loops and imperative business logic. There's always that someone that argues that splitting up the code with function calls would somehow make it more "maintainable".
I'm reluctant to do it, because the code is identical, only non-linear which means harder to understand. Combine this with global symbols and it's a recipe for accidental complexity. This is so obvious that I for a long time thought people with the opposite opinion were just cargo-culting some OOP "best practice" mess which has doomed so many projects, but maybe they're just wired differently.
Do you write comments in your code? If you ever have a long function that like
....
// Here's where we apply coupon codes
...
It makes sense to me to break that out into it's own function, preferably one with no side effects that I can write a test for if I need to. I can give it a name related to the business logic.
Inevitably there will come a new special case where, for example, where Gold Members will get to use double coupon codes on Thursday. You'll start by looking in `checkout()` where you'll see either 1000 lines of other special cases, or maybe 30 lines with function calls like `foo =
applyCouponCode(shoppingCart, couponCode)`.
For me, it's easier to see where to start with the latter.
I think a better measure than simple LoCs is to cluster the local variables by use. If you get entirely separate clusters or large clusters that only share a few variables, then you should break them up at the cluster boundaries.
> Knowing At a high level what needed to happen to accomplish what the code did, I know for a fact it could have and should have been broken down more. However, because of the complexity of what he wrote, no one had the patience to go through it. When he left, the code got thrown away.
And today an LLM could probably have refactored it fairly well, automatically.
I worked closely with an engieer that would write these massive long functions; but they read clear and linear. They were really easy to follow and understand.
He'd of course develop functions and macros to avoid repeating themselves.
It just worked for his style and problem solving approach. He knew when to stretch a function. So, I'm convinced long functions can be done elegantly, it's probably uncommon.
> LOC is often a rough approximation for complexity.
Setting aside the anecdote, this statement is key. If you exclude deliberate efforts to game the LOC metric, LOC is closely correlated to complexity, however else you measure it (branch count, instruction count, variable count, rate of bugs, security vulnerabilities, etc). Unlike any other metrics however, LOC is dead simple to measure. Thus, it's an extremely useful metric, so long as you don't set up an incentive for devs to game it.
The whole point of a rule-of-thumb is to be dumb. There are very few good reasons for a 500 loc func, and the code reviewer won't want to verify it's as simple as you claim. You could still have a 50 loc func that's overly complex, and they'll complain about complexity instead of length.
One consideration is that about 40 loc will comfortably fit in one code split, and it's nice if one func will fit in 1-2 splits. Past that length, no matter how simple it is, the reader will have to start jumping around for declarations. That might be ok at first, but at some point the ratio of offscreen to onscreen code gets too big.
Sometimes I write long functions because I do not have time to write a shorter function. Eventually, if I have time, I go back and refactor a much shorter and tidier version. While later versions may be somewhat more efficient computationally, it is mostly a no-op beyond the aesthetics.
What is the LoC for that function? The first implementation or the final rewrite? They express the same thing.
What an arrogant, wordy, and partly wrong take on this matter.
No, LoC is not a dumb metric. In the same way that cognitive complexity, code coverage, and similar metrics aren't. CC is definitely not "superior" to LoC, it's just different.
These are all signals that point to a piece of code that might need further attention. It is then up to the developer to decide whether to do something about it, or not.
You know what's dumb? Being dogmatic, one way or the other. There are very few hard rules in software development—they're mostly tradeoffs. So whenever I hear someone claiming that X is always bad or always good, I try to poke holes in their argument, or just steer clear from their opinion.
It may be a bad metric, if you're being proscriptive, but it's a great heuristic. If I see a 500-line function where I'm not expecting one, I'm going to pay a lot more attention to it in the PR to try to figure out just why it isn't shorter.
Worth noting that "cognitive complexity" may mean SonarQube metric – a metric that is not widely recognized by industry, created by SonarQube employee as (imho failed) attempt to address "issues" with "cyclomatic complexity" (principled, intdustry recognised metric). It'll count things like nullish coalescing and optional chaning as +1 on your complexity which makes it unusable with jsx or ts code.
With low thresholds ("clean code" like low) both LoC and "cognitive complexity" (as in SQ) are bad measures that lit up in red large percentage of otherwise correct code in complex projects. The way people usually "solve" them is through naive copy pasting which doesn't reduce any cognitive load - it just scatters complexity around making it harder to reason about and modify in the future.
It's not the LoC I care about, it's the logical separation of concerns and testability. Large functions usually do many things which makes them really hard to test. Also, just mashing all the things in a single function is indicative of the author not having a clear picture of what problem he or she is dealing with.
I have been programming for 30 years and, while I don't consider myself a great programmer, I don't like large functions. In my experience they usually fail at clearly expressing intent and therefore make the code a lot harder to reason about. There are, of course, exceptions to this.
It's about structures. A long function is unfortunately "flat" at the first glance. Even if there are inherent structures, it usually burns a lot of brain power for a human to abstract structures out of "flat". Being flat is a more acute issue for LLM to understand. I think in the LLM era it's more important than ever to keep things short and structured.
> Locality is the design principle of grouping related things together. It’s a mental shortcut or heuristic that allows the mind to assume a higher degree of independence from the code block being read, easing the mental burden. Spreading code across your codebase diminishes locality.
> Linearity is the idea of having instructions be read one after the other, from top to bottom, just like how code is executed. Having to jump back and forth from function call to function call reduces linearity.
A personal guideline for a lot of stuff is that a function may be too long when people add comments to mark what sections of it do. (ofc not really a hard rule).
I just think it's easier to see "oh this is calling the load_some_stuff function, which I can easily see returns some data from a file." Rather than <100 lines of stuff, inlined in a big function, that you have to scan through to realize it loads some stuff and/or find the comment saying it loads some stuff>.
That is to say, descriptive functions names are easier to read than large chunks of code!
smaller functions are also usually easier to test :shrug:
Love the article thanks. to me it reads really reasonably. i work in a verbose language (C) and there its too easy to try and optimise things away to 'save typing' to a point it becomes actually more of a burden than an optimization.
Some good advices in here on what balance to strike with some clear examples. Always a good reminder :). thanks for the writeup!
The winning feature of LoC as a metric is that it can be "eye-balled" easily without tools. Cyclomatic complexity is a more interesting metric; however, it's not the easy, quick, and rather dirty spitball that Lines of Code is.
My feelings on LoC really depend on what you want to measure. LoC tends to correlate with # bugs. A 100-line function is probably going to have more bugs than a 20-line function that does the same thing.
I don’t think functions inherently need to be small though. 5 20-line functions are just as buggy as that 1 100-line function. Bugs scale with LoC, not how well you’ve broken functions apart.
The lesson is really just to avoid overcomplicating things. Use less LoC if you can. You should also avoid overly-shortening or cleverness but it tends to be the lesser evil.
25 comments
[ 3.2 ms ] story [ 61.5 ms ] threadKnowing At a high level what needed to happen to accomplish what the code did, I know for a fact it could have and should have been broken down more. However, because of the complexity of what he wrote, no one had the patience to go through it. When he left, the code got thrown away.
While 10 LOC vs 50 LOC doesn’t matter, when commas enter the number, it’s a safe bet that things have gone off the rails.
It could have been me behind one of those thousand line functions. Some tings lends itself naturally to this style, such as interpreter-like loops and imperative business logic. There's always that someone that argues that splitting up the code with function calls would somehow make it more "maintainable".
I'm reluctant to do it, because the code is identical, only non-linear which means harder to understand. Combine this with global symbols and it's a recipe for accidental complexity. This is so obvious that I for a long time thought people with the opposite opinion were just cargo-culting some OOP "best practice" mess which has doomed so many projects, but maybe they're just wired differently.
Inevitably there will come a new special case where, for example, where Gold Members will get to use double coupon codes on Thursday. You'll start by looking in `checkout()` where you'll see either 1000 lines of other special cases, or maybe 30 lines with function calls like `foo = applyCouponCode(shoppingCart, couponCode)`.
For me, it's easier to see where to start with the latter.
And today an LLM could probably have refactored it fairly well, automatically.
He'd of course develop functions and macros to avoid repeating themselves.
It just worked for his style and problem solving approach. He knew when to stretch a function. So, I'm convinced long functions can be done elegantly, it's probably uncommon.
Setting aside the anecdote, this statement is key. If you exclude deliberate efforts to game the LOC metric, LOC is closely correlated to complexity, however else you measure it (branch count, instruction count, variable count, rate of bugs, security vulnerabilities, etc). Unlike any other metrics however, LOC is dead simple to measure. Thus, it's an extremely useful metric, so long as you don't set up an incentive for devs to game it.
Ironic way to end an article that repeatedly belittles the target audience.
https://www.youtube.com/watch?v=bmSAYlu0NcY
which is for the book:
https://www.goodreads.com/book/show/39996759-a-philosophy-of...
One consideration is that about 40 loc will comfortably fit in one code split, and it's nice if one func will fit in 1-2 splits. Past that length, no matter how simple it is, the reader will have to start jumping around for declarations. That might be ok at first, but at some point the ratio of offscreen to onscreen code gets too big.
What is the LoC for that function? The first implementation or the final rewrite? They express the same thing.
No, LoC is not a dumb metric. In the same way that cognitive complexity, code coverage, and similar metrics aren't. CC is definitely not "superior" to LoC, it's just different.
These are all signals that point to a piece of code that might need further attention. It is then up to the developer to decide whether to do something about it, or not.
You know what's dumb? Being dogmatic, one way or the other. There are very few hard rules in software development—they're mostly tradeoffs. So whenever I hear someone claiming that X is always bad or always good, I try to poke holes in their argument, or just steer clear from their opinion.
With low thresholds ("clean code" like low) both LoC and "cognitive complexity" (as in SQ) are bad measures that lit up in red large percentage of otherwise correct code in complex projects. The way people usually "solve" them is through naive copy pasting which doesn't reduce any cognitive load - it just scatters complexity around making it harder to reason about and modify in the future.
I have been programming for 30 years and, while I don't consider myself a great programmer, I don't like large functions. In my experience they usually fail at clearly expressing intent and therefore make the code a lot harder to reason about. There are, of course, exceptions to this.
> Linearity is the idea of having instructions be read one after the other, from top to bottom, just like how code is executed. Having to jump back and forth from function call to function call reduces linearity.
1000 times yes.
smaller functions are also usually easier to test :shrug:
Some good advices in here on what balance to strike with some clear examples. Always a good reminder :). thanks for the writeup!
I don’t think functions inherently need to be small though. 5 20-line functions are just as buggy as that 1 100-line function. Bugs scale with LoC, not how well you’ve broken functions apart.
The lesson is really just to avoid overcomplicating things. Use less LoC if you can. You should also avoid overly-shortening or cleverness but it tends to be the lesser evil.