top | item 46075628

Tiger Style: Coding philosophy (2024)

129 points| nateb2022 | 3 months ago |tigerstyle.dev | reply

135 comments

order
[+] keyle|3 months ago|reply
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.

[+] stouset|3 months ago|reply
“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.

[+] rhubarbtree|3 months ago|reply
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.

[+] zem|3 months ago|reply
> 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.

[+] igogq425|3 months ago|reply
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%!

[+] frje1400|3 months ago|reply
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.
[+] teo_zero|3 months ago|reply
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.

[+] jillesvangurp|3 months ago|reply
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.

[+] asimpletune|3 months ago|reply
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.

[+] inejge|3 months ago|reply
> 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.

[+] benrutter|3 months ago|reply
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.

[+] psychoslave|3 months ago|reply
> 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.

[+] exceptione|3 months ago|reply
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.
[+] bfLives|3 months ago|reply
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.
[+] mjlawson|3 months ago|reply
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.
[+] zoul|3 months ago|reply
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.)
[+] NeutralCrane|3 months ago|reply
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”.

[+] thomascountz|3 months ago|reply
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.

[+] eviks|3 months ago|reply
> 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.

[+] skydhash|3 months ago|reply
> 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.

[+] nilirl|3 months ago|reply
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.

[+] benrutter|3 months ago|reply
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.

[+] titanomachy|3 months ago|reply
Their product is a high-throughput database for financial transactions, so they might have different design requirements than the things you work on.
[+] NeutralCrane|3 months ago|reply
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.

[+] brabel|3 months ago|reply
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.
[+] aranw|3 months ago|reply
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.
[+] jorangreef|3 months ago|reply
The author graciously gifted the domain to us and we’re literally days away from launching original TigerStyle here.
[+] baalimago|3 months ago|reply
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.
[+] stouset|3 months ago|reply
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.

[+] rerdavies|3 months ago|reply
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.

[+] hansvm|3 months ago|reply
Which parts of it seem objectionable?
[+] adfawlolass|3 months ago|reply
This does not apply to you. You use a garbage collector.
[+] binary132|3 months ago|reply
This just makes me want to write sloppy dangerous throwaway code with wild abandon
[+] andsoitis|3 months ago|reply
> 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.

[+] rerdavies|3 months ago|reply
Presumably why it says "Avoid recursion if possible".
[+] abhashanand1501|3 months ago|reply
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.

[+] andsoitis|3 months ago|reply
> Allocate all necessary memory during startup and avoid dynamic memory allocation after initialization.

Why?

[+] Manfred|3 months ago|reply
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.

[+] alextingle|3 months ago|reply
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.

[+] Cwizard|3 months ago|reply
You can run out of memory and trigger a crash.
[+] hairdrop|3 months ago|reply
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.

[+] ramses0|3 months ago|reply
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.

[+] huqedato|3 months ago|reply
"Zero technical debt" - I doubt this is feasible in practice.
[+] harlequinetcie|3 months ago|reply
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.

[+] dionian|3 months ago|reply
Even if it is, it sounds highly ineffective, unless the only value you are delivering is source code.
[+] jakubmazanec|3 months ago|reply
> 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.

[+] bicepjai|3 months ago|reply
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