John Carmack on inlined code (2014)
number-none.com
number-none.com
Concretely related to the topic, I've often found myself inlining short pieces of one-time code that made functions more explicit, while at other times I'll spend days just breaking up thousand line functions into simpler blocks just to be able to follow what's going on. In both cases I was creating inconsistencies that younger developers nitpick -- I know I did.
My goal in most cases now is to optimize code for the limits of the human mind (my own in low-effort mode) and like to be able to treat rules as guidelines. The trouble is how can you scale this to millions of developers, and what are those limits of the human mind when more and more AI-generated code will be used?
And this led to a really good discussion pulling the reference architecture of this system apart and understanding what it optimizes for (resilience and fault tolerance), what it sacrifices (cost, number of systems to maintain) and what we need. And yes, following the reference architecture in one place and breaking it in another place makes sense.
And I think that understanding the different options, as well as the optimization goals setting them apart, allows you to make a more informed decision and allows you to make a stronger argument why this is a good decision. In fact, understanding the optimization criteria someone cares about allows you to avoid losing them in topics they neither understand nor care about.
For example, our CEO will not understand the technical details why the reference architecture is resilient, or why other choices are less resilient. And he would be annoyed about his time being wasted if you tried. But he is currently very aware of customer impacts due to outages. And like this, we can offer a very good argument to invest money in one place for resilience, and why we can save money in other places without risking a customer impact.
We sometimes follow rules, and in other situations, we might not.
I think the truth is that we just CAN'T scale that way with the current programming languages/models/paradigms. I can't PROVE that hypothesis, but it's not hard to find examples of big software projects with lots of protocols, conventions, failsafes, QA teams, etc, etc that are either still hugely difficult to contribute to (Linux kernel, web browsers, etc) or still have plenty of bugs (macOS is produced by the richest company on Earth and a few years ago the CALCULATOR app had a bug that made it give the wrong answers...).
I feel like our programming tools are pretty good for programming in the small, but I suspect we're still waiting for a breakthrough for being able to actually make complex software reliably. (And, no, I don't just mean yet another "framework" or another language that's just C with a fancier type system or novel memory management)
Just my navel gazing for the morning.
In the physical world, when we build something complex like a car engine, a microprocessor, or bookcase, the laws of physics guide us and help prevent invalid states. Not all of them -- an upside down bookcase still works -- but a lot of them.
Of course, part of the problem is that when we build the software equivalent of an upside down bookcase, we 'patch' it by creating trim and shims to make it look better and more structurally sound instead of tossing it and making another one the right way.
But mostly, we write software in a way that allows for a ton of incorrect states. As a trivial example, expressing a person's age as an 'int', allowing for negative numbers. As a more complicated example, allowing for setting a coupon's redemption date when it has not yet been clipped.
I would argue that that's actually a better guide to how we manage complexity in the physical world. Mechanical engineers generally like to minimize the number of moving parts in a system. When they can't avoid moving parts, they tend to fixate on them, and put a lot of effort into creating linkages and failsafes to try to prevent them from interacting in catastrophic ways.
The software engineering way would be to create extra moving parts just because complicated things make us feel smart, and deal with potential adverse interactions among them by posting signs that say "Careful, now!" without clearly explaining what the reader is supposed to be careful of. 50 years later, people who try to stick to the (very sound!) principles that Backus proposed are still regularly dismissed as being hipsters and pedants.
We will never get rid of the need for QA. Automated tests are great, I believe in them (Note that I didn't say unit tests or integration tests). Formal proofs appear great (I have never figured out how to prove my code), but as Knuth said "Beware of bugs in the above code; I have only proved it correct, not tried it". There are many ways code can be meet the spec and yet wrong because in the real world you rarely understand the problem well enough to write a correct spec in the first place. QA should understand the problem well enough to say "this isn't what I expected to happen."
Imagine you are limited to only a few ingredients in programming: statements, expressions, functions, objects, arrays, and operators that are not overloaded. That list does not contain classes, inheritance, declarative helpers, or a bunch of other things. With a list of ingredients so small no internal structure or paradigm is imposed on you, so you are free to create any design decisions that you want. Those creative decisions about the organization of things is how you dictate the scale of it all.
Most people, though, cannot operate like that. They claim to want the freedom of infinite scale, but they just need a little help. With more help supplied by the language, framework, whatever the less freedom you have to make your own decisions. Eventually there is so much help that all you do as a programmer is contend with that helpful goodness without any chance to scale things in any direction.
To be fair here, I don't think it's reasonable to expect that once you have "software development skills" it automatically gives you the ability to fix any code out there. The Linux Kernel and web browsers are not hard to contribute to because of conventions, they're hard because most of that code requires a lot of outside knowledge of things like hardware or HTML spec, etc.
The actual submitting part isn't the easiest, but it's well documented if you go looking, I'm pretty sure most people could handle it if they really had a fix they wanted to submit.
So, my comment was specifically about code. Yes, developing a kernel driver requires knowledge of the hardware and its quirks. But, if we're just talking about the code, why shouldn't a competent C developer be able to read the code for an existing hardware driver and come away understanding the hardware?
And what about the parts that are NOT related to fiddly hardware? For example, look at all of the recent drama with the Linux filesystem maintainer(s) and interfacing with Rust code. Forget the actual human drama aspect, but just think about the technical code aspect: The Rust devs can't even figure out what the C code's semantics are, and the lead filesystem guy made some embarrassing outbursts saying that he wasn't going to help them by explaining what the actual interface contracts are. It's probably because he doesn't even know what his own section of the kernel does in the kind of detail that they're asking for... That last part is my own speculation, but these Rust guys are also competent at working with C code and they can't figure out what assumptions are baked into the C APIs.
Web browser code has less to do with nitty gritty hardware. Yet, even a very competent C++ dev is going to have a ton of trouble figuring out the Chromium code base. It's just too hard to keep trying to use our current tools for these giant, complex, software projects. No amount of convention or linting or writing your classes and functions to be "easy to understand" is going to really matter in the big picture. Naming variables is hard and important to do well, but at the scale of these projects, individual variable names simply don't matter. It's hard to even figure out what code is being executed in a given context/operation.
I don't think this is true, or at least it wasn't circa 2018 when I was writing C++ professionally and semi-competently. I sometimes had to read, understand and change parts of the Chromium code base since I was working on a component which integrated CEF. Over time I began to think of Chromium as a good reference for how to maintain a well-organized C++ code base. It's remarkably plain and understandable, greppable even. Eventually I was able to contribute a patch or two back to CEF.
The hardest thing by far with respect to making those contributions wasn't understanding the C++, it was understanding how to work the build system for development tasks.
The Chromium code base is a joy to read and I would routinely spend hours just reading it to understand deeper topics relating to the JS runtime.
Compared to my company's much smaller code base that would take hours just to understand the most simplest things because it was written so terribly.
My point was that making meaningful contributions such a big fixes requires understanding how the code is _supposed_ to function vs. how it actually functions, that's the hard part. In the majority of cases that's simply not something the code can tell you, there's no replacement for comparing the code to a datasheet or reading the HTML spec to understand how the rendering engine is supposed to work, and those things take time to learn. For the simpler parts people do actively contribute to those without tons of previous experience (or because they already have experience with a library or etc.).
That's kind of my point, though. I'm trying to zoom out and "think outside the box" for a minute. It's hard to compose smaller pieces into larger systems if the smaller pieces have behavior that's not very well defined. And our programming languages and tools don't always make it easy for the author of a piece of code to always understand that they introduced some unintended behavior.
To your first point: I'm not shitting on Chromium or Firefox or any other software projects, but they're honestly ALL "buggy messes" in a sense. I'm a middling software dev and the software I write for my day job is definitely more buggy, overall, than these projects. So, I'm not saying that other developers are stupid (quite the opposite!). But, the fact that there are plenty of bugs at any given point in any of these projects is saying something important, IMO. If I use our current programming tools to write a Base64 encode/decode library, I can do a pretty good job and there's a good chance that it'll have zero bugs in a fairly short amount of time. But, using the same tools, there's absolutely no hope that I (we, you, whoever) could write a web browser that doesn't have any bugs. That's actually a problem! We've come to accept it because that's all we've got today, but my point is that this isn't actually an ideal place to settle.
I don't know what the answer is, but I think a lot of people don't even seem to realize there's a problem. My claim is that there is a problem and that our current paradigms and tools simply don't scale well. I'm not creative enough to be the one who has the eureka moment that will bring us to the next stage of our evolution, but I suspect that it's what we'll need to actually be able to achieve complex software that actually works as we intend it to.
Readability is for human optimization for self or for other people's posterity and code comprehension to the readers mind. We need a new way to visualize/comprehension code that doesn't involve heavy reading and the read's personal capabilities of syntax parsing/comprehension.
This is something we will likely never be able to get right with our current man machine interfaces; keyboard, mouse/touch, video and audio.
Just a thought. As always I reserve the right to be wrong.
This is stated as if surprising, presumably because we think of a calculator app as a simple thing, but it probably shouldn't be that surprising--surely the calculator app isn't used that often, and so doesn't get much in-the-field testing. Maybe you've occasionally used the calculator in Spotlight, but have you ever opened the app? I don't think I have in 20 years.
You and I only think it's complex because we've become accustomed to everything being complex when it comes to writing software. That's my point. The mathematical operations are not hard (even the "fancy" ones like the trig functions). Formatting a number to be displayed is also not hard (again, those $100 calculators do it just fine). So, why is it so hard to write the world's 100,000th calculator app that the world's highest paid developers can't get it 100% perfect? There's something super wrong with our situation that it's even possible to have a race condition between the graphical effects and the actual math code that causes the calculator to display the wrong results.
If we weren't forced to build a skyscraper with Lego bricks, we might stand a better chance.
Right, and that's my point: if all you want is a rock-solid computational platform, then you can use, for example, `bc`. (That's what I do.) I assume that Apple assumes that their users want something fancier than that, and it's there, with the fanciness of a shiny user interface on a less-exercised code path, that the bugs will inevitably come.
I don't assume that my usage pattern is common. (My usage pattern is to drop to `bc`.) I assume that Calculator usage isn't common, but, recognizing that that is an assumption and that the only way to get evidence is to ask, I asked:
> Maybe you've occasionally used the calculator in Spotlight, but have you ever opened the app?
And you answered, so now together we have double the evidence that I alone did before. :-)
(Speaking of... has anyone done a 80x24 TUI client for HN? That would be interesting to play with.)
lynx still exists
And so asking “will AI generated code help” is like asking “will AI generated blog spam help”?
No - companies with GitHub copilot are basically asking how do I self-spam my codebase
It’s great to get from zero to something in some new JS framework but for your core competancy - it’s like outsourcing your thinking - always comes a cropper
(Book still being written)
That's almost its primary purpose in my opinion... the CPU does not care about Ruby vs Python vs Rust, it's just executing some binary code instructions. The code is so that other people can change and extend what the system is doing over time and share that with others.
There's a (3) where they'll quickly confirm their hypothesis using godbolt (or similar) if in doubt or they want to actually think in binary.
Fortunately for the programming community, many of us are able to create useful or interesting things without that kind of depth
Computational thinking is the mathematical thinking.
What makes a journeyman successful is sticking to the rules of thumb, unless directed by a master.
What makes a master successful is knowing why the rules of thumb exist, what their limits are, when to not follow them, and being able to make up new rules.
They spend so much time chasing perfection that it negatively affects their output. Multiple times a day I find myself saying 'is that a realistic problem for our use case?'
I don't blame them, it's admirable. But I feel like we need to teach YAGNI. Anymore I feel like a saboteur, polluting our codebase with suboptimal solutions.
It's weird because my own career was different. I was a code spammer who learned to wrangle it into something more thoughtful. But I'm dealing with overly thoughtful folks I'm trying to get to spam more code out, so to speak.
If you aren’t using the abstraction on 3 cases when you build it, it’s too early.
Even two turns into a higher bar than I expected.
A good abstraction that makes actual sense is perfectly good even when it's used only once.
On the other hand, the idea of deduplicating code by creating an indirection is often not worth it for long-term maintenance, and is precisely the kind of thing that will cause maintenance headaches and anti-patterns.
For example: don't mix file system or low-level database access with your business code, just create a proper abstraction. But deduplicating very small fragments of same-abstraction-level can have detrimental effects in the long run.
I think the only principle in programming I think can be followed at all times is YAGNI (you aren’t going to need it). I think every programming course, book, whatever should start by telling you to never, ever, abstract things before you absolutely can’t avoid it. This includes DRY. It’s a billion times better to have similar code in multiple locations that are isolated in their purpose, so that down the line, two-hundred developers later you’re not sitting with code where you’ll need to “go to definition” fifteen times before you get to the code you actually need to find.
Of course the flip-side is that, sometimes, it’s ok to abstract or reuse code. But if you don’t have to, you should never ever do either. Which is exactly the opposite of what junior developers do, because juniors are taught all these “hopeless” OOP practices and they are taught to mindlessly follow them by the book. Then 10 years later (or like 50 years in the case of Uncle Bob) they realise that functional programming is just easier to maintain and more fun to work with because everything you need to know is happening right next to each other and not in some obscure service class deep in some ridiculous inheritance tree.
The problems usually start when people take this as an opportunity to go nuts on generalizing the abstraction right away - that is, instead of refactoring the common piece of code into a simple function, it becomes a generic class hierarchy to cover all conceivable future cases (but, somehow, rarely the actual future use case, should one arise in practice).
Most of this is just cargo cult thinking. OOP is a valid tool on the belt, and it is genuinely good at modelling certain things - but one needs to understand why it is useful there to know when to reach for it and when to leave it alone. That is rarely taught well (if at all), though, and even if it is, it can be hard to grok without hands-on experience.
Ideas on how to overcome that?
I had this problem with an overzealous junior developer and the solution was showing some different perspectives. For example John Ousterhout's A Philosophy of Software Design.
How do you respond to that?
From what you've described, you have a coworker who is not open to learning and considering alternative solutions. They are not able to defend their approach, and are instead dismissive (and using an ageist joke to do it). This is toxic to a collaborative work environment.
I give some leeway to assholes who can justify their reasoning. Assholes who just want their way because it's their way aren't worth it and won't make your product better.
As others have said, if they can't or won't get that that's unacceptable behavior, fire them. (jerf is more patient than I am...)
However, to answer the question more generally, I've had some success first acknowledging that I agree the situation is suboptimal, and giving some of the reasons. These reasons vary; we were strapped for time, we simply didn't know better yet, we had this and that specific problem to deal with, sometimes it's just straight up "yeah I inherited that code and would never have done that", honestly.
I then indicate my willingness to spend some time fixing the issues, but make it clear that there isn't going to be a Big Bang rewriting session, but that we're going to do it incrementally, with the system working the whole time, and they need to conceive of it that way. (Unless the situation is in the rare situation where a rewrite is needed.) This tends to limit the blast radius of any specific suggestion.
Also, as a senior engineer, I do not 100% prioritize "fixing every single problem in exactly the way I'd do it". I will selectively let certain types of bad code through so that the engineer can have experience of it. I may not let true architecture astronautics through, but as long as it is not entirely unreasonable I will let a bit more architecture than perhaps I would have used through. I think it's a common fallacy of code review to think that the purpose of code review is to get the code to be exactly as "I" would have written it, but that's not really it.
Many people, when they see this degree of flexibility, and that you are not riding to the defense of every coding decision made in the past, and are willing to take reasonable risks to upgrade things, will calm down and start working with you. (This is also one of the subtle reasons automated tests are super super important; it is far better for them to start their refactoring and have the automated tests explain the difficulties of the local landscape to them than a developer just blathering.)
There will be a set that do not. Ultimately, that's a time to admit the hire was a mistake and rectify it appropriately. I don't believe in the 10x developer, but not for the usual egalitarian reasons... for me the problem is I firmly, firmly believe in the existence of the net-negative developer, and when you have those the entire 10x question disappears. Net negative is not a permanent stamp, the developer has the opportunity to work their way out of it, and arguably, we all start there both as a new developer and whenever we start a new job/position, so let me sooth the egalitarian impulse by saying this is a description of someone at a point in time, not a permanent label to be applied to anyone. Nevertheless, someone who insists on massive changes, who deploys morale-sapping insults to get their way, whose ego is tied up in some specific stack that you're not using and basically insists either that we drop everything and rewrite now "or else", who one way or another refuses to leave "net negative" status... well, it's time to take them up on the "or else". I've exaggerated here to paint the picture clearly in prose, but, then again, of the hundreds of developers I've interacted with to some degree at some point, there's a couple that match every phrase I gave, so it's not like they don't exist at all either.
I get the impressions that programmers don't like ambiguity in general, let alone in things they have to confront in real life.
The rules are there for a reason. The tricky part is making sure you’re applying them for that reason.
Back then, a lot of code optimizations was magic to me. I still just barely understand the famous inverse square root optimization in the Quake III Arena source code. But I wanted to be able to do what those guys were doing. I wanted to learn assembly and to be able to drop down to assembly and to know where and when that would help and why.
And I wasn't alone. This is because these optimizations are not obvious. There is a "mystique" to them. Which makes it cool. So virtually ALL young, aspiring game programmers wanted to learn how to do this crazy stuff.
What did the old timers tell us?
Stop. Don't. Learn how to write clean, readable, maintainable code FIRST and then learn how to profile your application in order to discover the major bottlenecks and then you can optimize appropriately in order of greatest impact descending.
If writing the easiest code to maintain and understand also meant writing the most performant code, then the concept of code optimization wouldn't even exist. The two are mutually exclusive, except in specific cases where it's not and then it's not even worth discussing because there is no conflict.
Carmack seems to acknowledge this in his email. He realizes that inlining functions needs to be done with careful judgment, and the rationale is both performance and bug mitigation. But that if inlining were adopted as a matter of course, a policy of "always inline first", the results would quickly be an unmaintainable, impossible to comprehend mess that would swing so far in the other direction that bugs become more prominent because you can't touch anything in isolation.
And that's the bane of software development: touch one thing and end up breaking a dozen other things that you didn't even think about because of interdependence.
So we've come up with design patterns and "best practices" that allow us to isolate our moving parts, but that has its own set of trade-offs which is what Carmack is discussing.
Being a 26 year veteran in the industry now (not making games btw), I think this is the type of topic that you need to be very experienced to be able to appreciate, let alone to be able to make the judgment calls to know when inlining is the better option and why.
After a while they consider both rules as useful, and will move to a more granular case-by-base analysis. Some people get stuck at rule-based thinking though, and they'll even accuse you of being inconsistent if you try to do case-by-case analysis.
So, more like Heraclitus's union of opposites maybe if you really want to label it?
> Concretely related to the topic, I've often found myself inlining short pieces of one-time code that made functions more explicit, while at other times I'll spend days just breaking up thousand line functions into simpler blocks just to be able to follow what's going on. In both cases I was creating inconsistencies that younger developers nitpick -- I know I did.
Absolutely, I'll break up a long block of code into several functions, even if there is nowhere else they will be called, just to make things easier to understand (and potentially easier to test). If a function or procedure does not fit on one screen, I will almost always break it up.
Obviously "one screen" is an approximation, not all screens/windows are the same size, but in practice for me this is about 20-30 lines.
* Inlining code is the best form of breaking up code.
* Love is evil.
* Rightwing populism is a return to leftwing politics.
* etc.
The purpose is to induce aporia (puzzlement), and hence make it possible to evaluate apparent contradictions. However, a lot of people resent feeling uncertain, and so, people who speak this way are often disliked.This gives you the right mindset to focus on advancing the field in a significant way.
Believing in the paradigm too much will lead to only incremental results, and not believing enough will not provide enough footholds for you to work on a problem productively.
I think you would appreciate the philosophy of the Grug Brained Developer: https://grugbrain.dev
Obligatory: “A foolish consistency is the hobgoblin of little minds"
Continued because I'd never read the full passage: "... adored by little statesmen and philosophers and divines. With consistency a great soul has simply nothing to do. He may as well concern himself with his shadow on the wall. Speak what you think now in hard words, and to-morrow speak what to-morrow thinks in hard words again, though it contradict every thing you said to-day. — 'Ah, so you shall be sure to be misunderstood.' — Is it so bad, then, to be misunderstood? Pythagoras was misunderstood, and Socrates, and Jesus, and Luther, and Copernicus, and Galileo, and Newton, and every pure and wise spirit that ever took flesh. To be great is to be misunderstood.” ― Ralph Waldo Emerson, Self-Reliance: An Excerpt from Collected Essays, First Series
We already have a technology which scales infinitely with the human mind: abstraction and composition of those abstractions into other abstractions.
Until now, we’ve focused on getting AI to produce correct code. Now that this is beginning to be successful, I think a necessary next step for it to be useful is to ensure it produces well-abstracted and clean code (such that it scales infinitely)
As a consequence of the Rule of Three, you are allowed to have rules that have one exception without having to rethink the law. All X are Y except for Z.
I sometimes call this the Rule of Two. Because it deserves more eyeballs than just being a subtext of another rule.
Holding multiple truths is a sign that you understand the problem. Insisting on a singular judgement is a sign that you're just parroting catchy phrases as a short cut to thinking; the real world is rarely so cut and dry.
Pure functional programming is the bigger insight here that most programmers will just never understand why there's a benefit there. In fact most programmers don't even completely understand what FP is. To most people FP is just a bunch of functional patterns like map, reduce, filter, etc. They never grasp the true nature of "purity" in functional programming.
You see this lack of insight in this thread. Most responders literally ignore the fact that Carmack called his email completely outdated and that he mostly does pure FP now.
https://web.archive.org/web/20170116040923/http://gamasutra....
He does not say that that his email is completely outdated - he just says that calling pure functions is exempt from the inlining rule.
He's not off writing pure FP now. His approach is still deeply pragmatic. In the link above he discusses degrees of function purity. "Pure FP" has a whole different connotation - where whole programs are written in that constrained style.
> In the years since I wrote this, I have gotten much more bullish about pure functional programming, even in C/C++ where reasonable: (link) > >The real enemy addressed by inlining is unexpected dependency and mutation of state, which functional programming solves more directly and completely. However, if you are going to make a lot of state changes, having them all happen inline does have advantages; you should be made constantly aware of the full horror of what you are doing.
He explicitly says that functional programming solves the same issue as inlining but more directly and completely.
Are you going to write that extra data structure, when your task is actually something else? Some management breathing down your neck, asking when something will be done? Or not so well versed coworkers complaining about you adding a lot of code that might need to be maintained by them, while they don't understand FP? Do you even have the knowledge to implement that data structure in the first place, or will you need to study a couple of papers and carefully translate their code, if any, i to your language and then verify expected performance in meaningful benchmarks?
Lots of problems can arise there in practice.
In math people never use procedures. They write definitions in math in terms of formulas and expressions.
If you can get everything to fit on one line in your programming. Then you are doing functional programming.
The lack of side effects, lack of mutation and high modularity are the beneficial outcome of fp, it is not the core of what you're doing. The core of what you're doing is your defining your program as a formula/equation/expression rather then a list of procedures or steps. Of course, why you would write your program this way is because of the beneficial outcomes.
By coincidence if you write your code in a way where you just account for the side effects like deliberately avoiding mutation, IO and side effects... then your program will become isomorphic to a mathematical function. So it goes both ways.
Another thing you will note and most people don't get this is that for loops don't exist in FP. The fundamental unit of "looping" in fp is always done with recursion, just like how they would do it in mathematical expressions.
Talking about state and error handling is helpful because it helps explain why to use the tool, not how the tool was forged (or originally conceived)
You could argue that those are still desugared to recursion, but I think at that point it's kinda moot - the construct is still readily recognizable as a loop, and it's most likely also implemented under the hood as an imperative loop with encapsulated local state; not that it matters so long as semantics stay the same.
In general, so long as mutation can be encapsulated in modules that only expose pure functional interfaces, I think it should still count as FP for practical purposes.
I never said he said his email was completely outdated. He for sure implies it’s outdated and updates us on his views of inlining which I also mentioned.
From your prior message:
> Carmack called his email completely outdated
He still does inlining.
Neither of those are true. He does more FP ”where reasonable“, and that decreases the need for inlining. He does not do pure FP, and he still inlines.
There are tons of benefits to get from learning this lesson in a more intentional way—I know that I changed my entire outlook on programming after some time working in Clojure!—but I’ve seen other devs take the same lessons in multi-paradigm contexts as well.
In pure fp. You can break your function down into the smallest computational unit possible. This is what prevents technical debt of the architectural nature as you can rewrite your code as simply recomposing your modular logic.
1) programming which is based on "functions" (procedures) as values, including anonymous lambdas (hence map/fold/etc paradigms), which is only really possible in languages that intentionally support it
2) programming where every procedure (except some boundary code for IO/etc) is truly is a well-defined mathematical function, which is possible in almost any programming language
1) describes any language you would call a "functional programming language," whereas 2) involves well-understood concepts around mutability and determinism that a minority (correctly) describe as "pure functional programming."
So I think it's a bit judgmental to say "lack of insight" when it's more about shifting terminology. A very high-reliability C program might be "purely functional" (inside of an IO/memory boundary) and built by engineers with the precise insight you're discussing, but in most contexts it would be odd to say "purely functional," especially if the code eschews C mechanics around function pointers. In most imperative contexts it is clearer to describe purely functional ideas in terms of imperative programming (which are equally clear, if less philosophically interesting).
For me, these were the gateway drugs to FP, because they weren't available in the languages I was used to, namely C++ and Java. I encountered map and filter in Python in the 1990s, immediately realized a ton of Java and C++ code I wrote would be simpler with them, and dove into Lisp when I found out that's where Python got them. They have nothing to do with pure functional programming, of course; they're just nice idioms that came from functional languages. That led to a long slippery slope of ideas that upgraded my non-FP programming at every step, long before I got into anything that could be described as pure FP.
I don't know if it helps to draw a strict line between "pure" and "impure" FP. I mostly code in Scala, which is an imperative, side-effecting language. Scala gives you exactly the same power as Java to read and mutate global state. However, by design, Scala provides extremely good support for functional idioms, and you can use an effects system (such as Cats Effect or ZIO) to write in a pure FP style. But is it "pure FP" if you can read and mutate global state, and if you have to rely on libraries that are written in Java? Maybe, maybe not, but I don't think trying to answer that question yields much insight.
To you you’re just fulfilling an ocd need to simplify your code and make it more pretty. There is deeper insight here that you missed and even when you read what carmack wrote about pure fp I doubt you’ll internalize the point.
Lisp is not pure. That’s why you don’t have the insight. For the true insight you need to learn about Haskell in a non trivial way. Not just youtube videos, but books that teach you the language from first principles. You need to understand the IO monad. And why it makes Haskell pure and how it forces you to organize your code in a completely different way. This is not an easy thing to understand.
The IO monad, when it appears, infects your code with the IO type and makes it extremely annoying to get rid of. I had a friend who learned Haskell and hated Haskell because of the IO monad. He stopped learning haskell to early and he never "got it".
If you reach this point you have to keep learning about Haskell until you understand why things are the way they are with haskell.
Just remember this: the annoyance of the IO monad is designed like that so that you write your logic in a way that doesn’t allow the monad to pollute most of your code.
This is why I say it's not useful to try to draw a strict line. I'm not going to argue with you on whether using an IO monad in an impure language is "pure FP" or not, but some Scala devs would. That argument in itself is not nearly as illuminating as knowing all the tools and concepts.
The larger issue is performance which is a legitimate reason for not using fp in many cases. But additionally in many cases there is no performance trade off.
this is a really non-productive comment. you have an opportunity to teach and share knowledge but instead you hoard and condescend, and rant about your implied superiority.
if so many programmers don't understand -- what's more productive: this comment, or helping "most programmers" to get it, and understand?
I love to teach and explain but this is one of those things that can’t be conveyed. You have to do it yourself.
If you want an explanation though, use Google. But I don’t think explanations actually help you grok what’s really happening. You really have to come to catharsis yourself.
Learn Haskell. Learn it to the point where you completely understand the purpose of the IO monad and why it exists and helps a program organize better. Then you will understand.
When you do get it. You’ll be among a small few who have obtained something that’s almost like forbidden knowledge. No one will “get” you.
John Karmack did and talked about it https://m.youtube.com/watch?v=1PhArSujR_A
I had a friend (who’s in general a good programmer) learn Haskell and then get completely annoyed by the IO monad so that he quit learning Haskell. So yeah, it’s not easy to “get it” You only get it with reading and practice. Really you just need to completely internalize and grasp the purpose of the IO monad in Haskell.
In this era of 3-5 frame latency being the norm (at least on e.g. the Nintendo Switch), I really appreciate a game developer having anxiety over a single frame.
Wikipedia indicates the Switch maxes out at 1080p60, and the newest Zelda only at 900p30 even when docked
smash brothers ultimate for example runs at 60fps and has 5-6 frames of input lag
I can type about 90 words per minute on QWERTY, which is about 8 keystrokes per second. That means that the average interval between keystrokes is about 120 milliseconds, already significantly less than my 200-millisecond reaction time, and many keystrokes are closer together than that—but I rarely make typographical errors. Fast typists can hit 150 words per minute. Performing musicians consistently nail note timing to within about 40 milliseconds. So it turns out that people do routinely time their physical movements a lot more precisely than their reaction time. Their jitter is much lower than their latency, a phenomenon you are surely familiar with in other contexts, such as netcode for games.
If someone's latency is 200 milliseconds but its jitter (measured as standard deviation) is 10 milliseconds, then reducing the frame latency from a worst-case 16.7 milliseconds (or 33.3 milliseconds in your 30Hz example) to a worst-case 8.3 milliseconds, and average-case 8.3 milliseconds to average-case 4.2 milliseconds, you're knocking off a whole 0.42 standard deviations off their latency. If they're playing against someone else with the same latency, that 0.42σ advantage is very significant! I think they'll win almost 61% of the time, but I'm not sure of my statistics†.
See also https://danluu.com/input-lag/#appendix-why-measure-latency:
> Latency matters! For very simple tasks, people can perceive latencies down to 2 ms or less. Moreover, increasing latency is not only noticeable to users, it causes users to execute simple tasks less accurately. If you want a visual demonstration of what latency looks like and you don’t have a super-fast old computer lying around, check out this MSR demo on touchscreen latency.
> The most commonly cited document on response time is the nielsen group[sic] article on response times, which claims that latncies[sic] below 100ms feel equivalent and perceived[sic] as instantaneous. One easy way to see that this is false is to go into your terminal and try sleep 0; echo "pong" vs. sleep 0.1; echo "test" (or for that matter, try playing an old game that doesn't have latency compensation, like quake 1, with 100 ms ping, or even 30 ms ping, or try typing in a terminal with 30 ms ping). For more info on this and other latency fallacies, see this document on common misconceptions about latency.
(The original contains several links substantiating those claims.)
https://danluu.com/keyboard-latency/#appendix-counter-argume... has a longer explanation.
______
† First I tried sum(rnorm(100000) < rnorm(100000) + 0.42)/1000, which comes to about 61.7 (%). But it's not a consistent 0.42σ of latency being added; it's a random latency of up to 0.83σ, so I tried sum(rnorm(100000) < rnorm(100000) + runif(100000, max=0.83))/1000, which gave the same result. But that's not taking into account that actually both players have latency, so if we model random latency of up to a frame for the 60Hz player with sum(rnorm(100000) + runif(100000, max=1.67) > rnorm(100000) + runif(100000, max=0.83))/1000, we get more like a 60.8% chance that the 120fps player will out-twitch them. I'm sure someone who actually knows statistics can tell me the correct way to model this to get the right answer in closed form, but I'm not sure I could tell the correct closed-form formula from an incorrect one, so I resorted to brute force.
Honestly, I have not. I'm not much of a gamer, even though I used to be a game developer.
Certainly the difference between 30Hz and 60Hz is noticeable.
Maybe this is just because I'm old school but if it were me, I would absolutely prioritize low latency over high frame rate. When you played an early console game, the controls felt like they were concretely wired to the character on screen in a way that most games I play today lack. There's a really annoying spongey-ness to how games feel that I attribute largely to latency.
I don't really give a shit about fancy graphics and animation (I prefer 2D games). But I want the controls to feel solid and snappy.
I also make electronic music and it's the same thing there. Making music on a computer is wonderful and powerful in many ways, but it doesn't have the same immediacy as pushing a button on a hardware synth (well, on most hardware synths).
So it would take a 12 frames animation and a trained gamer for a couple of frames to make a difference (e.g. push the right button before the animation ends and the opponent's action takes effect).
[1] https://humanbenchmark.com/tests/reactiontime/statistics
this is for a stylus, but people can detect input latency as low as 1ms (possibly lower)
with VR, they use the term "motion to photon latency", and if it's over ~20ms, people start getting dizzy. at 200ms, nobody is going to be keeping their lunch down
google noticed people making fewer searches if they delayed the result by 100ms
edit: if you want an easy demo, open up vim/nano over ssh, and type something. then try it locally
Imagine playing a drum that took between 0 and 5 extra frames at 60FPS between striking the head and it producing a sound. Most people would notice that kind of delay, even if they can't "react" that quickly.
In games, frame delay translates to having to hold down a key (or wait before pressing the next one) for longer than is strickly necessary in order to produce an effect. Since fighting games are all about key sequences, the difference between needing to hold key for 0 frames and 5 frames is massive when you consider key combinations might be sequences of up to 5 key presses. 5 frames of delay x five sequential key presses x 8ms a frame = 1600ms vs 1 frame x 5 seq. key presses x 8ms = 40ms.
There's a massive difference between taking 1.6s to execute a complex move and 0.040s.
The thing with latency is it needs to be consistent. If your latency is between 3 to 5 frames you blew it because you can't guarantee the same experience on every button press. If you always have 3 frames of latency, with modern screens, analog controls, and game design aware of those limitations, that's much better. Look at modern games like Celeste, who has introduced Coyote Time to account for all the latency of our modern hardware.
Which titles is this true for? Have you or anyone else measured?
I’ve really gone to town with this in Python.
def parse_news_email(…):
def parse_link(…):
…
def parse_subjet(…):
…
…
If you are careful, you can rely on the outer function’s variables being available inside the inner functions as well. Something like a logger or a db connection can be passed in once and then used without having to pass it as an argument all the time: # sad
def f1(x, db, logger): …
def f2(x, db, logger): …
def f3(x, db, logger): …
def g(xs, db, logger):
for x0 in xs:
x1 = f1(x0, db, logger)
x2 = f2(x1, db, logger)
x3 = f3(x2, db, logger)
yikes x3
# happy
def g(xs, db, logger):
def f1(x): …
def f2(x): …
def f3(x): …
for x in xs:
yield f3(f2(f1(x)))
Carmack commented his inline functions as if they were actual functions. Making actual functions enforces this :)Classes and “constants” can also quite happily live inside a function but those are a bit more jarring to see, and classes usually need to be visible so they can be referred to by the type annotations.
For example, if your function has an implicit, undocumented contract such as assuming the DB is only a few milliseconds away, but they then reuse the code for logging to DBs over the internet, then they find it’s slow and speed it up with caching. Now your DB writing code has to suffer their cache logic bugs when it didn’t have to.
Personally I like that functions can be inside functions, as a trade off between inlining and functional seperation in C++.
The scope reduction makes it easier to track bugs while it has the benefits of separation of concern.
None, it was just a simple joke based on the typo in the post I replied to. I like Python, and have in fact been happily using it as my main language for over 20 years.
Congrats, you've got an untestable unit.
> while not improving test quality
The big wins from fine-grained testing are
1. Knowing _where_ your program is broken
2. Testing "rare" edge cases
Elaborating on (2), your code probably works well enough on some sort of input or you wouldn't ship it. Tests allow you to cheaply test all four Turkish "i"s and some unicode combining marks, test empty inputs, test what happens when a clock runs backward ever or forward too slowly/quickly, .... You'll hit some of those cases eventually in prod, where pressures are high and debugging/triaging is slow, and integration tests won't usually save you. I'm also a huge fan of testing timing-based logic with pure functions operating on the state being passed in (so it's tested, better than an integration test would accomplish, and you never have to wait for anything godawful like an actual futex or sleep or whatever).
> makes refactoring and adding new features hard
What you're describing is a world where accomplishing a single task (refactoring, adding a new feature) has ripple effects through the rest of the system, or else the tests are examining proxy metrics rather than invariants the tiny subunits should actually adhere to. Testing being hard is a symptom of that design, and squashing the symptom (avoiding tests on tiny subunits) won't fix any of the other problems it causes.
If you're stuck in some codebase with that property and without the ability to change it, by all means, don't test every little setup_redis_for_db_payment_handling_special_case_hulu method. Do, however, test things with sensible, time-invariant names -- data structures, algorithms, anything that if you squint a bit looks kind of like parsing or serialization, .... If you have a finicky loop with a bunch of backoff-related state, pull the backoff into its own code unit and test how it behaves with clocks that run backward or other edge cases. The loop itself (or any other confluence of many disparate coding concepts) probably doesn't need to be unit tested for the reasons you mention, but you usually can and should pull out some of the components into testable units.
I think developers should generally try and aim for, at every scale, the outputs of a system to be pure functions of the inputs (whether by reducing the scope of the system or expanding the set of things considered inputs). Beyond that there are so many decisions at the margin that are going to be based on personal inclination.
Perhaps we have a difference in definition. To me, a unit test for a function such as "parse_news_email" would explore variations in parameters and states. Because of combinatorial explosion, that often means at least some white-box testing. I'm not going to generate random subjects and senders, and received-froms, I'm going to target based on internal details. Are we doing smart things with the message ID hostname? Then what happens if two messages come in with the same message ID but from different relays? The objective is that the unit test wrings out the implementation details, and the caller's unit test doesn't need to exhaustively test them.
This white-box texting may require directly poking at or mocking internal functions or at least abusing how they're called. For example, parsing the news item might entail pulling up and modifying conversation thread cache entries or state. For some of the tests you may need hand-crafted cache state, it's not feasible to create unique states for each parameter combination you're testing, and testing a combination will pollute the state for the following combinations. Or maybe the function depends upon an external resource you can't beat to death with a million identical requests. So the least-bad, simplest solution may be to freeze or back out part of the normal state update in the unit test. Which would usually involve directly invoking the internal routines.
Can this lead to fragile, false-positve to the point of useless tests? You betcha. That's where entertaining two contrary viewpoints is needed :) Use experience and good judgement about pros and cons in the particular situation.
Any time you merge state with function you can no longer move the function. This is the same problem as OOP. Closures can't be modular the same way methods in objects can't be modular.
The smallest unit of testable module is the combinator. John Carmack literally mentioned he does pure functional programming now which basically everyone in this entire thread is completely ignoring.
On visibility, one of the patterns I’ve always liked in Java is using package level visibility to limit functions to that code’s package and that packages tests, where they are in the same package (but possibly defined elsewhere.)
(This doesn’t help though with the reduction in argument verbosity, of course.)
Imperative blocks of code in Haskell (do-notation) also work like this.
(1) 40ms for inline code;
(2) 150ms for an inner function with one expression;
(3) 200ms for a slightly more complex inner function; and
(4) 4000ms+ for an inner function and an inner class.
def f1(n: int) -> int:
return n * 2
def f2(n: int) -> int:
def g():
return n * 2
return g()
def f3(n: int) -> int:
def g():
for _ in range(0):
try:
pass
except Exception as exc:
if isinstance(exc, 1):
pass
else:
while True:
pass
raise Exception()
return n * 2
return g()
def f4(n: int) -> int:
class X:
def __init__(self, a, b, c):
pass
def _(self) -> float:
return 1.23
def g():
for _ in range(0):
try:
pass
except Exception as exc:
if isinstance(exc, 1):
pass
else:
while True:
pass
raise Exception()
return n * 2
return g()It’s not needed. In fact, you should leave it out for read-only variables. That’s standard practice - if you use `nonlocal` people reading the code will expect to see writes to the variables.
int main() {
int a = -1;
[&] {
a = 42;
printf("I'm an uncallable inline block");
}();
printf(" ");
[&] {
printf("of code\n");
}();
[&] {
printf("Passing state: %d\n", a);
}();
return 0;
}https://benoitessiambre.com/entropy.html
In short, it reduces scope of logic.
The more logic you have broken out to wider scopes, the more things will try to reuse it before it is designed and hardened for broader use cases. When this logic later needs to be updated or refactored, more things will be tied to it and the effects will be more unpredictable and chaotic.
Prematurely breaking out code is not unlike using a lot of global variables instead of variables with tighter scopes. It's more difficult to track the effects of change.
There's more to it. Read the link above for the spicy details.
That's a really funny way to say it.
As mentioned by others, C++ has lambdas. Even if you don't use lambdas, people used to achieve the same effect by using plenty of private functions inside classes, even though the class might have zero variables and simply holds functions. In even older C code, people are used to making one separate .c file for each public function and then define plenty of static functions within each file.
It is nice, if a performance optimization manages to keep the same degree of readability and maintainability. Those concerns covered, sure we should go ahead and make the performance optimization.
John Carmack on Inlined Code - https://news.ycombinator.com/item?id=39008678 - Jan 2024 (2 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=33679163 - Nov 2022 (1 comment)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=25263488 - Dec 2020 (169 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=18959636 - Jan 2019 (105 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=14333115 - May 2017 (2 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=12120752 - July 2016 (199 comments)
John Carmack on Inlined Code - https://news.ycombinator.com/item?id=8374345 - Sept 2014 (260 comments)
Should we change the top link to that URL?
A lot of code written toward the "uncle bob" style where you maximize the number of functions has fantastic local clarity, you can see exactly what the code you are looking at is doing; but atrocious global clarity, where it's nearly impossible to figure out what the system does on a larger scale.
Inlining can help with that, local clarity deteriorates a bit, but global clarity typically improves by reducing the number of indirections. The code does indeed also tend to get faster, as it's much easier to identify and remove redundant code when you have it all in front of you. ... but this also improves the clarity of the code!
You can of course go too far, in either direction, but my sense is that we're often leaning much too far toward short isolated functions now than is optimal.
To some extent you can have that in languages that let you create a named lambda with explicit captures and immediately invoke it, e.g. in C++:
int g;
void doThisAndThat(int a, int b, int c) {
doThis: auto x = [&a, &b] {
...
}();
doThat: [&g, &c, &x] {
...
}();
}
The syntax makes it kind of an eyesore though. Would be nice to have something specifically designed for this purpose.much like microservices.
Compare this with enterprise software, which is orders of magnitude more complex than video games in terms of business logic (the complexity in video games is in performance optimization), but whose developers tend to add many layers of abstraction and indirection, so the core business process is obfuscated, or there's a billion non-functional side activities also being applied (logging, analytics, etc), again obfuscating the core functionality.
It's fun to go back to more elementary programming things, in e.g. Advent of Code challenges or indeed, game development.
For people who think game programming is less complex than enterprise software, I suggest the CharacterMovementComponent class in unreal engine which is the logic of movement of characters (people) in a networked game environment... With multiple thousand lines of code in just the header is not uncommon in unreal. And this is not complex because of optimization mostly. This is very complex and messy logic. Of course we can argue that networking and physics could be done in a simple naive way, which would be unacceptable in terms of latency and throughput, so all in all complexity is because of optimization after all. But it is not the 'fun' elegant kind of optimization, it is close to messy enterprise software in some sense in my opinion.
I dont buy it in games like gta, cyberpunk or witcher 3
Smooth side-scrollers did exist on the PC before Keen (An early one would be the PC port of Defender). Moon Patrol even had jumping in the early '80s.
Furthermore other contemporaries of Carmack were making full-fledged side-scrolling platformers in ways different from how Keen did it (there were many platformers released in 1990). They all involved various limitations on level design (as did what Keen used), but I don't believe any of them allowed both X and Y scrolling like the Keen games did.
I wonder what is the actual original source (from Saab, maybe?), and if this indeed holds true?
Just from a sheer readability perspective being able to read a routine from top to bottom and understand what everything is doing is invaluable.
I have thought about it many times, I wish there was an IDE where you could expand function calls inline.
I’m not a fan either.
The issue does not have to be that those things are split out into separate small functions. The issue might be something else.
And avoids needing to think about it being prefix or postfix after you don't that one time.
But at other times it's insufferable, when the abstraction is leaky and unintuitive.
But then after things have solidified somewhat, it's good practice to go back through your code and determine whether those “verbs” ended up being used more than once. Quite often, something that I thought would be repeated enough to justify being its own function, is actually only invoked in one specific place—so I go back and inline these functions as needed.
The less my code looks like a byzantine tangle of function invocations, and the more my code reads like a straightforward list of statements to execute in order, the better it makes me feel, because I know that I'm not unnecessarily hiding complexity, and I can get a better, more concrete feel for what my program's execution looks like.
...and then you want to parallelize as much as possible to allow for fast boot times which helps the development process immensely.
One of the things I've learned is that optimizing for developer quality of life is one of the best approaches when it comes to correctness and performance. Then, the developers would be able to run multiple iterations of the real thing.
Yess, I finally feel vindicated. I've been having this argument with embedded people since forever. I was of the opinion that if million line big boy PC apps can make do with just one thread, having fifteen threads and synchronizing between them using mutexes and condition variables on a microcontroller with 64kb RAM is just bonkers.
For some reason, the statement that a while(true) loop + ISRs + DMA can do everything an RTOS like FreeRTOS can do, can rile up embedded folks to no end.
The link is no longer valid, I believe this is the article in question:
https://www.gamedeveloper.com/programming/in-depth-functiona...
Clean architecture can be summarized thusly:
1. Bubble up mutation and I/O code.
2. Push business logic down.
This is how it's stated in [1]:
> The concentric circles represent different areas of software. In general, the further in you go, the higher level the software becomes. The outer circles are mechanisms. The inner circles are policies.
Inlining as a practice is in service of #1, while factoring logic into pure functions addresses #2, noted in the foreword:
> The real enemy addressed by inlining is unexpected dependency and mutation of state, which functional programming solves more directly and completely. However, if you are going to make a lot of state changes, having them all happen inline does have advantages; you should be made constantly aware of the full horror of what you are doing. When it gets to be too much to take, figure out how to factor blocks out into pure functions (and don.t let them slide back into impurity!).
1: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-a...
I think that summarizes the case pro inlining.
> "it is often better to go ahead and do an operation, then choose to inhibit or ignore some or all of the results, than try to conditionally perform the operation."
I'm not the audience for this topic, I do javascript from a designer-dev perspective. But I get in the weeds sometimes, maxing out my abilities and bogged down by conditional logic. I like his quote it feels liberating... "just send it all for processing and cherry-pick the results". Lightbulb moment.
let x = block {
…
return 5
} // x == 5
And the way to mark copypaste, e.g. common foo {
asdf(qwerty(i+j));
printf(“%p”, write));
bar();
}
…(repeats verbatim 20 times)…
…
common foo {
asdf(qwerty(i+k));
printf(“%d”, (int)write); // cast to int
bar();
}
…
And then you could `mycc diff-common foo` and see: <file>:<line>: common
<file>:<line>: common
…
<file>:<line>:
@@…@@
-asdf(qwerty(i+j));
+asdf(qwerty(i+k));
@@…@@
-printf(“%p”, write));
+printf(“%d”, (int)write); // cast to int
With this you can track named common blocks (allows using surrounding context like i,j,k). Without them being functions and subject for functional entanglement $subj discusses. Most common code gets found out and divergences get bold. IDE support for immediate highlighting, snippeting and auto-common-ing similar code would be very nice.Multi-patching common parts with easily reviewing the results would also be great. Because the bugs from calling a common function arise from the fact that you modify it and it suddenly works differently for some context. Well, you can comment a common block as fragile and then ignore it while patching:
common foo {
// @const: modified and fragile!
…
}
You still see differences but it doesn’t add in a multi-patch dialog.Not expecting it to appear anywhere though, features like that are never considered. Maybe someone interested can feature it in circles? (without my name associated)
auto x = []{
/*...*/
return 5;
}();
There is/was an attempt to introduce a more of a first-class language construct for such immediate "block expressions":https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p28...
I'm not convinced that automatic checking of copy-paste errors of such blocks make much sense though. At least I think the false positive rate would be way too high.
They're also used extensively in the Linux kernel, mostly to implement macros: https://github.com/search?q=repo%3Atorvalds%2Flinux%20%22(%7...
false positive rate would be way too high
The key idea is not to have identical blocks, but to have a way to overview changes in similar code, similar by origin and design. It’s a snippet diff tool, not a typo autocorrector. There’s no false positives cause if “common foo” has zero diff in all cases, it probably should be “foo(…)”.
# perl
my $x = do {
…
5
};
;; Rebol/Red
x: do [
…
5
] { ...; expr } --> x;
// x is a new variable initialized to expr
I feel like this would help readability when the compound statement is very large. let x = {
whatever;
5
}; // assigns 5 to xTo prevent unintended uses of a helper function in C, you can make it static. Then at least nothing from outside of that translation unit can call it.
I would love to hear some war stories about the development of flight software. A lot of it is surely classified, but I'm fascinated by how those systems are put together.
Limiting scope is one of the best tools we have to prevent bugs. It's one reason why we don't just use globals for everything.
Foo f = null;
{
... stuff with variables
f = barbaz;
}The worst I've seen is old school C programmers who insisted on reusing loop variables in other loops. Even worse, those loop variables were declared inside the loop declaration, which old C standards allowed to visible outside of it.
So they would have stuff like this
for(int i=0; i<10; i++) { ... }
for (;i<20;i++) { ... }
Later versions of C++ disallowed this, which led to some interesting compile failures, which led to insistence of the old stubborn programmers that new compilers simply not be usedYou can't do it in C, but in functional style languages you can do this:
let f = {
let bar = ...;
let baz = ...;
let barbaz = ...;
barbaz
};
Which is a lot nicer. But if you ask me it's just a function by another name except it still doesn't limit scope quite as precisely as a function. auto f = [&] {
...
return barbaz;
}();
with the side benefit that you can also make the use of state explicit inside of [] instead of using wildcard capture.Given that it's neither reused nor parametrized, I'm not sure why you see this kind of pattern as a "function by another name", though. Semantically it's more of a namespace if anything.
If that's an important property maybe it would be worth supporting an annotation on normal functions to enforce that. I guess you could easily write a linter for that.
https://web.archive.org/web/20241009062005/http://number-non...
I thought that at least his crash was a result of bad constants in flight software: https://www.youtube.com/watch?v=SWZLmVqNaQc
The first comment appears to agree with me.
_If a function is called from multiple places, see if it is possible to arrange for the work to be done in a single place, perhaps with flags, and inline that._
Thanks,
def processOrder():
# Some common processing logic
print("Processing the order...")
def placeOnlineOrder():
processOrder()
print("Sending confirmation email...")
def placeInStoreOrder():
processOrder()
print("Printing receipt...")
# Calls from different locations
placeOnlineOrder()
placeInStoreOrder()
Could become: def processOrder(order_type):
# Common processing logic
print("Processing the order...")
if order_type == "online":
print("Sending confirmation email...")
elif order_type == "in_store":
print("Printing receipt...")
# Unified calls with different flags
processOrder("online")
processOrder("in_store")To the other point though, the quality of compiler inlining heuristics is a bit of a white lie. The compiler doesn't make optimal choices, but very few people care enough to notice the difference. V8 used a strategy of considering the entire source code function length (including comments) in inlining decisions for many years, despite the obvious drawbacks.
John Carmack on Inlined Code - https://news.ycombinator.com/item?id=39008678 - Jan 2024 (2 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=33679163 - Nov 2022 (1 comment)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=25263488 - Dec 2020 (169 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=18959636 - Jan 2019 (105 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=14333115 - May 2017 (2 comments)
John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=12120752 - July 2016 (199 comments)
John Carmack on Inlined Code - https://news.ycombinator.com/item?id=8374345 - Sept 2014 (260 comments)
while(1) {
if (condition1)
...
if (condition2)
...
// etc
}
Subroutine calls can be eliminated by inlining everything, using macros to make the code more manageable. Loops can be simulated using macros that expand to multiple copies of the code, one for each step.One advantage is that the program will never get into an unbounded loop because the program counter will always advance towards the end of the main loop.
typedef enum state {EVEN, ODD} state_t;
state_t task1 = EVEN;
state_t task2 = EVEN;
while (1) {
switch(task1) {
case EVEN:
// do even things
task1 = ODD;
break;
case ODD:
// do odd things
task1 = EVEN;
break;
default:
fprintf(stderr, "WTF?\n");
exit(1);
}
switch(task2) {
case EVEN:
// do even things
task2 = ODD;
break;
case ODD:
// do odd things
task2 = EVEN;
break;
default:
fprintf(stderr, "WTF?\n");
exit(1);
}
}
For every "process" you've unrolled like this, you have to place it into its own switch/case or call out to a function which has similar logic (when subroutines aren't disallowed). If the process is short enough you let it execute all the way through, bigger processes would need to be broken apart like above to avoid consuming an entire cycle's time (especially important in real-time systems).can anyone expound on this? I'm not sure what he's exactly referring to here
But For C++ template combinatorics are going to dominate any slow down due to function length.
> The real enemy addressed by inlining is unexpected dependency and mutation of state, which functional programming solves more directly and completely. However, if you are going to make a lot of state changes, having them all happen inline does have advantages; you should be made constantly aware of the full horror of what you are doing. When it gets to be too much to take, figure out how to factor blocks out into pure functions (and don.t let them slide back into impurity!).
Some years ago at job foo I wrote a Ruby library that was doing some stuff. Time was of the essence, I was a one-man team, and the trickiness of it required a clear understanding of the details, so I wrote a single ~1000 LOC file comprising of the entirety of the namespace module of that library, with but a couple or three functions.
Then a new hire joined my one-man team. I said: apologies for this unholy mess, it's overdue for a refactoring, with a bunch of proper classes with small methods and split in a few files accordingly. They said: not at all, the code was exceptionally clear; I could sit and understand every bit of it down to the grittier critical details in under an hour, and having seen it written this way it is obvious to me that these details of interactions would not have been abstracted away, but obscured away.
There's one camp that wants to use abstractions and names, and there's another (in my experience, smaller) camp which prefers to have as few abstractions as possible, and "every gritty detail visible".
I think both strategies have advantages and disadvantages. The group that likes abstractions can "ignore parts of the code" quickly, which potentially makes them "search" faster. If there's a bug that needs fixing, or a new feature that needs to be added, they will reach the part of the code that will need modifications faster.
The detail-oriented people can take a bit longer to identify the code that needs modification, but they also tend to be able to make those modifications faster. They also tend to be be great "spelunkers". They seem to have a "bigger cache", so to speak. But it is not infinite. They will eventually not be able to hold all the complexity in their heads, just like the first group. It will just take a bit longer.
I am firmly on the first group and that is how I write my code. I have been fortunate enough to encounter enough people from the other group to know not to diss their code immediately, and to appreciate it for its merits. When working in a team with both kinds of personalities one has to make compromises ("please remove all of these 1-line functions, Jonathan will hate them", and "could you split this 3k lines function into 2 or 3 smaller ones, for easier review?").
I do however have a problem with indirections that don't really abstract anything and only exist for aesthetical reasons.
Not every function/method is an "abstraction". Having too many one-line methods is as bad as pretending that functions with 2k/3k lines are appropriate in all cases.
If your control loop must always run under 16ms, you better make sure the worst case is 16ms rather than trying to optimise best or mid case. Avoid ifs that skips processing, that's good for demo but doesn't help reaching prod quality goals. Sometimes it doesn't bring the benefits you think, sometimes it hides poorly optimised paths, sometimes it creates subtle bugs. Of course always use your own discernment...
That would be very different in a typical cloud app where the goal is to keep CPU, memory and network usage as low as possible, not much caring about having a constant response time on each REST endpoint.
All the code which performance, memory cost, etc. is critical, should be adjusted to fit into required confine even if it will violate all other tenets. This often results in combination of opposite approaches - anything that does well.
Finally, one just profiles the code and fixes all most spending paths. This is what now any average programmer can do. What it canʼt do - and what Carmack has been doing for decades - is to predict such places and fixes them proactively at architectural level; and to find tricky solutions that average joe-the-programmer hasnʼt heard ever.
And one drawback I can think of is that when there are more than something like ten variables finding a particular variable's value in an IDE debugger gets pretty difficult. It would be at this point that I would use "watches", at least in the case of Jetbrains's IDEs.
But then yeah you can also just log each step in a custom way verifying the key values are correct which is what I am doing as we speak.
Here’s the actual rule, do what works and ships. Don’t posture. Don’t lament. Don’t idealize. Just solve the fucking problem with the tool and method that fits and move on.
And do not try to use this comment threat to understand FP. Too many cooks, and most of the are condescending douchebags. Go look at Wikipedia or talk with an AI about it. Don’t ask this place, it’s all just lectures and nitpicks.
Ten years ago - a long time in coding.
It's clear that Carmack's article is addressing a particular sort of C++ codebase that might be familiar to game developers, but isn't familiar to a lot of us here who work on web applications and backend distributed systems. His "functions" aren't really what we think of as functions: they're clearly mutating huge amounts of global state. They sound more like highly undisciplined methods on large namespaces. You can see that from the following quotes:
> There might be a FullUpdate() function that calls PartialUpdateA(), and PartialUpdateB(), but in some particular case you may realize (or think) that you only need to do PartialUpdateB(), and you are being efficient by avoiding the other work. Lots and lots of bugs stem from this. Most bugs are a result of the execution state not being exactly what you think it is.
> if a function only references a piece or two of global state, it is probably wise to consider passing it in as a variable.
In the world of many people here, i.e. away from Carmack's C++ game dev codebases of the 2000s with huge amounts of global mutable state, the standard common sense still applies: we invented structured programming with functions for profoundly important reasons: modularity and abstraction. Those reasons haven't gone away; use functions.
- In a large codebase you do not need or want to read the full tree of implementation in one go. Use functions: they have return types; you know what they do. A substantial piece of implementation should be written as a sequence of calls to subfunctions with very carefully chosen names that serve as documentation in themselves.
- Make your functions as pure as possible subject to performance considerations etc.
- This brings a huge advantage to helper functions over inlining: it is now easy to see which variables in the top-level function are being mutated.
- The implementation is much harder to understand in a single function with 10 mutable variables, than in two functions with 5 mutable variables. I think ultimately that's just a fact of combinatorics; not something we can hold opinions about.
- But sure, if the 10 mutable variables cannot be decomposed into two independent modules then don't create spurious functions.
- A separate function is testable; a block inside a function is not. It wasn't really clear that the sort of test suites that many of us here work with were part of Carmack's codebases at all!
- It is absolutely fine to use a function if it improves modularity / readability even if it only called once.