42 comments

[ 3.0 ms ] story [ 64.9 ms ] thread
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.

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.
(comment deleted)
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.

> 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 t...

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:

    def process_order(order):
        _validate(order)
        _reserve(order)
        _charge(order)
        _confirm(order)
    
    def _validate(order):
        ...
    
    def _reserve(order):
        ...
> Now you have 12 functions, scattered over multiple packages, [...]

Who says that the 12 functions need to be in multiple packages? And you can also keep them in the order you call them.

Having 12 functions is probably easier to test, too.

Yes, the 70 lines rules is arbitrary. But it's only as arbitrary as using 80 columns.

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.
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.

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.

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.

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.

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.

this smells like Clean Code BS. no real substance either, I could have made ChatGPT generate this article
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.
> 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.

> Allocate all necessary memory during startup and avoid dynamic memory allocation after initialization.

Why?

Two reasons.

1. Dynamic memory allocation is error prone. Mixing it in with your control flow makes that hard to manage.

Many of the strategies for keeping track of dynamic memory add significantly to the complexity of the program... You need to consider object ownership, maybe introduce reference counting, even garbage collection, or "borrow checkers".

If you can avoid all of that by making good architectural choices up front then your code will be much simpler and robust.

2. Dynamic allocation is slow, especially in a multithreaded environment. It's doubly slow if you layer garbage checking or whatever on top of it.

Prefer fewer trips to the allocator, preferably none in your code's hot path.

Things an engineer early in his career would write.
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.
> 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.

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.
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.

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.
This just makes me want to write sloppy dangerous throwaway code with wild abandon
> 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.