I was enjoying what I was reading until the "Limit function length" part which made me jolt out of my chair.
This is a common misconception.
Limit function length: Keep functions concise, ideally under 70 lines. Shorter functions are easier to understand, test, and debug. They promote single responsibility, where each function does one thing well, leading to a more modular and maintainable codebase.
Say, you have a process that is single threaded and does a lot of stuff that has to happen step by step.
New dev comes in; and starts splitting everything it does in 12 functions, because, _a function, should do one thing!_ Even better, they start putting stuff in various files because the files are getting too long.
Now you have 12 functions, scattered over multiple packages, and the order of things is all confused, you have to debug through to see where it goes. They're used exactly once, and they're only used as part of a long process. You've just increased the cognitive load of dealing with your product by a factor of 12. It's downright malignant.
Code should be split so that state is isolated, and business processes (intellectual property) is also self contained and testable. But don't buy into this "70 lines" rule. It makes no sense. 70 lines of python isn't the same as 70 lines of C, for starters. If code is sequential, and always running in that order and it reads like a long script; that's because it is!
Focus on separating pure code from stateful code, that's the key to large maintainable software! And choose composability over inheritance. These things weren't clear to me the first 10 years, but after 30 years, I've made those conclusions. I hope other old-timers can chime in on this.
The length of functions in terms of line count has absolutely nothing to do with "a more modular and maintainable codebase", as explained in the manifesto.
Just like "I committed 3,000 lines of code yesterday" has nothing to do with productivity. And a red car doesn't go faster.
“Ideally under 70 lines” is not “always under 70 lines under pain of death”.
It’s a guideline. There are exceptions. Most randomly-selected 100-line functions in the wild would probably benefit from being four 25-line functions. But many wouldn’t. Maturity is knowing when the guideline doesn’t apply. But if you find yourself constantly writing a lot of long functions, it’s a good signal something is off.
Sure, language matters. Domain matters too. Pick a number other than 70 if you’re using a verbose language like golang. Pick a number less if you’re using something more concise.
People need to stop freaking out over reasonable, well-intentioned guidelines as if they’re inviolable rules. 150 is way too many for almost all functions in mainstream languages. 20 would need to be violated way too often to be a useful rule of thumb.
As with all guidelines, some people will turn it into a hard rule, and build a linter to enforce it. Then they will split long functions into shorter ones, but with a lot of arguments. And then their other linter that limits argument count will kick in.
And someone else will use the idea that this is a misconception to justify putting hundreds of lines in one function.
Some junior dev will then come along with his specific user story and, not wanting to tear the whole thing up, will insert his couple of lines. Repeat over a 10 year lifecycle and it becomes completely unmanageable.
I remember trying to tease apart hundreds of lines of functions in a language which didn't have the IDE support we have these days. It was always painful.
Even the most script like functions I've ever worked with benefit from comments like:
# Validate that the payment was successfully sent.
These subheadings can just as easily become functions. If you limit the context in each function it becomes a genuine improvement.
When I was a junior developer working in the outsourcing industry, I regularly had to maintain projects where the entire codebase was in a few major functions. It was a stressful nightmare to debug - such projects destroyed my health. No feature could be developed without extensive forking of the codebase and prototyping to see what works and what doesn't.
After several such bad experiences, I religiously incorporated my personal guideline of functions never crossing the upper limit of 100 lines - with some 1 in 10k exceptions like in the parsing domain or state machine dispatch. Short functions also heavily aid in unit testing.
The only folks who believe in big functions are developers who haven't been exposed to large quantities of other people's shoddy code as a significant fraction of their working career. Once you have been through several such nightmares, you will worship small functions.
That’s why linters allow you to suppress specific rules for a single statement (and ideally let you write a short comment that explains why you chose to suppress it.)
At this point we're suggesting that we shouldn't implement some guidelines because there exist developers who will naively take it to extremes and stubbornly refuse to budge. Those developers will be an issue practically independent of the guidelines themselves.
It's using a parameter of secondary importance as a primary, so it's wrong with any number. The comment even has a helpful analogy to LOCs. People need to stop freaking out over reasonable, well-intentioned criticism of guidelines as if they were arguing something else like misundertstanding of that strictness of rules.
Until we have an absolute metric to use for measuring comprehensibility, composability, and number of high-level concepts addressed by some unit of code, highly-correlated secondary metrics will have to suffice.
I would love for you to show me a high-quality code base where even 5% of functions exceed 70 lines.
I for sure haven’t seen one. Most of the best, easiest to work on, and stablest projects I’ve worked in my 25-year career have had the overwhelming majority under 30 LOC, and maybe 1% over 70 LOC.
As with all smells, yes, they are simply correlated symptoms of deeper issues and not necessarily proof positive. But they are correlated. Pretending they aren’t is nonsense.
And yes, arbitrarily chopping things up to quiet a linter doesn’t necessarily solve the problem, much in the same way dumping perfume onto rotten eggs doesn’t fix the underlying issue. It’s a fast and easy warning to alert you to potential issues, but at the end of the day it’s still up to developers to exercise good judgment and development practice.
I don't understand what you expect here - you're treating these metrics as a sure undeniable sign of quality, so by definition the examples won't be high quality.
So of course you "for sure haven’t seen one", what kind of circular logic is that?
> Pretending they aren’t
But you're the one doing the pretending, making up high correlation with nothing to correlate, and coming to circular conclusions (if you define it as smell, then yes, you aren't taking about roses)
Just chiming in here to say, absolutely you should keep functions small and doing one thing. Any junior reading this should go and read the pragmatic programmer.
Of course a function can be refactored in a wrongheaded way as you’ve suggested, but that’s true of any coding - there is taste.
The ideal of refactoring such a function you describe would be to make it more readable, not less. The whole point of modules is so you don’t have to hold in your head the detail they contain.
Long functions are in general a very bad idea. They don’t fit on a single screen, so to understand them you end up scrolling up and down. It’s hard to follow the state, because more things happen and there is more state as the function needs more parameters and intermediate variables. They’re far more likely to lead to complecting (see Rich Hickey) and intertwining different processes. Most importantly, for an inexperienced dev it increases the chance of a big ball of mud, eg a huge switch statement with inline code rather than a series of higher level abstractions that can be considered in isolation.
I don’t think years worked is an indicator of anything, but I’ve been coding for nearly 40 years FWIW.
> They don’t fit on a single screen, so to understand them you end up scrolling up and down.
Split them up into multiple functions and there is more scrolling, and now also jumping around because the functions could be anywhere.
> It’s hard to follow the state, because more things happen
It's easier to follow state, because state is encapsulated in one function, not constantly passed around to multiple.
> a huge switch statement with inline code
Huge switch statements are a common reason for large functions. Python has this architecture and largely won because the interpreter is surprisingly easy to understand and extend.
> Split them up into multiple functions and there is more scrolling, and now also jumping around because the functions could be anywhere.
No, this is the fundamental misconception used in defense of longer functions.
With a long function, you must scroll no matter what. If you're interested in every detail, you scroll. If you want to figure out the logical steps involved, you scroll (and likely have to take notes as you go). If you want to ignore a step for now, you scroll over it.
With properly factored code, you only jump away when you need to. If you want to figure out the logical steps involved, you just read them off. If you want to ignore a step for now, you just ignore it. If you're interested in every detail, you get to start with an overview, and then you jump around. But caring about details is the exceptional case. Most of the code works most of the time in any sane development practice; when the current code works, the details don't help with understanding the current function.
Jumping around is also not more difficult than scrolling, with a proper editor.
The need for fuctions are usually abstraction or reusability. If you can extract part of the logic under a nice domain concept please do so. It’s better than comments. Not extracting the logic usually means a lot of intermediate variables to keep track for.
If you are passing your whole state around, that usually means, you’ve not designed your state well.
> Say, you have a process that is single threaded and does a lot of stuff that has to happen step by step.
> New dev comes in; and starts splitting everything it does in 12 functions, because, _a function, should do one thing
I would almost certainly split it up, not because "a function should only do one thing" but because invariably you get a run of several steps that can be chunked into one logical operation, and replacing those steps with the descriptive name reduces the cognitive load of reading and maintaining the original function.
If you read Clean Code and other similar books, they don't necessarily advise moving the sub-functions to other files, or splitting them arbitrarily.
They simply have the top function delegate to sub-functions that are lower in the same file.
Even better if these sub-functions can be marked as private in your language (to avoid polluting the public API of your object).
And here the goal is to use function names to document what each block of code (sub-function) does.
Wow, that brings back memories! I remember reading this years ago, and it made me change my programming style a bit. The copy-paste errors he mentioned especially opened my eyes, I realised I was doing that too. For writing repetitive "0,1,2,3" or "x,y,z" code I switched to using Wolfram Mathematica's symbolic expansion and simplification followed by a C code generation step.
I think that you are describing an ideal scenario that does not reflect what I see in reality. In the "enterprise applications" that I work on, long functions evolve poorly. Meaning, even if a long function follows the ideal of "single thread, step by step" when it's first written, when devs add new code, they will typically add their next 5 lintes to the same function because it's already there. Then after 5 years you have a monster.
This is a balancing act between conflicting requirements. It is understandable that you don't want to jump back and forth between countless small subfunctions in order to meticulously trace a computation. But conceptually, the overall process still breaks down into subprocesses. Wouldn't it make sense to move these sub-processes into separate functions and name them accordingly? I have a colleague who has produced code blocks that are 6000 lines long. It is then almost impossible to get a quick overview of what the code actually does. So why not increase high-level readability by making the conceptual structure visible in this way?
A ReverseList function, for example, is useful not only because it can be used in many different places, but also because the same code would be more disruptive than helpful for understanding the overall process if it were inline. Of course, I understand that code does not always break down into such neat semantic building blocks.
> Focus on separating pure code from stateful code, that's the key to large maintainable software! And choose composability over inheritance.
Agree that "splitting for splittings' sake" (only to stay below an arbitrary line count) does indeed not make sense.
On the other hand I often see functions like you describe - something has to be executed step-by-step (and the functionality is only used there) - where I _whish_ it was split up into separate functions, so we could have meaningful tests for each step, not only for the "whole thing".
If I have a huge function, and I can peel parts off into sensible well-encapsulated sub-functions, and I name them well, then my ability to comprehend the whole goes up.
If I do that, future me will thank me, because I will almost inevitably be back to that function at some point.
But for this to work, the sub-functions have to really do what they say, and not do anything more. I have to be able to trust that I can understand them by just their name and arguments.
What would be a good example of the kinds of things a 100 line function would be doing?
I don't see that in my world so i'm naively trying to inline functions in codebases i'm familiar with and not really valuing the result i can dream up.
For one, my tests would be quite annoying, large and with too much setup for my taste. But i don't think i'd like to have to scroll a function, especially if i had to make changes to the start and end of the function in one commit.
I'm curious of the kinds of "long script" flavoured procedures, what are they doing typically?
I ask because some of the other stuff you mentioned i really strongly agree with like "Focus on separating pure code from stateful code" - this is such an under valued concept, and it's an absolute game changer for building robust software. Can i extract a pure function for this and separately have function to coordinate side effects - but that's incompatible with too long functions, those side effectfull functions would be so hard to test.
> What would be a good example of the kinds of things a 100 line function would be doing?
For me i see it alto in older video game engines with a switch statement that has 100s of possible states that need to be taken into account.
That kind of code is less common these days but its nice to be able to see all the possible states in a single switch statements vs them all being abstracted away in multiple classes that handle a subset of each state transition.
This is one case where having a super long function makes readability far better than abstracting away the states into multiple classes that all need to be read to understand the state of the world.
> What would be a good example of the kinds of things a 100 line function would be doing?
If you have a shopping list with 100 items, that does not mean the complexity of that is high.
Longer functions are worse, everything else being equal, but sometimes they have very low complexity.
To answer your question: if you have, for example, a function that returns for a key a certain value (basically a dictionary) for example for some translations that can be a very long function. Of course you could take the data out of the code into a data file, etc. but there is nothing wrong on principle about a function like that. A function like that is closer to extract state into data than one where that gets refactored into many small functions.
Funny this is the assertion in the I would most agree to use as general design principle to apply as thoroughly as possible, with tightened variable scope on equal position. Though no general principle should be followed blindly of course.
That's not the the function length per se. A function that is 1000 lines of mere basic assignments or holding a single giant switch can sometime be an apt option with careful consideration of tradeoffs as origin of the design. Number of line doesn't tell much of the function complexity and cognitive load of will imply to grasp what it does, though it can be a first proxy metric.
But most of the time giant functions found in the wild grow up organically with 5 levels of intertwined control control moving down and up, accumulating variables instead of const without consideration to scope span. In that case every time a change is needed, the cognitive load to grasp everything that need to be considered to change this finding is extremely huge. All the more as this giant function most likely won't have an test suit companion, because good engineering practices are more followed at equal level on several points.
I have been programming professionally for 17 years and I think this guideline is fine. I have difficulty imagining a function of 70 lines that would not be better off being split into multiple functions. It is true that if a function is just a list of stuff longer functions can be allowed then when it does multiple different things but 70 lines is really pushing that.
i see these rules and think “70 lines wow that’s short”
and then i read code and see a 50 line function and am like “wow this function is doing a lot”
sure strict rules aren’t amazing, but i think it would be cool to have a linter warning when there are more than X functions with over 70 lines (this is language dependent - verbosity)
I see huge >130 line functions as a liability. There's so much state in it that a mistake on one line is not obvious. It makes those functions "sticky" and they tend to become the center of a lot of call graphs... like a neutron star. When a mistake is made maintaining or modifying this function it tends to have far-reaching side effects.
On the other hand some APIs (looking at you, OpenGL) are just so verbose that you can't avoid long functions.
I think it's generally good to compose functions from smaller functions where possible. Sometimes you can't and probably shouldn't. But it's hard to give a quantifiable rule in my experience. Approximations work but will never be perfect.
To me, that's key here. That things are scattered over multiple files is a minor issue. Any competent IDE can more or less hide that and smoothen the experience. But if you have factored some code into a function, suddenly other places may call it. You have inadvertently created an API and any change you make needs to double check that other callers either don't exist or have their assumptions not suddenly violated. That's no issue if the code is right therr. No other users, and the API is the direct context of the lines of code right around it. (Yes you can limit visibility to other modules etc but that doesn't fully solve the issue of higher cognitive load.)
That is an important consideration which you should weigh against the clarity gained by breaking up your code in units of logic.
That is precisely the reason why for a function the scope of its container should be constrained. A class with a Singe Responsibility (SRP) would not suffer from several private methods. Breaking up your methods in a God Class brings both a decrease and an increase of mental load.
Also, in functional programming languages one can nest functions, and OOP languages oftentimes can nest classes.
Haskell does function hiding the most elegant imo, by means of `where`:
foo x =
if odd x then
short x
else
reallyLongFunc x
where
short y = y*2
reallyLongFunc z = z + 2
The point of breaking down the long function in God Classes , is to prepare the code for a second refactoring, which is usually extract class, long params list, etc..
If you were to simply leave it a mess like that, you would be right.
> These things weren't clear to me the first 10 years, but after 30 years, I've made those conclusions. I hope other old-timers can chime in on this.
'Sup, it's me, the "new dev". Except I, too, have been at it for decades, and I get more and more attached to short functions year over year. (You are correct about composition and about isolating state mutations. But short functions are tools that help me to do those things. Of course, it helps a ton to have functions as first-class objects. Function pointers are criminally underused in C codebases from what I've seen. They can be used for much more than just reinventing C++ vtables.)
People put numbers on their advice because they don't trust the audience to have good taste, or to have a sense of the scale they have in mind. Of course that has the downside that metrics become targets. When I see a number in this kind of advice, I kinda take it in two passes: understand what kind of limit is proposed (Over or under? What is being limited?) and then go back and consider the numeric ballpark the author has in mind. Because, yes, 70 lines of Python is not the same as 70 lines of C.
But I can scarcely even fathom ten lines in a Python function that I write nowadays. And I'm rather skeptical that "LOC needed to represent a coherent idea" scales linearly with "LOC needed to make a whole program work".
> Now you have 12 functions, scattered over multiple packages, and the order of things is all confused, you have to debug through to see where it goes. They're used exactly once, and they're only used as part of a long process. You've just increased the cognitive load of dealing with your product by a factor of 12. It's downright malignant.
Well, no, that isn't what happens at all.
First off, the files where new functions get moved, if they get moved at all, are almost certainly going to be in the same "package" (whatever that means in the programming language in use). The idea that it might be hard to find the implementation code for something not in the current file, is pretty close to being a problem unique to C and C++. And I'm pretty sure modern IDEs have no problem dealing with that anyway.
Second, it absolutely does not "increase the cognitive load by a factor of 12". In my extensive experience, the cognitive load is decreased significantly. Because now the functions have names; the steps in the process are labelled. Because now you can consider them in isolation — the code for the adjacent steps is far easier to ignore.
Why would you "have to debug through to see where it goes"? Again, the functions have names. If the process really is purely sequential, then the original function now reads like a series of function calls, each naming a step in the sequence. It's now directly telling you what the code does and how. And it's also directly telling you "where it goes": to the function that was called, and back.
You also no longer have to read comments interspersed into a longer code flow, or infer logical groupings into steps. You can consider each step in isolation. The grouping is already done for you — that's the point. And if you aren't debugging a problem, that implies the code currently works. Therefore, you don't need to go over the details all at once. You are free to dig in at any point that tickles your curiousity, or not. You don't have to filter through anything you aren't interested in.
(Notice how in the three paragraphs above, I give one-sentence descriptions in the first paragraph of individual advantages, and then dedicate a separate paragraph to expanding on each? That is precisely the same idea of "using short functions", applied to natural language. A single, long paragraph would have been fewer total words, but harder to read and understand, and less coherent.)
All of that said, you don't really debug code primarily by single-stepping through long functions, do you? I find problems by binary search (approximately, guided by intuition) with breakpoints and/or logging. And when the steps are factored out into helper functions, it becomes easier to find natural breakpoints in the "main" function and suss out the culprit.
Shorter functions absolutely do have the properties described in the quote. Almost definitionally so. Nobody really groks code on the level of dozens of individual statements. We know brains don't work like that (https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus...).
What has always baffled me is how CS uses the word "safety" where all other industries use "robustness".
A robust design is one that not only is correct, but also ensures the functionality even when boundary conditions deviate from the ideal. It's a mix of stability, predictability and fault tolerance. Probably "reliable" can be used as a synonym.
At the same time, in all industries except CS "safety" has a specific meaning of not causing injuries to the user.
In the design of a drill, for example, if the motor is guaranteed to spin at the intended rpm independently of orientation, temperature and state of charge of the battery, that's a robust design. You'll hear the word "safe" only if it has two triggers to ensure both hands are on the handles during operation.
You use safety more in relation to correctness aspects of algorithms. Some safety properties you can actually prove. When it comes to robustness it is more about dealing with things sometimes being incorrect regardless of the safety mechanisms. So, a try catch for something that isn't actually expected to normally fail makes you robust against the scenario when that does fail. But you'd use e.g. a type system to prevent classes of failures as safety mechanism.
It's a very soft distinction, I agree. And possibly one that translates less well to the physical world where wear and tear are a big factor for robustness. You can't prove an engine to be safe after thousands of hours. But you can make it robust against a lot of expected stuff it will encounter over those hours. Safety features tend to be more about protecting people than the equipment.
> What has always baffled me is how CS uses the word "safety" where all other industries use "robustness".
FWIW "safety factors" are an important part of all kinds of engineering. The term is overloaded and more elusive in CS because of the protean qualities of algorithmic constructs, but that's another discussion.
> At the same time, in all industries except CS "safety" has a specific meaning of not causing injuries to the user.
That makes sense, as CS is transversal to industries.
Same practices that can contribute to literally save lifes in one domain will just avoid minor irritating feeling in an other.
Pretty good list, but a hidden assumption is that the reader works in an imperative style. For instance, recursion is the bread and butter of functional and logical programming and is just fine.
The most important advice one can give to programmers is to
1. Know your problem domain.
2. Think excessively deep about a conceptual model that captures the
relevant aspects of your problem domain.
3. Be anal about naming your concepts. Thinking about naming oftentimes
feeds back to (1) and (2), forming a loop.
4. Use a language and type system that is powerful enough to implement
previous points.
I got this sense as well, particularly from the section about assertions. Most of the use cases they describe (e.g checking function arguments and return values) are much better handled by an expressive type system than ad-hoc checks.
But that last 10% of checking may be really hard to encode in types. It may be especially hard to do so in the language that you want to use for other reasons.
The "split long functions" advice works well for most code but falls apart in transaction processing pipelines.
I work on financial data processing where you genuinely have 15 sequential steps that must run in exact order: parse statement, normalize dates, detect duplicates, match patterns, calculate VAT, validate totals, etc. Each step modifies state that the next step needs.
Splitting these into separate functions creates two problems: (1) you end up passing huge context objects between them, and (2) the "what happens next" logic gets scattered across files. Reading the code becomes an archaeology exercise.
What I've found works better: keep the orchestration in one longer function but extract genuinely reusable logic (date parsing, pattern matching algorithms) into helpers. The main function reads like a recipe - you can see the full flow without jumping around, but the complex bits are tucked away.
70 lines is probably fine for CRUD apps. But domains with inherently sequential multi-step processes sometimes just need longer functions.
> Each step modifies state that the next step needs.
I've been bitten by this. It's not the length that's the problem, so much as the surface area which a long function has to stealthily mutate its variables. If you have a bunch of steps in one function all modifying the same state, there's a risk that the underlying logic which determines the final value of widely-used, widely-edited variables can get hard to decipher.
Writing a function like that now, I'd want to make very sure that everything involved is immutable & all the steps are as close to pure functions as I can get them. I feel like it'd get shorter as a consequence of that, just because pure functions are easier to factor out, but that's not really my objective. Maybe step 1 is a function that returns a `Step1Output` which gets stored in a big State object, and step 2 accesses those values as `state.step1Output.x`. If I absolutely must have mutable state, I'd keep it small, explicit, and as separate from the rest of the variables as possible.
I'll admit this may be naive, but I don't see the problem based on your description. Split each step into its own private function, pass the context by reference / as a struct, unit test each function to ensure its behavior is correct. Write one public orchestrator function which calls each step in the appropriate sequence and test that, too. Pull logic into helper functions whenever necessary, that's fine.
I do not work in finance, but I've written some exceptionally complex business logic this way. With a single public orchestrator function you can just leave the private functions in place next to it. Readability and testability are enhanced by chunking out each step and making logic obvious. Obviously this is a little reductive, but what am I missing?
The typestate pattern common in Rust applications allows the compiler to verify that the operations are executed in the right order and that previous states are not accidentally referenced. Here’s a good description: https://cliffle.com/blog/rust-typestate/
I frequently write software like this (in other domains). Structs exist (in most languages) specifically for the purpose of packaging up state to pass around, I don't really buy that passing around a huge context is the problem. I'm not opposed to long functions (they can frequently be easier to understand than deep callstacks), especially in languages that are strictly immutable like Clojure or where mutation is tightly controlled like Rust. Otherwise it really helps to break down problems into sub problems with discrete state and success/failure conditions, even though it results in more boilerplate to manage.
This is an admirable goal but complicated by the inevitable conditional logic around what to do in certain situations. Push the conditions into the sub methods, or keep them in primary method? Thinking about what are testable chunks of logic can be a valuable guide. A lot of discipline is required so these primary methods dont become spaghetti themselves.
Lots of debating about the color to paint the fence in the design meetings for the nuclear reactor...
I don't know how this philosophy is applied at TigerBeetle. When I establish engineering guidelines I try to frame them as exactly that: guidelines. The purpose is to spawn defensible reasoning, and to trigger reflection.
For example, I might say this:
We use a heuristic of 70 lines not as a hard limit, but as a "tripwire." If you cross it, you are not 'wrong,' but you are asked to pause and consider if you're introducing unintentional complexity. If you can justify it, keep it—there's no need to code golf.
"Style," "philosophy," "guides," they're all well-meaning and often well-informed, but you should be in command of the decision as the developer and not forget your own expertise around cohesion, cognitive load, or any functional necessities.
There are staunch believers in gating deploys based solely on LOCs, I'm sure... I like the idea of finding ways to transparently trigger cognitive provocations in order for everyone to steer towards better code without absolutes.
Zero technical debt certainly is... ambitious. Sure, if we knew _what_ to build the first time around this would be possible. From my experience, the majority of technical debt is sourced from product requirement changes coupled with tight deadlines. I think even the most ardent follower of Tiger Style is going to find this nigh impossible.
I would even say that from a project management perspective, zero technical debt is undesirable. It means you have invested resources into perfecting something that, almost by definition, could have waited a while, instead of improving some more important metric such as user experience. (I do understand tech debt makes it harder to work with the codebase, impacting all metrics, I just don’t think zero tech debt is a good target.)
> perfecting something that, almost by definition, could have waited a while
No technical debt is not the same thing as “perfection”. Good enough doesn’t mean perfect.
Would it be ok to submit an essay with only 90% of the underlined spelling mistakes fixed? Do you paint your outdoor table but leave the underside for later?
Do it once, do it right. That doesn’t mean perfect, it means not cutting corners.
There are contexts where quick and dirty and (hopefully) come back later are warranted. Far more often it is just an excuse for shoddy work. You used the word “perfection” as the contrast to “technical debt”. Granted, technical debt is not a well defined term, but I am simply highlighting that “free from technical debt” in no way implies anything like perfect. It just implies well made.
Technical debt is not a current defect. It just means that for the sake of having something quickly done today, you accept that the cost of changing stuff tomorrow will be greater than normal. If you never have to change something (switching jobs, consultancy project you don’t care about,…) then it may be a great trade off.
Ironically, some of the worst tech debt I’ve ever dealt with has been because the initial implementation was an overengineered disaster by an dev who thought they were solving all possible problems before we really understood what all possible problems are.
“Zero tech debt” is an impossibility. The most elegant solutions incur some kind of tech debt, it’s just less than others. More realistic than “zero tech debt” is a continuing dedication to addressing tech debt combined with using implementations that minimize “one way doors”.
> Limit line lengths: Keep lines within a reasonable length (e.g., 100 characters) to ensure readability. This prevents horizontal scrolling and helps maintain an accessible code layout.
Do you not use word wrap? The downside of this rule is that vertical scrolling is increased (yes, it's easier, but with a wrap you can make that decision locally) and accessibility is reduced (and monitors are wide, not tall), which is especially an issue when such a style is applied to comments so you can't see all the code in a single screen due to multiple lines of comments in that long formal grammatically correct style
Similarly,
> Limit function length: Keep functions concise, ideally under 70 lines.
> and move non-branching logic to helper functions.
Break accessibility of logic, instead of linearly reading what's going on you have to jump around (though popups could help a bit). While you can use block collapse to hide those helper blocks without losing their locality and then expand only one helper block.
> which is especially an issue when such a style is applied to comments so you can't see all the code in a single screen due to multiple lines of comments in that long formal grammatically correct style
It’s easier to hide comments than do code wrapping correctly. And comments are usually in a lighter color than code and easy to skip over.
> Break accessibility of logic, instead of linearly reading what's going on you have to jump around (though popups could help a bit)
Extracting function is for abstraction, instead of deciphering some block and tracking its entanglement, you have a nice name and a signature that document its input and output. And maybe a nice docstring that summarizes its purpose.
> a lighter color than code and easy to skip over.
How does color and the ease of skipping address the fact that you don't see code because it's pushed off screen? Especially compared to the ease of skipping when you wrap the whole paragraph of comments into a single long line when you don't need to read it.
> And maybe a nice docstring that summarizes its purpose.
Which you could just as well have right there where you read the block.
The nice name/signature yes, that can be situationally better
There’s place where you can break the code before 100 or even 80 line limit. And the only reason that you cannot do so with code is excessive indentation (nested scope). Which you will find easily in very long functions.
> Which you could just as well have right there where you read the block.
A function forces encapsulation and make explicit the external factors. A block doesn’t make clear which variables in the parent scope it’s using or if everything in the block is one logical step.
Can you have a coding philosophy that ignores the time or cost taken to design and write code? Or a coding philosophy that doesn't factor in uncertainty and change?
If you're risking money and time, can you really justify this?
- 'writing code that works in all situations'
- 'commitment to zero technical debt'
- 'design for performance early'
As a whole, this is not just idealist, it's privileged.
Will save you time and cost in designing, even in the relatively near term of a few months when you have to add new features etc.
There's obviously extremes of "get something out the door fast and broken then maybe neaten it up later" vs "refactor the entire codebase any time you think soemthing could be better", but I've seen more projects hit a wall due to leaning to far to the first than the second.
Either way, I definitely wouldn't call it "privileged" as if it isn't a practical engineering choice. That seems to judt frame things in a way where you're already assuming early design and commitment to refactoring is a bad idea.
Your argument hinges on getting the design right, upfront.
That assumes uncertainty is low or non-existent.
Time spent, monetary cost, and uncertainty, are all practical concerns.
An engineering problem where you can ignore time spent, monetary cost, and uncertainty, is a privileged position. A very small number of engineering problems can have an engineering philosophy that makes no mention of these factors.
It’s the equivalent of someone running on a platform where there would be world peace and no hunger.
That’s great and all as an ideal but realistically impossible so if you don’t have anything more substantial to offer then you aren’t really worth taking seriously.
You forgot “get it right first time” which goes against the basic startup mode of being early to the market or die.
For some companies, trying to get it right the first time may make sense but that can easily lead to never shipping anything.
This seems pretty knee-jerk. I do most of this and have delivered a hell of a lot of software in my life. Many projects are still running, unmodified, in production, at companies I’ve long since left.
You can get a surprising amount done when you aren’t spending 90% of your time fighting fires and playing whack-a-mole with bugs.
Well, I'm sure you're well aware of perils of premature optimization and know how to deliver a product within a reasonable timeframe. TigerStyle seems to me to not be developed through the lens of producing value for a company via software, but rather having a nice time as a developer (see: third axiom).
I'm not saying the principles themselves are poor, but I don't think they're suitable for a commercial environment.
I had the same association but interestingly this version appears to be a "remix" of TigerBeetle's style guide, by an unrelated individual. At a glance, there is a lot of a crossover but some changes as well.
I think the point is well made though. When you're building something like a transactions database, the margin for error is rather low.
Then I'm curious about what their principles on deadlines. I don't see how it aligns with their coding styleguide. Taking the TigerStyle at face value does not encourage deliverance. They're practically saying "take the time you need to polish your thing to perfection, 0 technical debt".
But ofc, I understand styleguides are... well.. guides. Not law.
I don't really see anything in it that particularly difficult our counter-productive. Or, to be honest, anything that isn't just plain good coding practice. All suitably given as guidelines not hard and fast rules.
The real joy of having coding standards, is that it sets a good baseline when training junior programmers. These are the minimum things you need to know about good coding practice before we start training you up to be a real programmer.
If you are anything other than a junior programmer, and have a problem with it, I would not hire you.
The attribution to TigerBeetle should be at the top of the page with a link to the original tigerstyle, not buried at the bottom. Right now it reads like official TigerBeetle content until you scroll down, which isn't fair to either you or the original team.
That's really nice of the author and awesome to hear. Would love to see TigerStyle be published in a similar format and expanded upon with more detail be great for referencing with my colleagues. Would love to adopt it on projects or some of the rules at least
Not to midwit but I will stick to MISRA C for this sort of thing. It is very clear in why each thing must be done and it is hard to argue against most of it. It’s usually where safety and robustness deeply matter and using it would have avoided a number of high-profile exploits we have seen in the recent past. While not every “rule” may be applicable to all languages, many standard languages could adopt a good number of rules from it and benefit greatly. Anyone with a number of years experience with some reflection on what is good, clear and unambiguous style would reinvent a decent portion of MISRA C.
> Avoid recursion if possible to keep execution bounded and predictable, preventing stack overflows and uncontrolled resource use.
In languages with TCO (e.g. Haskell, Scheme, OCaml, etc.) the compiler can rewrite to a loop.
Some algorithms are conceptually recursive and even though you can rewrite them, the iterative version would be unreadable: backtracking solvers, parsing trees, quicksort partition & subprblems, divide-and-conquer, tree manipulation, compilers, etc.
Interesting use of `latency_ms_max` as a naming convention. I'm definitely guilty of `max_latency_ms` instead, but they make a convincing argument for putting `max` at the end.
If this topic floats your boat, go look up the NASA coding standards. For a few projects, I tried to follow a lot of their flow control recommendations, and will still reach for: `while ... && LIMIT > 0` in some situations.
Still a huge fan of including some type info in the variable name, eg: duration_s, limit_ms makes it extremely clear that you shouldn't mix math on those integers.
1. 100% code coverage
2. 100% branch coverage
3. 100% lint (without noqa)
4. 100% type check pass(for python/js)
5. 100% documentation coverage
6. All functions with complexity less than 5. All functions with no of lines less than 70. All files with number of lines less than 1000.
These make code high quality, and quality of life is directly proportional to qualify of your code.
> Do it right the first time: Take the time to design and implement solutions correctly from the start.
Doing good design is off course important, but on the other hand software design is a lot of times iterative because of unknown unknown s. Sometimes it can be better to create quick prototype(s) to see which direction is the best to actually "do it right", instead of spending effort designing something that in the end won't be build.
> Allocate all necessary memory during startup and avoid dynamic memory allocation after initialization.
Absolutely not an ultimatum advice, even harmful in most cases. Just measure you memory usage and don't rely on and avoid external OOM handler.
I think they are hinting at the idea that a long running process on a dedicated server should allocate all necessary memory during initialization. That way you never accidentally run out of memory during high loads, a time when you really don't want your service to go down.
You would need so much more physical memory with this approach. For example, you may want to allocate a lot of memory for the browser, but then close the tab. 2 seconds later you want to use it for a compiler. And then you switch to a code editor.
“Rugged” describes software development organizations that have a culture of rapidly evolving their ability to create available, survivable, defensible, secure, and resilient software.
This is the kind of development that one needs for safety critical applications. E.g., nuclear power plants or airplane control software. I don't think it is economically feasible for less critical software. It presumes a great degree of stability in requirements which is necessary for such applications.
Limit function length: Keep functions concise, ideally under 70 lines. Shorter functions are easier to understand, test, and debug.
The usual BS... yes, shorter functions are easier to understand by themselves but what matters, especially when debugging, is how the whole system works.
Edit: care to refute? Several decades of experience has shown me what happens. I'm surprised this crap is still being peddled.
This one, like some others in this style guide, can also be found in Clean Code. Not sure why you feel the need to call it "BS". Nobody is saying that your 75 line function is bad.
It's a reasonable guideline. Juniors won't do this automagically.
The "Clean Code" book is famous for making bad suggestions and producing unreadable code with too many functions just for the sake of following its rules.
It's worth noting that the author of "Clean Code" does not appear to have written any software of significance. He's nothing more than a preaching salesman.
There are far more real developers to learn good practices from. Brian Kernighan, Dennis Ritchie, Linus Torvalds, etc.
Don't get me wrong, I often apply it myself and refactor code into smaller functions. But readability, understandability and ease of maintenance comes first.
Especially juniors will apply such rules overzealously, bending over backwards to stay within an arbitrary limit while at the same time not really understanding why.
Frustratingly, their lack of experience makes it impossible to discuss such dogmatic rules in any kind of nuanced way, while they energetically push for stricter linter rules etc.
I've tried, and one even argued there's never ever a reason to go over N lines because their book is "best-practice" and said so. And you should not deviate from "best-practice" so that's it. I'm not making this up!
That said, I'm always open to discuss the pros and cons for specific cases, and I do agree the default should be to lean towards smaller functions in 90% of cases.
Yeah, you can’t build a building unless you’re certain that every brick, rebar, and cement is all solid and meets a quality bar. Of course you need architectural schematics to make sense of the broader picture, but you need quality control on the pieces being put in place too.
I like this, to me it reads like a collection of fairly obvious best practices (even if there might be practical reasons to avoid when shipper fast etc), so I'm surpised to see so many enraged comments.
Any recommendations for other coding philosophies or "first principle" guides? I know of "extreme programming" but not much else.
I would like to note that this text was transformative in my current project (an effort to model CNC work in 3D) --- when I first read the book, I did it chapter-by-chapter, applying the lesson learned from that chapter to the entire codebase before moving on to the next --- I am currently re-reading the book for the third time, and I'm planning on a compleat re-write before I do 1.0....
I literally laughed out loud when I got to this line
> Do it right the first time
So easy, why didn't I think of that!? /s
Reminds me of the mental health meme of telling depressed people to just be happier instead.
I don't necessarily disagree with a lot in this philosophy, but much of it is puffery if not accompanied by practical positive and negative examples. If a junior with little experience reads this, I'm not sure if they'll be better or worse off.
For example, "Design for performance early" is dangerous if it leads to premature optimization. But that's not mentioned. Practical positive and negative examples that illustrate the balance between these two concerns would make the advice actionable.
I was enjoying what I was reading until the "Limit function length" part which made me jolt out of my chair.
This is a common misconception.
Say, you have a process that is single threaded and does a lot of stuff that has to happen step by step.New dev comes in; and starts splitting everything it does in 12 functions, because, _a function, should do one thing!_ Even better, they start putting stuff in various files because the files are getting too long.
Now you have 12 functions, scattered over multiple packages, and the order of things is all confused, you have to debug through to see where it goes. They're used exactly once, and they're only used as part of a long process. You've just increased the cognitive load of dealing with your product by a factor of 12. It's downright malignant.
Code should be split so that state is isolated, and business processes (intellectual property) is also self contained and testable. But don't buy into this "70 lines" rule. It makes no sense. 70 lines of python isn't the same as 70 lines of C, for starters. If code is sequential, and always running in that order and it reads like a long script; that's because it is!
Focus on separating pure code from stateful code, that's the key to large maintainable software! And choose composability over inheritance. These things weren't clear to me the first 10 years, but after 30 years, I've made those conclusions. I hope other old-timers can chime in on this.
The length of functions in terms of line count has absolutely nothing to do with "a more modular and maintainable codebase", as explained in the manifesto.
Just like "I committed 3,000 lines of code yesterday" has nothing to do with productivity. And a red car doesn't go faster.
“Ideally under 70 lines” is not “always under 70 lines under pain of death”.
It’s a guideline. There are exceptions. Most randomly-selected 100-line functions in the wild would probably benefit from being four 25-line functions. But many wouldn’t. Maturity is knowing when the guideline doesn’t apply. But if you find yourself constantly writing a lot of long functions, it’s a good signal something is off.
Sure, language matters. Domain matters too. Pick a number other than 70 if you’re using a verbose language like golang. Pick a number less if you’re using something more concise.
People need to stop freaking out over reasonable, well-intentioned guidelines as if they’re inviolable rules. 150 is way too many for almost all functions in mainstream languages. 20 would need to be violated way too often to be a useful rule of thumb.
As with all guidelines, some people will turn it into a hard rule, and build a linter to enforce it. Then they will split long functions into shorter ones, but with a lot of arguments. And then their other linter that limits argument count will kick in.
And someone else will use the idea that this is a misconception to justify putting hundreds of lines in one function.
Some junior dev will then come along with his specific user story and, not wanting to tear the whole thing up, will insert his couple of lines. Repeat over a 10 year lifecycle and it becomes completely unmanageable.
I remember trying to tease apart hundreds of lines of functions in a language which didn't have the IDE support we have these days. It was always painful.
Even the most script like functions I've ever worked with benefit from comments like:
These subheadings can just as easily become functions. If you limit the context in each function it becomes a genuine improvement.When I was a junior developer working in the outsourcing industry, I regularly had to maintain projects where the entire codebase was in a few major functions. It was a stressful nightmare to debug - such projects destroyed my health. No feature could be developed without extensive forking of the codebase and prototyping to see what works and what doesn't.
After several such bad experiences, I religiously incorporated my personal guideline of functions never crossing the upper limit of 100 lines - with some 1 in 10k exceptions like in the parsing domain or state machine dispatch. Short functions also heavily aid in unit testing.
The only folks who believe in big functions are developers who haven't been exposed to large quantities of other people's shoddy code as a significant fraction of their working career. Once you have been through several such nightmares, you will worship small functions.
> and build a linter to enforce it.
That’s why linters allow you to suppress specific rules for a single statement (and ideally let you write a short comment that explains why you chose to suppress it.)
Sounds like an opportunity to guide that person.
Good luck Arguing with stubborn devs who know it all
At this point we're suggesting that we shouldn't implement some guidelines because there exist developers who will naively take it to extremes and stubbornly refuse to budge. Those developers will be an issue practically independent of the guidelines themselves.
You mean like the stubborn devs who argue we shouldn’t have guidelines because one in a thousand times it doesn’t apply?
> It’s a guideline ... Pick a number
It's using a parameter of secondary importance as a primary, so it's wrong with any number. The comment even has a helpful analogy to LOCs. People need to stop freaking out over reasonable, well-intentioned criticism of guidelines as if they were arguing something else like misundertstanding of that strictness of rules.
Until we have an absolute metric to use for measuring comprehensibility, composability, and number of high-level concepts addressed by some unit of code, highly-correlated secondary metrics will have to suffice.
Until then you don't have anything to "highly" correlate, so you can continue to guide people towards substance instead of questionable metrics
I would love for you to show me a high-quality code base where even 5% of functions exceed 70 lines.
I for sure haven’t seen one. Most of the best, easiest to work on, and stablest projects I’ve worked in my 25-year career have had the overwhelming majority under 30 LOC, and maybe 1% over 70 LOC.
As with all smells, yes, they are simply correlated symptoms of deeper issues and not necessarily proof positive. But they are correlated. Pretending they aren’t is nonsense.
And yes, arbitrarily chopping things up to quiet a linter doesn’t necessarily solve the problem, much in the same way dumping perfume onto rotten eggs doesn’t fix the underlying issue. It’s a fast and easy warning to alert you to potential issues, but at the end of the day it’s still up to developers to exercise good judgment and development practice.
I don't understand what you expect here - you're treating these metrics as a sure undeniable sign of quality, so by definition the examples won't be high quality. So of course you "for sure haven’t seen one", what kind of circular logic is that?
> Pretending they aren’t
But you're the one doing the pretending, making up high correlation with nothing to correlate, and coming to circular conclusions (if you define it as smell, then yes, you aren't taking about roses)
Just chiming in here to say, absolutely you should keep functions small and doing one thing. Any junior reading this should go and read the pragmatic programmer.
Of course a function can be refactored in a wrongheaded way as you’ve suggested, but that’s true of any coding - there is taste.
The ideal of refactoring such a function you describe would be to make it more readable, not less. The whole point of modules is so you don’t have to hold in your head the detail they contain.
Long functions are in general a very bad idea. They don’t fit on a single screen, so to understand them you end up scrolling up and down. It’s hard to follow the state, because more things happen and there is more state as the function needs more parameters and intermediate variables. They’re far more likely to lead to complecting (see Rich Hickey) and intertwining different processes. Most importantly, for an inexperienced dev it increases the chance of a big ball of mud, eg a huge switch statement with inline code rather than a series of higher level abstractions that can be considered in isolation.
I don’t think years worked is an indicator of anything, but I’ve been coding for nearly 40 years FWIW.
> They don’t fit on a single screen, so to understand them you end up scrolling up and down.
Split them up into multiple functions and there is more scrolling, and now also jumping around because the functions could be anywhere.
> It’s hard to follow the state, because more things happen
It's easier to follow state, because state is encapsulated in one function, not constantly passed around to multiple.
> a huge switch statement with inline code
Huge switch statements are a common reason for large functions. Python has this architecture and largely won because the interpreter is surprisingly easy to understand and extend.
> Split them up into multiple functions and there is more scrolling, and now also jumping around because the functions could be anywhere.
No, this is the fundamental misconception used in defense of longer functions.
With a long function, you must scroll no matter what. If you're interested in every detail, you scroll. If you want to figure out the logical steps involved, you scroll (and likely have to take notes as you go). If you want to ignore a step for now, you scroll over it.
With properly factored code, you only jump away when you need to. If you want to figure out the logical steps involved, you just read them off. If you want to ignore a step for now, you just ignore it. If you're interested in every detail, you get to start with an overview, and then you jump around. But caring about details is the exceptional case. Most of the code works most of the time in any sane development practice; when the current code works, the details don't help with understanding the current function.
Jumping around is also not more difficult than scrolling, with a proper editor.
The need for fuctions are usually abstraction or reusability. If you can extract part of the logic under a nice domain concept please do so. It’s better than comments. Not extracting the logic usually means a lot of intermediate variables to keep track for.
If you are passing your whole state around, that usually means, you’ve not designed your state well.
> Say, you have a process that is single threaded and does a lot of stuff that has to happen step by step. > New dev comes in; and starts splitting everything it does in 12 functions, because, _a function, should do one thing
I would almost certainly split it up, not because "a function should only do one thing" but because invariably you get a run of several steps that can be chunked into one logical operation, and replacing those steps with the descriptive name reduces the cognitive load of reading and maintaining the original function.
[dead]
If you read Clean Code and other similar books, they don't necessarily advise moving the sub-functions to other files, or splitting them arbitrarily. They simply have the top function delegate to sub-functions that are lower in the same file. Even better if these sub-functions can be marked as private in your language (to avoid polluting the public API of your object). And here the goal is to use function names to document what each block of code (sub-function) does.
Example:
And here's John Carmack on the subject of 1000s of lines of code in a single function: http://number-none.com/blow/blog/programming/2014/09/26/carm...
Wow, that brings back memories! I remember reading this years ago, and it made me change my programming style a bit. The copy-paste errors he mentioned especially opened my eyes, I realised I was doing that too. For writing repetitive "0,1,2,3" or "x,y,z" code I switched to using Wolfram Mathematica's symbolic expansion and simplification followed by a C code generation step.
I think that you are describing an ideal scenario that does not reflect what I see in reality. In the "enterprise applications" that I work on, long functions evolve poorly. Meaning, even if a long function follows the ideal of "single thread, step by step" when it's first written, when devs add new code, they will typically add their next 5 lintes to the same function because it's already there. Then after 5 years you have a monster.
This is a balancing act between conflicting requirements. It is understandable that you don't want to jump back and forth between countless small subfunctions in order to meticulously trace a computation. But conceptually, the overall process still breaks down into subprocesses. Wouldn't it make sense to move these sub-processes into separate functions and name them accordingly? I have a colleague who has produced code blocks that are 6000 lines long. It is then almost impossible to get a quick overview of what the code actually does. So why not increase high-level readability by making the conceptual structure visible in this way?
A ReverseList function, for example, is useful not only because it can be used in many different places, but also because the same code would be more disruptive than helpful for understanding the overall process if it were inline. Of course, I understand that code does not always break down into such neat semantic building blocks.
> Focus on separating pure code from stateful code, that's the key to large maintainable software! And choose composability over inheritance.
100%!
Agree that "splitting for splittings' sake" (only to stay below an arbitrary line count) does indeed not make sense.
On the other hand I often see functions like you describe - something has to be executed step-by-step (and the functionality is only used there) - where I _whish_ it was split up into separate functions, so we could have meaningful tests for each step, not only for the "whole thing".
How about "splitting for comprehension's sake"?
If I have a huge function, and I can peel parts off into sensible well-encapsulated sub-functions, and I name them well, then my ability to comprehend the whole goes up.
If I do that, future me will thank me, because I will almost inevitably be back to that function at some point.
But for this to work, the sub-functions have to really do what they say, and not do anything more. I have to be able to trust that I can understand them by just their name and arguments.
What would be a good example of the kinds of things a 100 line function would be doing?
I don't see that in my world so i'm naively trying to inline functions in codebases i'm familiar with and not really valuing the result i can dream up.
For one, my tests would be quite annoying, large and with too much setup for my taste. But i don't think i'd like to have to scroll a function, especially if i had to make changes to the start and end of the function in one commit.
I'm curious of the kinds of "long script" flavoured procedures, what are they doing typically?
I ask because some of the other stuff you mentioned i really strongly agree with like "Focus on separating pure code from stateful code" - this is such an under valued concept, and it's an absolute game changer for building robust software. Can i extract a pure function for this and separately have function to coordinate side effects - but that's incompatible with too long functions, those side effectfull functions would be so hard to test.
> What would be a good example of the kinds of things a 100 line function would be doing?
For me i see it alto in older video game engines with a switch statement that has 100s of possible states that need to be taken into account.
That kind of code is less common these days but its nice to be able to see all the possible states in a single switch statements vs them all being abstracted away in multiple classes that handle a subset of each state transition.
This is one case where having a super long function makes readability far better than abstracting away the states into multiple classes that all need to be read to understand the state of the world.
> What would be a good example of the kinds of things a 100 line function would be doing?
If you have a shopping list with 100 items, that does not mean the complexity of that is high.
Longer functions are worse, everything else being equal, but sometimes they have very low complexity.
To answer your question: if you have, for example, a function that returns for a key a certain value (basically a dictionary) for example for some translations that can be a very long function. Of course you could take the data out of the code into a data file, etc. but there is nothing wrong on principle about a function like that. A function like that is closer to extract state into data than one where that gets refactored into many small functions.
Containing the giant switch statement in a byte code interpreter or a tokeniser.
Funny this is the assertion in the I would most agree to use as general design principle to apply as thoroughly as possible, with tightened variable scope on equal position. Though no general principle should be followed blindly of course.
That's not the the function length per se. A function that is 1000 lines of mere basic assignments or holding a single giant switch can sometime be an apt option with careful consideration of tradeoffs as origin of the design. Number of line doesn't tell much of the function complexity and cognitive load of will imply to grasp what it does, though it can be a first proxy metric.
But most of the time giant functions found in the wild grow up organically with 5 levels of intertwined control control moving down and up, accumulating variables instead of const without consideration to scope span. In that case every time a change is needed, the cognitive load to grasp everything that need to be considered to change this finding is extremely huge. All the more as this giant function most likely won't have an test suit companion, because good engineering practices are more followed at equal level on several points.
I have been programming professionally for 17 years and I think this guideline is fine. I have difficulty imagining a function of 70 lines that would not be better off being split into multiple functions. It is true that if a function is just a list of stuff longer functions can be allowed then when it does multiple different things but 70 lines is really pushing that.
i see these rules and think “70 lines wow that’s short”
and then i read code and see a 50 line function and am like “wow this function is doing a lot”
sure strict rules aren’t amazing, but i think it would be cool to have a linter warning when there are more than X functions with over 70 lines (this is language dependent - verbosity)
I'm on the fence about this.
I see huge >130 line functions as a liability. There's so much state in it that a mistake on one line is not obvious. It makes those functions "sticky" and they tend to become the center of a lot of call graphs... like a neutron star. When a mistake is made maintaining or modifying this function it tends to have far-reaching side effects.
On the other hand some APIs (looking at you, OpenGL) are just so verbose that you can't avoid long functions.
I think it's generally good to compose functions from smaller functions where possible. Sometimes you can't and probably shouldn't. But it's hard to give a quantifiable rule in my experience. Approximations work but will never be perfect.
At first, I was thinking the same but then realized this is over a full page of code. It isn't an insane rule of thumb at all.
At least we aren't talking about "clean code" level of absurdity here: 5-20 lines with 0 - 2 parameters.
> They're used exactly once
To me, that's key here. That things are scattered over multiple files is a minor issue. Any competent IDE can more or less hide that and smoothen the experience. But if you have factored some code into a function, suddenly other places may call it. You have inadvertently created an API and any change you make needs to double check that other callers either don't exist or have their assumptions not suddenly violated. That's no issue if the code is right therr. No other users, and the API is the direct context of the lines of code right around it. (Yes you can limit visibility to other modules etc but that doesn't fully solve the issue of higher cognitive load.)
That is an important consideration which you should weigh against the clarity gained by breaking up your code in units of logic.
That is precisely the reason why for a function the scope of its container should be constrained. A class with a Singe Responsibility (SRP) would not suffer from several private methods. Breaking up your methods in a God Class brings both a decrease and an increase of mental load.
Also, in functional programming languages one can nest functions, and OOP languages oftentimes can nest classes.
Haskell does function hiding the most elegant imo, by means of `where`:
The point of breaking down the long function in God Classes , is to prepare the code for a second refactoring, which is usually extract class, long params list, etc..
If you were to simply leave it a mess like that, you would be right.
> These things weren't clear to me the first 10 years, but after 30 years, I've made those conclusions. I hope other old-timers can chime in on this.
'Sup, it's me, the "new dev". Except I, too, have been at it for decades, and I get more and more attached to short functions year over year. (You are correct about composition and about isolating state mutations. But short functions are tools that help me to do those things. Of course, it helps a ton to have functions as first-class objects. Function pointers are criminally underused in C codebases from what I've seen. They can be used for much more than just reinventing C++ vtables.)
People put numbers on their advice because they don't trust the audience to have good taste, or to have a sense of the scale they have in mind. Of course that has the downside that metrics become targets. When I see a number in this kind of advice, I kinda take it in two passes: understand what kind of limit is proposed (Over or under? What is being limited?) and then go back and consider the numeric ballpark the author has in mind. Because, yes, 70 lines of Python is not the same as 70 lines of C.
But I can scarcely even fathom ten lines in a Python function that I write nowadays. And I'm rather skeptical that "LOC needed to represent a coherent idea" scales linearly with "LOC needed to make a whole program work".
> Now you have 12 functions, scattered over multiple packages, and the order of things is all confused, you have to debug through to see where it goes. They're used exactly once, and they're only used as part of a long process. You've just increased the cognitive load of dealing with your product by a factor of 12. It's downright malignant.
Well, no, that isn't what happens at all.
First off, the files where new functions get moved, if they get moved at all, are almost certainly going to be in the same "package" (whatever that means in the programming language in use). The idea that it might be hard to find the implementation code for something not in the current file, is pretty close to being a problem unique to C and C++. And I'm pretty sure modern IDEs have no problem dealing with that anyway.
Second, it absolutely does not "increase the cognitive load by a factor of 12". In my extensive experience, the cognitive load is decreased significantly. Because now the functions have names; the steps in the process are labelled. Because now you can consider them in isolation — the code for the adjacent steps is far easier to ignore.
Why would you "have to debug through to see where it goes"? Again, the functions have names. If the process really is purely sequential, then the original function now reads like a series of function calls, each naming a step in the sequence. It's now directly telling you what the code does and how. And it's also directly telling you "where it goes": to the function that was called, and back.
You also no longer have to read comments interspersed into a longer code flow, or infer logical groupings into steps. You can consider each step in isolation. The grouping is already done for you — that's the point. And if you aren't debugging a problem, that implies the code currently works. Therefore, you don't need to go over the details all at once. You are free to dig in at any point that tickles your curiousity, or not. You don't have to filter through anything you aren't interested in.
(Notice how in the three paragraphs above, I give one-sentence descriptions in the first paragraph of individual advantages, and then dedicate a separate paragraph to expanding on each? That is precisely the same idea of "using short functions", applied to natural language. A single, long paragraph would have been fewer total words, but harder to read and understand, and less coherent.)
All of that said, you don't really debug code primarily by single-stepping through long functions, do you? I find problems by binary search (approximately, guided by intuition) with breakpoints and/or logging. And when the steps are factored out into helper functions, it becomes easier to find natural breakpoints in the "main" function and suss out the culprit.
Shorter functions absolutely do have the properties described in the quote. Almost definitionally so. Nobody really groks code on the level of dozens of individual statements. We know brains don't work like that (https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus...).
What has always baffled me is how CS uses the word "safety" where all other industries use "robustness".
A robust design is one that not only is correct, but also ensures the functionality even when boundary conditions deviate from the ideal. It's a mix of stability, predictability and fault tolerance. Probably "reliable" can be used as a synonym.
At the same time, in all industries except CS "safety" has a specific meaning of not causing injuries to the user.
In the design of a drill, for example, if the motor is guaranteed to spin at the intended rpm independently of orientation, temperature and state of charge of the battery, that's a robust design. You'll hear the word "safe" only if it has two triggers to ensure both hands are on the handles during operation.
You use safety more in relation to correctness aspects of algorithms. Some safety properties you can actually prove. When it comes to robustness it is more about dealing with things sometimes being incorrect regardless of the safety mechanisms. So, a try catch for something that isn't actually expected to normally fail makes you robust against the scenario when that does fail. But you'd use e.g. a type system to prevent classes of failures as safety mechanism.
It's a very soft distinction, I agree. And possibly one that translates less well to the physical world where wear and tear are a big factor for robustness. You can't prove an engine to be safe after thousands of hours. But you can make it robust against a lot of expected stuff it will encounter over those hours. Safety features tend to be more about protecting people than the equipment.
Without any context safety _can_ mean a lot of things, but it's usually used as a property of a system and used alongside liveness.
Basically, safety is "bad things won't happen" and liveness is "good things eventually happen".
This is almost always the way safety is used in a CS context.
> What has always baffled me is how CS uses the word "safety" where all other industries use "robustness".
FWIW "safety factors" are an important part of all kinds of engineering. The term is overloaded and more elusive in CS because of the protean qualities of algorithmic constructs, but that's another discussion.
I think in things like philosophy/debate/etc it's common to talk about "safe assumptions". I always figured that's the metaphor being leaned on here.
I agree it's confusing when you step outside of CS world though.
> At the same time, in all industries except CS "safety" has a specific meaning of not causing injuries to the user.
That makes sense, as CS is transversal to industries. Same practices that can contribute to literally save lifes in one domain will just avoid minor irritating feeling in an other.
Pretty good list, but a hidden assumption is that the reader works in an imperative style. For instance, recursion is the bread and butter of functional and logical programming and is just fine.
The most important advice one can give to programmers is to
I got this sense as well, particularly from the section about assertions. Most of the use cases they describe (e.g checking function arguments and return values) are much better handled by an expressive type system than ad-hoc checks.
If you can, yes.
But that last 10% of checking may be really hard to encode in types. It may be especially hard to do so in the language that you want to use for other reasons.
The "split long functions" advice works well for most code but falls apart in transaction processing pipelines.
I work on financial data processing where you genuinely have 15 sequential steps that must run in exact order: parse statement, normalize dates, detect duplicates, match patterns, calculate VAT, validate totals, etc. Each step modifies state that the next step needs.
Splitting these into separate functions creates two problems: (1) you end up passing huge context objects between them, and (2) the "what happens next" logic gets scattered across files. Reading the code becomes an archaeology exercise.
What I've found works better: keep the orchestration in one longer function but extract genuinely reusable logic (date parsing, pattern matching algorithms) into helpers. The main function reads like a recipe - you can see the full flow without jumping around, but the complex bits are tucked away.
70 lines is probably fine for CRUD apps. But domains with inherently sequential multi-step processes sometimes just need longer functions.
> Each step modifies state that the next step needs.
I've been bitten by this. It's not the length that's the problem, so much as the surface area which a long function has to stealthily mutate its variables. If you have a bunch of steps in one function all modifying the same state, there's a risk that the underlying logic which determines the final value of widely-used, widely-edited variables can get hard to decipher.
Writing a function like that now, I'd want to make very sure that everything involved is immutable & all the steps are as close to pure functions as I can get them. I feel like it'd get shorter as a consequence of that, just because pure functions are easier to factor out, but that's not really my objective. Maybe step 1 is a function that returns a `Step1Output` which gets stored in a big State object, and step 2 accesses those values as `state.step1Output.x`. If I absolutely must have mutable state, I'd keep it small, explicit, and as separate from the rest of the variables as possible.
I'll admit this may be naive, but I don't see the problem based on your description. Split each step into its own private function, pass the context by reference / as a struct, unit test each function to ensure its behavior is correct. Write one public orchestrator function which calls each step in the appropriate sequence and test that, too. Pull logic into helper functions whenever necessary, that's fine.
I do not work in finance, but I've written some exceptionally complex business logic this way. With a single public orchestrator function you can just leave the private functions in place next to it. Readability and testability are enhanced by chunking out each step and making logic obvious. Obviously this is a little reductive, but what am I missing?
The typestate pattern common in Rust applications allows the compiler to verify that the operations are executed in the right order and that previous states are not accidentally referenced. Here’s a good description: https://cliffle.com/blog/rust-typestate/
I frequently write software like this (in other domains). Structs exist (in most languages) specifically for the purpose of packaging up state to pass around, I don't really buy that passing around a huge context is the problem. I'm not opposed to long functions (they can frequently be easier to understand than deep callstacks), especially in languages that are strictly immutable like Clojure or where mutation is tightly controlled like Rust. Otherwise it really helps to break down problems into sub problems with discrete state and success/failure conditions, even though it results in more boilerplate to manage.
This is an admirable goal but complicated by the inevitable conditional logic around what to do in certain situations. Push the conditions into the sub methods, or keep them in primary method? Thinking about what are testable chunks of logic can be a valuable guide. A lot of discipline is required so these primary methods dont become spaghetti themselves.
Lots of debating about the color to paint the fence in the design meetings for the nuclear reactor...
I don't know how this philosophy is applied at TigerBeetle. When I establish engineering guidelines I try to frame them as exactly that: guidelines. The purpose is to spawn defensible reasoning, and to trigger reflection.
For example, I might say this:
"Style," "philosophy," "guides," they're all well-meaning and often well-informed, but you should be in command of the decision as the developer and not forget your own expertise around cohesion, cognitive load, or any functional necessities.There are staunch believers in gating deploys based solely on LOCs, I'm sure... I like the idea of finding ways to transparently trigger cognitive provocations in order for everyone to steer towards better code without absolutes.
Zero technical debt certainly is... ambitious. Sure, if we knew _what_ to build the first time around this would be possible. From my experience, the majority of technical debt is sourced from product requirement changes coupled with tight deadlines. I think even the most ardent follower of Tiger Style is going to find this nigh impossible.
I would even say that from a project management perspective, zero technical debt is undesirable. It means you have invested resources into perfecting something that, almost by definition, could have waited a while, instead of improving some more important metric such as user experience. (I do understand tech debt makes it harder to work with the codebase, impacting all metrics, I just don’t think zero tech debt is a good target.)
> perfecting something that, almost by definition, could have waited a while
No technical debt is not the same thing as “perfection”. Good enough doesn’t mean perfect.
Would it be ok to submit an essay with only 90% of the underlined spelling mistakes fixed? Do you paint your outdoor table but leave the underside for later?
Do it once, do it right. That doesn’t mean perfect, it means not cutting corners.
Would you keep fixing the underlined spelling mistakes on your “watch out for holes in the pavement” sign while people are already walking there?
There are contexts where quick and dirty and (hopefully) come back later are warranted. Far more often it is just an excuse for shoddy work. You used the word “perfection” as the contrast to “technical debt”. Granted, technical debt is not a well defined term, but I am simply highlighting that “free from technical debt” in no way implies anything like perfect. It just implies well made.
My argument works just as well if you replace “perfecting” with “improving”.
Technical debt is not a current defect. It just means that for the sake of having something quickly done today, you accept that the cost of changing stuff tomorrow will be greater than normal. If you never have to change something (switching jobs, consultancy project you don’t care about,…) then it may be a great trade off.
Ironically, some of the worst tech debt I’ve ever dealt with has been because the initial implementation was an overengineered disaster by an dev who thought they were solving all possible problems before we really understood what all possible problems are.
“Zero tech debt” is an impossibility. The most elegant solutions incur some kind of tech debt, it’s just less than others. More realistic than “zero tech debt” is a continuing dedication to addressing tech debt combined with using implementations that minimize “one way doors”.
> Limit line lengths: Keep lines within a reasonable length (e.g., 100 characters) to ensure readability. This prevents horizontal scrolling and helps maintain an accessible code layout.
Do you not use word wrap? The downside of this rule is that vertical scrolling is increased (yes, it's easier, but with a wrap you can make that decision locally) and accessibility is reduced (and monitors are wide, not tall), which is especially an issue when such a style is applied to comments so you can't see all the code in a single screen due to multiple lines of comments in that long formal grammatically correct style
Similarly, > Limit function length: Keep functions concise, ideally under 70 lines.
> and move non-branching logic to helper functions.
Break accessibility of logic, instead of linearly reading what's going on you have to jump around (though popups could help a bit). While you can use block collapse to hide those helper blocks without losing their locality and then expand only one helper block.
> which is especially an issue when such a style is applied to comments so you can't see all the code in a single screen due to multiple lines of comments in that long formal grammatically correct style
It’s easier to hide comments than do code wrapping correctly. And comments are usually in a lighter color than code and easy to skip over.
> Break accessibility of logic, instead of linearly reading what's going on you have to jump around (though popups could help a bit)
Extracting function is for abstraction, instead of deciphering some block and tracking its entanglement, you have a nice name and a signature that document its input and output. And maybe a nice docstring that summarizes its purpose.
It does require taste and design skill.
> a lighter color than code and easy to skip over.
How does color and the ease of skipping address the fact that you don't see code because it's pushed off screen? Especially compared to the ease of skipping when you wrap the whole paragraph of comments into a single long line when you don't need to read it.
> And maybe a nice docstring that summarizes its purpose. Which you could just as well have right there where you read the block.
The nice name/signature yes, that can be situationally better
There’s place where you can break the code before 100 or even 80 line limit. And the only reason that you cannot do so with code is excessive indentation (nested scope). Which you will find easily in very long functions.
> Which you could just as well have right there where you read the block.
A function forces encapsulation and make explicit the external factors. A block doesn’t make clear which variables in the parent scope it’s using or if everything in the block is one logical step.
Can you have a coding philosophy that ignores the time or cost taken to design and write code? Or a coding philosophy that doesn't factor in uncertainty and change?
If you're risking money and time, can you really justify this?
- 'writing code that works in all situations'
- 'commitment to zero technical debt'
- 'design for performance early'
As a whole, this is not just idealist, it's privileged.
I would argue that these:
- 'commitment to zero technical debt'
- 'design for performance early'
Will save you time and cost in designing, even in the relatively near term of a few months when you have to add new features etc.
There's obviously extremes of "get something out the door fast and broken then maybe neaten it up later" vs "refactor the entire codebase any time you think soemthing could be better", but I've seen more projects hit a wall due to leaning to far to the first than the second.
Either way, I definitely wouldn't call it "privileged" as if it isn't a practical engineering choice. That seems to judt frame things in a way where you're already assuming early design and commitment to refactoring is a bad idea.
Your argument hinges on getting the design right, upfront. That assumes uncertainty is low or non-existent.
Time spent, monetary cost, and uncertainty, are all practical concerns.
An engineering problem where you can ignore time spent, monetary cost, and uncertainty, is a privileged position. A very small number of engineering problems can have an engineering philosophy that makes no mention of these factors.
Their product is a high-throughput database for financial transactions, so they might have different design requirements than the things you work on.
Yes, my point is it's a privilege to have an engineering philosophy that doesn't need to address time spent, monetary cost, or uncertainty.
It’s the equivalent of someone running on a platform where there would be world peace and no hunger.
That’s great and all as an ideal but realistically impossible so if you don’t have anything more substantial to offer then you aren’t really worth taking seriously.
You forgot “get it right first time” which goes against the basic startup mode of being early to the market or die. For some companies, trying to get it right the first time may make sense but that can easily lead to never shipping anything.
I would not hire a monk of TigerStyle. We'd get nothing done! This amount of coding perfection is best for hobby projects without deadlines.
This seems pretty knee-jerk. I do most of this and have delivered a hell of a lot of software in my life. Many projects are still running, unmodified, in production, at companies I’ve long since left.
You can get a surprising amount done when you aren’t spending 90% of your time fighting fires and playing whack-a-mole with bugs.
Well, I'm sure you're well aware of perils of premature optimization and know how to deliver a product within a reasonable timeframe. TigerStyle seems to me to not be developed through the lens of producing value for a company via software, but rather having a nice time as a developer (see: third axiom).
I'm not saying the principles themselves are poor, but I don't think they're suitable for a commercial environment.
TigerStyle is developed in the context of TigerBeetle, who seem to be a successful company getting a lot done.
I had the same association but interestingly this version appears to be a "remix" of TigerBeetle's style guide, by an unrelated individual. At a glance, there is a lot of a crossover but some changes as well.
I think the point is well made though. When you're building something like a transactions database, the margin for error is rather low.
Then I'm curious about what their principles on deadlines. I don't see how it aligns with their coding styleguide. Taking the TigerStyle at face value does not encourage deliverance. They're practically saying "take the time you need to polish your thing to perfection, 0 technical debt".
But ofc, I understand styleguides are... well.. guides. Not law.
Why do you think developer enjoyment is orthogonal to productivity and delivery?
Which ones, specifically?
I don't really see anything in it that particularly difficult our counter-productive. Or, to be honest, anything that isn't just plain good coding practice. All suitably given as guidelines not hard and fast rules.
The real joy of having coding standards, is that it sets a good baseline when training junior programmers. These are the minimum things you need to know about good coding practice before we start training you up to be a real programmer.
If you are anything other than a junior programmer, and have a problem with it, I would not hire you.
Which parts of it seem objectionable?
This does not apply to you. You use a garbage collector.
The attribution to TigerBeetle should be at the top of the page with a link to the original tigerstyle, not buried at the bottom. Right now it reads like official TigerBeetle content until you scroll down, which isn't fair to either you or the original team.
The author graciously gifted the domain to us and we’re literally days away from launching original TigerStyle here.
That's really nice of the author and awesome to hear. Would love to see TigerStyle be published in a similar format and expanded upon with more detail be great for referencing with my colleagues. Would love to adopt it on projects or some of the rules at least
Oh that's very nice of them.
This just makes me want to write sloppy dangerous throwaway code with wild abandon
Not to midwit but I will stick to MISRA C for this sort of thing. It is very clear in why each thing must be done and it is hard to argue against most of it. It’s usually where safety and robustness deeply matter and using it would have avoided a number of high-profile exploits we have seen in the recent past. While not every “rule” may be applicable to all languages, many standard languages could adopt a good number of rules from it and benefit greatly. Anyone with a number of years experience with some reflection on what is good, clear and unambiguous style would reinvent a decent portion of MISRA C.
I’ll let others speak to the Barr standard.
This advice is great but lacks context.
For many shops it’s too much effort for the payoff. Unless you work on medical devices or aerospace then maybe.
Your situation will warrant some of these things, or doing them to a lesser degree. Part of our role is to decide and recommend what’s appropriate.
Just curious, is this something that evolved trying to tame agentic coding with rules ? because it does feel like something I will put in my CLAUDE.md
> Avoid recursion if possible to keep execution bounded and predictable, preventing stack overflows and uncontrolled resource use.
In languages with TCO (e.g. Haskell, Scheme, OCaml, etc.) the compiler can rewrite to a loop.
Some algorithms are conceptually recursive and even though you can rewrite them, the iterative version would be unreadable: backtracking solvers, parsing trees, quicksort partition & subprblems, divide-and-conquer, tree manipulation, compilers, etc.
Presumably why it says "Avoid recursion if possible".
The orginal piece is a much more nuanced read, I feel this one misses some context.
https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TI...
Interesting use of `latency_ms_max` as a naming convention. I'm definitely guilty of `max_latency_ms` instead, but they make a convincing argument for putting `max` at the end.
If this topic floats your boat, go look up the NASA coding standards. For a few projects, I tried to follow a lot of their flow control recommendations, and will still reach for: `while ... && LIMIT > 0` in some situations.
Still a huge fan of including some type info in the variable name, eg: duration_s, limit_ms makes it extremely clear that you shouldn't mix math on those integers.
"Zero technical debt" - I doubt this is feasible in practice.
I'm not even clear on what it means, technical debt is non-deterministic in many cases.
To say it differently: if you wrote code that was perfect in time 0, that code may become legacy in time 100.
Are they saying you should continuously refactor all your code to cover the 'current user needs'?
I just think it's an oversimplification for those cases where you don't mind not covering the 0,001% of use cases.
Even if it is, it sounds highly ineffective, unless the only value you are delivering is source code.
I like the 100% philosophy for coding:
1. 100% code coverage 2. 100% branch coverage 3. 100% lint (without noqa) 4. 100% type check pass(for python/js) 5. 100% documentation coverage 6. All functions with complexity less than 5. All functions with no of lines less than 70. All files with number of lines less than 1000.
These make code high quality, and quality of life is directly proportional to qualify of your code.
Related: https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TI...
> Do it right the first time: Take the time to design and implement solutions correctly from the start.
Doing good design is off course important, but on the other hand software design is a lot of times iterative because of unknown unknown s. Sometimes it can be better to create quick prototype(s) to see which direction is the best to actually "do it right", instead of spending effort designing something that in the end won't be build.
> Allocate all necessary memory during startup and avoid dynamic memory allocation after initialization. Absolutely not an ultimatum advice, even harmful in most cases. Just measure you memory usage and don't rely on and avoid external OOM handler.
> Allocate all necessary memory during startup and avoid dynamic memory allocation after initialization.
Why?
I think they are hinting at the idea that a long running process on a dedicated server should allocate all necessary memory during initialization. That way you never accidentally run out of memory during high loads, a time when you really don't want your service to go down.
You would need so much more physical memory with this approach. For example, you may want to allocate a lot of memory for the browser, but then close the tab. 2 seconds later you want to use it for a compiler. And then you switch to a code editor.
You can run out of memory and trigger a crash.
Master recursion and there will be nothing left to master. Avoid recursion and you will remain forever stuck in a loop.
See also: Rugged Manifesto
“Rugged” describes software development organizations that have a culture of rapidly evolving their ability to create available, survivable, defensible, secure, and resilient software.
https://ruggedsoftware.org/
https://github.com/rugged-software/rugged-software.github.io
This is the kind of development that one needs for safety critical applications. E.g., nuclear power plants or airplane control software. I don't think it is economically feasible for less critical software. It presumes a great degree of stability in requirements which is necessary for such applications.
Indeed, a lot of it is pulled from NASA's "Power of Ten – Rules for Developing Safety-Critical Code." The original tigerbeetle doc cites them explicitly: https://github.com/tigerbeetle/tigerbeetle/blob/ac75926f8868...
a zero technical debt approach is good sometimes, but im not sure i would take it this far. It depends.
Limit function length: Keep functions concise, ideally under 70 lines. Shorter functions are easier to understand, test, and debug.
The usual BS... yes, shorter functions are easier to understand by themselves but what matters, especially when debugging, is how the whole system works.
Edit: care to refute? Several decades of experience has shown me what happens. I'm surprised this crap is still being peddled.
This one, like some others in this style guide, can also be found in Clean Code. Not sure why you feel the need to call it "BS". Nobody is saying that your 75 line function is bad.
It's a reasonable guideline. Juniors won't do this automagically.
Clean Code is different. Clean Code suggests 2-5 line functions. A 70-line limit is worth considering; 5 lines is not.
The "Clean Code" book is famous for making bad suggestions and producing unreadable code with too many functions just for the sake of following its rules.
It's worth noting that the author of "Clean Code" does not appear to have written any software of significance. He's nothing more than a preaching salesman.
There are far more real developers to learn good practices from. Brian Kernighan, Dennis Ritchie, Linus Torvalds, etc.
Yes and it was BS in Clean Code too, not a fan.
Don't get me wrong, I often apply it myself and refactor code into smaller functions. But readability, understandability and ease of maintenance comes first.
Especially juniors will apply such rules overzealously, bending over backwards to stay within an arbitrary limit while at the same time not really understanding why.
Frustratingly, their lack of experience makes it impossible to discuss such dogmatic rules in any kind of nuanced way, while they energetically push for stricter linter rules etc.
I've tried, and one even argued there's never ever a reason to go over N lines because their book is "best-practice" and said so. And you should not deviate from "best-practice" so that's it. I'm not making this up!
That said, I'm always open to discuss the pros and cons for specific cases, and I do agree the default should be to lean towards smaller functions in 90% of cases.
It’s a lot easier to understand entire systems when they’re composed of small, pure, straightforward parts.
Yeah, you can’t build a building unless you’re certain that every brick, rebar, and cement is all solid and meets a quality bar. Of course you need architectural schematics to make sense of the broader picture, but you need quality control on the pieces being put in place too.
No it isn't. The whole system should be small.
The low level stuff is also important.
I like this, to me it reads like a collection of fairly obvious best practices (even if there might be practical reasons to avoid when shipper fast etc), so I'm surpised to see so many enraged comments.
Any recommendations for other coding philosophies or "first principle" guides? I know of "extreme programming" but not much else.
A Philosophy of Software Design by John Ousterhout is often recommended, and is very good.
There was an interesting debate between John and Uncle Bob on their differences in style recently[1], with a related HN discussion[2].
[1] https://github.com/johnousterhout/aposd-vs-clean-code/blob/m...
[2] https://news.ycombinator.com/item?id=43166362
I would like to note that this text was transformative in my current project (an effort to model CNC work in 3D) --- when I first read the book, I did it chapter-by-chapter, applying the lesson learned from that chapter to the entire codebase before moving on to the next --- I am currently re-reading the book for the third time, and I'm planning on a compleat re-write before I do 1.0....
Things an engineer early in his career would write.
Or things a senior engineer would write late in their career to make it easier to train junior programmers. <shrugs>
TigerBeetle is not early engineer lol
this smells like Clean Code BS. no real substance either, I could have made ChatGPT generate this article
I literally laughed out loud when I got to this line
> Do it right the first time
So easy, why didn't I think of that!? /s
Reminds me of the mental health meme of telling depressed people to just be happier instead.
I don't necessarily disagree with a lot in this philosophy, but much of it is puffery if not accompanied by practical positive and negative examples. If a junior with little experience reads this, I'm not sure if they'll be better or worse off.
For example, "Design for performance early" is dangerous if it leads to premature optimization. But that's not mentioned. Practical positive and negative examples that illustrate the balance between these two concerns would make the advice actionable.
> If a junior with little experience reads this
Won't happen. This is for TigerBeetle.