John Carmack on Inlined Code (2014)
number-none.com
number-none.com
There are other tradeoffs of code reuse and speed and worst-case-speed and probability of introducing bugs. If you haven't read the article, do, its worth it.
I love that Carmack tries to measure which styles introduce more bugs! Who else does that? Seriously, I would love to see more of that.
It's a big motivator in terms of deciding up best practices. And whenever I fix a bug, I try to identify the root cause. If it was a coding pattern to blame (as opposed to bad process or design) I look for ways to eliminate that pattern.
Related:
[1]: https://dubroy.com/blog/method-length-are-short-methods-actu...
You can only do it one at a time, and it's for quick scanning of what the function does, though.
Oh, I want to frame this and put it on the wall.
I can't stand code where even the simplest thing is implemented as a giant tree of sub-5-line functions nested 15 deep (and probably, for bonus points, scattered across half a dozen files).
config_filename = get_config_filename()
config = read_config(config_filename)
endpoint_url = config["endpoint_url"]
auth_data = get_auth_data(endpoint_url)
to just dumping all of that inline.
I've used the same technique - instead of creating a function that might be awkward due to all the inputs and outputs, I'll create an inner scope {} with nothing around it. Only the variables that it modifies/initializes are left on the outside. This is a simple way to enforce more of a data flow style within a function and ends up being a useful organizational tool. It is also similar to 'let' in some languages.
If your methods are functional, then
int fancyNumber = calculateFancyNumber();
Can be inclined cleanly/equivalently as: int fancyNumber;
{
// [Mathy stuff redacted]
fancyNumber = someIntermediateVariable / anotherIntermediateVariable;
}[&fancyNumber] { // [Mathy stuff redacted] fancyNumber = someIntermediateVariable / anotherIntermediateVariable; }();
Yep, namely when someone fixes a bug in one of the paths but forgets to update the second.
If it turns out said little dance needs to be done twice, instead, assign the function expression to a name (in an appropriate scope) and call it twice.
Nested funcs/procs in Pascal were useful for this kind of local partitioning, as well (before C; C++; Java came and peed in the development mindshare pool), even if you couldn't make closures out of them.
It really depends on how long the function is. Ignoring stuff like variable declarations and verifying constraints at the beginning of a function, I personally find anything longer than a screen or two is usually better off (in terms of readability) being in its own function.
* abstract class Connection would have maybe 3 or 4 methods;
* DataConnection would extend Connection and add a couple more methods that were specific to some proprietary protocol.
* POSDataConnection would extend DataConnection and wrap this proprietary protocol for POS machines
* ControllerDataConnection would extend POSDataConnection because a Point of Sale controller is technically a POS machine with a bit more functionality (Really just a couple flags turned on).
* There was plenty more in between; it's been so long now that I've forgotten it all.
It's just like we all learned in college! Object Oriented programming is supposed to model real-life! Except no, it's not. That's stupid and complicated.
Now the part of the OS that was C/C++ was absolutely beautiful. It took the UNIX style of programming / applications seriously; every little piece was its own program, and it worked flawlessly. Anyone could jump in and get to work immediately because it was so well written. You could follow any program top to bottom and it just... made sense!
My god.
A lot of these fancy language features sound good on paper but can be so abused that even just coming to understand the code takes up too much space in your head.
Also gofmt having the last word on what code is acceptable and what isn't helps kill a lot of arguments that aren't worth having.
https://www.youtube.com/watch?v=rFejpH_tAHM is a good overview.
That is definitely the way I was taught OOP in college. I came out thinking it as a steaming, overcomplicated pile of shit. I have since learned better. Of course, there is a whole world of enterprise programmers for whom what was described is "proper" OOP.
It is what I had to deal with, though.
These languages, as well as things like WSDL/SOAP, are the late 90's/early 00's snake oil designed to woo clueless manager-types into funneling money to consultants and 'architects'.
I worked a lot with those types and saw the futility of suggesting 'simple' solutions to simple problems - everything had to be over-complicated to justify their salaries.
It left me pretty bitter towards that whole business model. Then again, there's lots of money to be made there....
interface -> abstract -> base -> TheOnlyImplementationInstantiated
Repeat for a couple more chains, and one line bug fixes require visiting a dozen files to figure out.It is - in execution (message passing), not in taxonomies. You can't model the taxonomies because all set-in-stone languages are inadequate for that (see SICP, for example). Also, it sounds like someone had an inheritance mania on your project. That's an instant loss right there. (But maybe the language used didn't allow for anything better? C++ is notoriously bad in this respect, for example.)
Nothing is more annoying than a pointlessly huge call tree where the function is called from a single place.
In code as in life, all in moderation.
> In code as in life, all in moderation.
Beautifully said. Both methods (procedural vs. object oriented) give you different ways to shoot yourself in the foot. Instead of picking one or the other, it's more important to manage the scope at which your project grows. You have to avoid falling into the traps of each style.
If you modify your example to 400 nested 10-line function calls, how does that change your comparison?
But sometimes it is just a "Such is life" situation.
If a functional language, I'd prefer each being its own function, with the case matching in a single, standalone file that returns the appropriate function based on whatever. That way you still get just apply_tax_rate(type, amount) in your main calculation function.
However you end up solving it, it is way to big for a single function. On the other hand, a moderate bunch of functions can do it nicely -- and at that scale you don't need big complicated object-oriented abstractions other than the external interface itself.
My "problem" is an online store. It is about 75,000 lines of code. It needs all 75,000 to solve the problem. What you are saying is that it would be "somewhat" hard to deal with whether it is all in one big function or many functions.
But in reality, one big function would be incomprehensible. It is vastly easier to deal with broken out into many functions.
I will agree that if you are coding in the kind of environment Carmack is talking about, where you are writing code which interacts with massive shared global state, that purely procedural structured code is easiest to understand, because you can be sure that you can see all interactions with global data structures in one place.
But if you don't have massive shared global state, I'd argue that calling subfunctions is a hugely valuable aid to understanding, since it allows you to reason much more about the way data dependencies flow through code when you can be sure that a call to a function means that that function can only act on the data structures passed in to it and is guaranteed not to affect anything else. I would much rather in that case see:
a = subOperationA(arg)
b = subOperationB(a)
c = subOperationC(a, b)
d = subOperationD(b, c)
return d
than see those three suboperations inlined and have to figure out for myself how the data dependencies flow through the code. a = subOperationA(a, b, c, d, e, f, g, h, i)
b = subOperationB(a, d, e, f, g, h, i, j, k)
c = subOperationC(d, e, f, g, h, i, j, k, l)
return c
And then some poor fool breaks each of those up into about 8 functions each. So, you click into subOperationA to find it is subOperationAa chaining to subOperationAb,....My gripe is when we argue against having the full solution in your mind as you work the pieces. Logically, it makes sense to reduce everything down to named things. Cognitively, that is expensive.
And yes, I'm responding with an equally opposing strawman. I am beginning to reject that there is a "one true answer."
There are basically no guidelines that always apply. Novices should stick to them, and people with more experience can recognize when they don't apply.
For example, adding two n-length vectors is a good abstraction ("vector_add"). Adding two n-length vectors then multiplying the magnitude of the result by 3 is a bad abstraction.
I like Jens Gustedt's proposal for annonymous functions in C (at the end of Modern C[1]) by extending the syntax for compound literals. That would in a way give parameterized blocks, except that what you pass would be at the end (although if you name the parameters the same as what you pass it should work).
I think the main point is that it is helpful to be able to easily follow all the code that executes from point A to point B (at least up to the point of whatever portability layer you use). If you can reasonably structure it so that you just need to hit page down to read that code (without duplicated code) then that will often be easier to read IMO. I don't find one or two levels of function calls to be hard to follow, but after that it quickly gets more difficult. I suspect preferences here may to some extent be influenced by how good your short term memory is (and how good your IDE is). But I suspect there are also significant differences in how often different programmers try to step through code.
[1] http://icube-icps.unistra.fr/img_auth.php/d/db/ModernC.pdf
int a,b,c;
// ...
[a, &b]() {
// do stuff
}();Templates are usually more of a pain to debug.
Abstracting with functions is very beneficial to the writer because, well, they come up with the abstractions and know what they mean.
I feel they are less helpful to the reader, except perhaps at a very cursory level. If the reader is trying to actually understand the code to be able to modify it, many abstractions are actually a hindrance.
For example, in lisp, it's a fairly common practice to essentially write a DSL for the problem you are solving. It's a great tool to be able to do this easily and quickly. You can build massive and complex programs without overloading your brain.
However, for a reader new to your code base, there is a huge cognitive load to try to decipher the DSL. It's intuitive for the writer, because they invented it, but for the reader, it is a hurdle to overcome.
Once you learn someone's DSL, it's a very powerful tool now for you too. But when every project has it's own, it's really too much to bear.
Nested functions can actually allow you to even out your abstraction levels... Just group lower-level operations in more deeply nested functions. It's sort of the functional equivalent of extracting a group of similarly-leveled concepts into a new object in the OO approach.
Not sure if you were talking about C++, where my point probably doesn't apply... (Lack of nested function support, AFAIK)
The article includes comments from Carmack from 2014 saying he now favours breaking the code up into pure functions.
I have come to interpret this as: We are language designers. This isn't about functions. This is about building a language for the business case that is comprised of primitive expressions, means of combination, and means of abstraction. If the language is clear to the reader, expressing the problem well, it should be easier to detect problems or extend the existing language and its uses.
We are language designers already: If you build a traditional class with a bunch of methods, that is a language with how to deal with the concept embodied by the class. It must be held to the same standards of any language, DSL, or API design.
I like to remind people that we don't tend to dig into the code behind printf(); we trust what it does. We have years of experience using that primitive. It's a great example of a function that has been through many revisions due to security issues, untrustworthy in its inception. What is key here is trust that a primitive does as advertised so that one does not have to dig into its source code repeatedly. Nested functions are not an issue in the presence of trust.
My "secondarily" clause has a fatal flaw: Many languages are not suited for building languages while simultaneously not trading off speed and resource usage. The ones with pre-runtime macros/templates assist us developers in the building of expressions beyond the limitations of the base language with minimal fuss.
Heh. Anyone up for a Sussman&Abelson drinking game? :D
If another developer joins the team, then he might not have noticed that there is a taskY and may end up writing his own, completely different, implementation in yet another place.
For C, I tend to go the big function route but I usually try to put extra braces with comments to isolate subtasks from each other within the big function. Then I can pull out the tasks into functions if I see that I need them in more places. I don't know how my old code is doing so I don't know if anyone has followed my lead in those projects.
I hand wrote a LA(1) style parser (and lexer) for JSON, because I wanted to experiment with a source code generation notion I had (vs reflection, or bytecode generation). I was surprised that in micro benchmarks, Jackson + Afterburner is still twice as fast as my stuff. I had to understand why.
Turns out it does the lex/parse equivalent of unrolling and inlining (cut and paste) absolutely everything. The code is utterly baffling.
My implementation's jar is ~30Kb, fast enough (faster than all but Jackson + Afterburner, Boon), stupid simple, and works the way I want, so I'll keep using it for my projects.
In the video, he inlines a very simple function and his game gets twice as fast for no apparent reason. It's instructive to watch him dive into the generated assembly to figure out why.
If a function is only called from a single place, consider inlining it.
You should consider inlining your function, not always do it. Recently, I made a mod for a game and I had to draw an UI by code, and there, it made sens to use one-time function because it made the code easier to read (super-expressive functions like DrawLeftPane() or DrawHeader(), and next to no ties between functions).Most of the time, code readability should be prioritized over performance.
I work in two performance sensitive projects, both C++, and this has yet to be a reason to inline code. Algorithm choice is optimization of choice first and so far finally.
Excepting environments where performance is critical (games comes to mind), shouldn't we bias toward improving the code for human readability?
For now, there are macros if an inline function does not work properly. Attributes to force inlining exist in some compilers, at least GCC and clang support those.
Additionally marking the function as pure if applicable can help optimisers as well.
(Security attributes and recursive calls that may remain recursive calls instead of stack utilising loops.)
GCC and clang variants do not have this issue.
MSVC is generally not really known for high performance of generated code, which is partly why newest versions support a clang backend.
for(int I = 0; I < 4; ++I) {
real32 PixelPx = (real32)(XI + I);
real32 PixelPy = (real32)Y;
real32 dx = PixelPx - Origin.x;
real32 dy = PixelPy - Origin.y;
real32 U = dx*nXAxis.x + dy*nXAxis.y;
real32 V = dx*nYAxis.x + dy*nYAxis.y;
//rest of the loop
}
into something like real32 PixelPy = (real32)Y;
real32 dy = PixelPy - Origin.y;
real32 PixelPx = (real32)(XI);
real32 dx = PixelPx - Origin.x;
real32 U = dx*nXAxis.x + dy*nXAxis.y;
real32 V = dx*nYAxis.x + dy*nYAxis.y;
for(int I = 0; I < 4; ++I) {
U += nXAxis.x;
V += nYAxis.x;
//rest of the loop
}
PixelPy and dy are not affected by the counter in the loop which means they can safely moved outside the loop.This also results in the subexpression dynXAxis.y and dynYAxis.y being lifted outside the loop.
Now we've moved half of the code outside the loop but we aren't done yet.
The same can be done with PixelPx and dx, the trick is to then replace dxnXAxis.x with
(dx + I)*nXAxis.x
Expanding (dx + I)*nXAxis.x
yields dx*nXAxis.x + I*nXAxis.x
We can now lift the subexpression dx*nXAxis.x
out of the loop.The only thing that is now done in the loop is
I*nXAxis.x
which can be further simplified to U += nXAxis.x
The same happens with nYAxis.x.EDIT: Sorry for the bad formatting. The markdown parser ate my asterisks so I put things into code blocks which requires a new line each time.
It's another hurdle for the sufficiently smart compiler though. You need to know how the program will be run to know which is the better form. Once you get into making code-size Vs speed things get murky with instruction caches etc.
I love how everything in these emails is delivered as a calm series of reflections, chronicling with great honesty his own changing opinions over time - nothing is a diktat.
I also found it rather heartening that he makes the same copy/paste mistakes that the rest of us do - how many times have you duplicated a line and put "x" or "width" on both lines..? Seemingly Carmack can actually tell you how many times he's done that!
Hopefully because he is saying "did you really think vain attempts at premature optimization were going to impress me?".
http://blogs.valvesoftware.com/abrash/valve-how-i-got-here-w...
(And interesting history about Valve)
It's an interesting read. Previous HN discussion: https://news.ycombinator.com/item?id=5383650
void long_func(void) {
...
if (player.alive && player.health == 100) {
....
}
...
if (some_other_condition && player.alive && player.health == 100) {
}
}
Conventional wisdom says that you should write a function `is_player_untouched` and substitute the composite expressions with function calls, but the code in question can be refactored in a much more straightforward way: void long_func(void) {
...
const bool player_untouched = is_player_untouched();
if (player_untouched) {
....
}
...
if (some_other_condition && player_untouched) {
}
}
Had the function body been split into more functions for "clarity", you would be doing duplicate calls to `is_player_untouched()` which go unnoticed because they would be buried deep in the call graph.In my refactored example, you wouldn't be eventually calling `is_player_untouched()` once more if `some_other_condition` is true.
Is there any language where this is implemented, or is the effort too great for the gains?
Then it also would need to make sure that those parameters can't change between different calls within the same function.
This wouldn't be feasible in the example above, since that function explicitly depends on outside variables. If you were to supply both `health` and `alive` as arguments, you could pretty much write the check yourself from the beginning.
Of course, there are many bigger checks that could benefit from this, but still, you (or the compiler) have to make sure that all functions are pure, that no arguments can change etc etc. I imagine that this could make compile times quite slow (and also complicated to write the compiler itself).
For example, in quaternion-based rotation math, there exists a "sandwich product" where you take the (non-commutative) product of the transform and the input, followed by the product of that result and the conjugate of the transform.
It turns out that several of the embedded multiplication terms cancel out in that double operation, and if you avoid calculating the canceled terms in the first place, you can do a "sandwich product" in about 60% the total floating-point operations as two consecutive product operations.
In the application that used spatial transforms and rotations, the optimized quaternion functions were faster than the 4x4 matrix implementation, whereas the non-optimized quaternion functions were slightly slower. That change alone (adding an optimized sandwich product function) cut maybe 30 minutes off of our longest bulk data processing times.
You would never be able to figure that out from this.
out = ( rotation * in ) * ( ~rotation );
You have to inline all the operations to find the terms that cancel (or collapse into a scalar multiplication).I'd agree that if the majority of your code is mutating state, it makes sense to mash all that together in one place. You want to keep an eye on the dirty stuff.
But on the other hand, inlining pure functions that don't use or mutate any global state doesn't make sense to me. Why is making it "not possible to call the function from other places" a benefit?
How about calling that code from a unit test!
When it's a pure function that's not a problem. When it changes state then you lose track of ordering and such. That's his point, state changes need to be kept in the one big function so you can keep track of them easily.
And of course, almost all interesting software has mutable state. Otherwise you're just doing a computation and looking for a single output.
That was my point. He's conflating two different things. I understand why inlining mutation has benefits. Just not inlining functional code.
>> And of course, almost all interesting software has mutable state.
Of course, but I think most programmers overestimate how prevalent state needs to be throughout a program.
I once wrote an RSS aggregator as an eight-stage pipeline. It checked about 40k feeds, each every 60 seconds. Every stage had a 'main' file where the vast majority of state was kept. The rest was functional libraries. I suppose that would be a demonstration of what Carmack is proposing, with the difference being that my pure functions (the majority of the code) had clear names and were unit-tested.
It worked so well that almost every large program I've written since has been designed the same way!
When I worked on Guitar Hero and Rock Band, we worried about sub-frame latency (timing is more important when you're hitting a drum than when you're firing a gun).
I still use CRT screens, not because of latency, but because of better contrast and colour reproduction, and the capability to use whatever resolution I want.
I noticed that in new games, and using newer video-cards, there is some kinda weird lag there, like if they were geared on purpose for slow LCDs (there seemly even some variables that you can control on AMD cards, using Windows Registry, or tweaking the Linux driver, related to screen input lag, they are on the "PowerPlay" part of the drivers for some reason though, I couldn't figure yet what they do exactly).
EDIT: Also, I stopped playing music-games almost entirely, I found many of them completely unplayable on my setup, I just can't find the correct settings to make the timing work. The least aggravating one is "Necrodancer" that seemly is really good in calibrating.
The fundamental problem is that there are two independent delays that both depend on your individual system: the delay from the time that the console produces a video frame to the time that the user sees it, and the delay from the time that the console produces a sound to the time that the user hears it. In a beatmatching game, you really need the user's perceptions to be in sync, which means delaying either the video or the audio. Of course, the more you delay one or the other, the more the repercussions you run into.
In a regular video game, it's not a big deal if you fire a gun and hear the shot 50ms later, but in a beatmatching game, that delay is really noticeable.
But most of the requirements center around nitpicks of software polish: Specific words and phrases used to discuss the device, loading screens must not just be a black screen, the game should not crash if the user mashes the optical eject button, etc. These things add a level of consistency but aren't the same as "solid 60hz" or "no input lag". The latter sort of issues can be shipped most of the time, they just impact the experience everywhere.
RMS's hell is entirely within closed source software and everyone there calls it "Linux".
And RMS's hell is one where everyone uses open source to some extent, calls it open source, and has no philosophical reason for using it, only practical ones, and have no qualms about mixing it with closed source software.
I guess this could trip up if the compiler optimisations available when considering all the code at once means that the out-of-context code actually does something different in testing...
It is of course essential that the smaller functions be well-named and manage side-effects carefully. That is, they should either be pure functions, or the side effects should be "what the function does", so that readers of the main function don't generally need to read the function's code to understand its side effects.
The intro suggests that he agrees that pure functions are an even better solution.
You can jump to them by name from a completely different part of the code (with some editor support). You see them in the headers of your Git hunks so you don't lose context.
In languages that are heavy on type inference, like Haskell or Python+MyPy, they make for a convenient boundary at which you can assert your types, to help with type errors.
I only say this because I've gone through a similar transition of valuing my mental computation time in the last 20 years of coding :).
The efficiency of inlining is compelling when you code the whole thing at once, in one session. Once you decide to break the work up over multiple sessions, it's too much to keep in your head over multiple days (or weeks).
Not a bad idea.
Robert C. Martin encourages style B because it reads topdown and replaces comments with names.
It seems counter-intuitive but in the long run this mentality best serves the business.
I get why this is a thing. Sometimes an unrolled loop is faster. But if this is really an issue, why isn't there a [UnRoll] modifier or a preprocessor or something that handles that for you?
Something like this:
for (int i = 0; i < x; i++;) {
dothing(x[i]);
}
versus: unroll for (int i = 0; i < x; i++;) {
dothing(x[i]);
}
Only the compiler / preprocessor would unroll the second one. You have the best of both worlds with a reduced chance of subtle errors. __attribute__((optimize("unroll-loops")))
? :-)See http://www.keil.com/support/man/docs/armcc/armcc_chr13591249...
For example.
any compiler worth its salt should be able to unroll w/o explicit demand from the developer.
I think the same logic applies to a putative 'unroll' keyword. Even if it's a short-term win, the environmental properties that make it a win are likely to change before the code is retired. To me, that argues for relying on the heuristic.
One note to this is that MSVC has both the usual 'inline' keyword as well as a proprietary stronger '__forceinline' keyword. __forceinline overrides the heurstic and forces the inlining of the function even if the compiler doesn't agree it makes sense. I can see how that kind of compiler-specific annotation might be useful tactically. (ie: You've found the compiler to be making the wrong choice for a specific platform and you wish to overrule.) But not a full-fledged language keyword...
The compiler error is clear: "variable 'lam' cannot be implicitly captured in a lambda with no capture-default specified"
Thanks for the example.
These are two separate/orthogonal issues, I doubt he would turn his nose up at the processor doing less work iff it was also deterministic and had predictable worst-case timing.
What he is effectively saying is to treat all code in the same manner as you would for a hard-real-time system.
I certainly agree with this for performance critical code (performance being overall duration or latency), but this is not a one-size-fits-all solution. There are a lot of cases where this is not appropriate.
but those two cases are the same.... they're both performance-critical code.
Not all code is.
I mean - if you don't care about total running time it makes sense to remove special cases that don't change the worst case because of readibility/bugs, no matter if you care about latency.
It's not particularly helpful to a server that's fielding vast numbers of requests of various types.
Haskell abstractions are often good because they flow from category theory and there are usually well established mathematical laws associated with them. I'm thinking of the "monad laws" and the "monoid laws."
Mathematicians tend to create abstractions if the abstraction satisfies coherent and provable properties. Programmers tend to be less rigorous about what and how they abstract.
There is nothing about C++ that prevents making good abstractions. It's just the culture of the language. Industry programmers are taught to not duplicate code and to keep functions short but they are not taught the fundamentals of what makes a good abstraction.
Functional programming ain't a panacea, either.
Citation: "I don’t think that purely functional programming writ large is a pragmatic development plan, because it makes for very obscure code and spectacular inefficiencies, but if a function only references a piece or two of global state, it is probably wise to consider passing it in as a variable."
His 2014 thoughts: No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient.
Carmack's talking about pure functions at the architecture/design level. Within those functions, there's still lots of temporary mutable state, I'd be willing to bet. He's writing graphics code, he's probably not passing functions to functions in order to sum an array, he'll just do the fast, iterative thing.
The beauty of functional programming is that it doesn't matter how map works. So you can make map work as fast as possible through all the techniques you want, since code can't rely on the behaviour. Only on the input.
Passing a function to a function is a bunch of indirection and extra stack frames(!) compared to updating very small, memory aligned mutable state in-line with the work you're doing. It's even worse with closures where you're creating anonymous data structures and passing them around. You can read up on TLBs and the speed difference between L1 cache and main memory if you'd like to know more.
You might not care about the above if it's more 'beautiful' to you, but it's vastly, vastly less performant.
You could totally implement these things as compile-time macros, or do many different optimisation passes, or so many other things.
In general, any dependence on external mutable state should be asserted or otherwise verified. Those checks can be disabled for performance later. Meshes very well with actual tests too.
Good night.
===
The older I get, the more my code (mostly C++ and Python) has been moving towards mostly-functional, mostly-immutable assignment (let assignments).
Lately, I've noticed a pattern emerging that I think John is referring to in the second part. The situation is that often a large function will be composed of many smaller, clearly separable steps that involve temporary, intermediate results. These are clear candidates to be broken out into smaller functions. But, a conflict arises from the fact that they would each only be invoked at exactly one location. So, moving the tiny bits of code away from their only invocation point has mixed results on the readability of the larger function. It becomes more readable because it is composed of only short, descriptive function names, but less readable because deeper understanding of the intermediate steps requires disjointly bouncing around the code looking for the internals of the smaller functions.
The compromise I have often found is to reformat the intermediate steps in the form of control blocks that resemble a function definitions. The pseudocode below is not a great example because, to keep it brief, the control flow is so simple that it could have been just a chain of method calls on anonymous return values.
AwesomenessT largerFunction(Foo1 foo1, Foo2 foo2)
{
// state the purpose of step1
ResultT1 result1; // inline ResultT1 step1(Foo1 foo)
{
Bar bar = barFromFoo1(foo);
Baz baz = bar.makeBaz();
result1 = baz.awesome(); // return baz.awesome();
} // bar and baz no longer require consideration
// state the purpose of step2
ResultT2 result2; // inline ResultT2 step2(Foo2 foo)
{
Bar bar = barFromFoo2(foo); // 2nd bar's lifetime does not overlap with the 1st
result2 = bar.awesome(); // return bar.awesome();
}
return result1.howAwesome(result2);
}
If it's done strictly in the style that I've shown above then refactoring the blocks into separate functions should be a matter of "cut, paste, add function boilerplate". The only tricky part is reconstructing the function parameters. That's one of the reasons I like this style. The inline blocks often do get factored out later. So, setting them up to be easy to extract is a guilt-free way of putting off extracting them until it really is clearly necessary.===
In the earlier discussion sjolsen did a good job of illustrating how to implement this using lambdas https://news.ycombinator.com/item?id=8375341 Improvements on his version would be to make everything const and the lambda inputs explicit.
AwesomenessT largerFunction(Foo1 foo1, Foo2 foo2)
{
const ResultT1 result1 = [foo1] {
const Bar bar = barFromFoo1(foo1);
const Baz baz = bar.makeBaz();
return baz.awesome();
} ();
const ResultT2 result2 = [foo2] {
const Bar bar = barFromFoo2(foo2);
return bar.awesome();
} ();
return result1.howAwesome(result2);
}
It's my understanding that compilers are already surprisingly good at optimizing out local lambdas. I recall a demo from Herb Sutter where std::for_each(someLambda) was faster than a classic for(int i;i<100000;i++) loop with a trivial body because the for_each internally unrolled the loop and the lamdba body was therefore inlined as unrolled.It's not a retraction so much as an expansion of solutions to include (with caveats) FP.
Also it clarifies some drawbacks to the approach on mobile/limited resource platforms.
A developer like Carmack and likely the teams he works with are able to keep a much larger system in their head at one time than an average developer.
And this is typically why they can write larger functions like that and get away with it.
A less talented developer will be much more likely to introduce bugs near the top of that function over time as they struggle to maintain the entire function in there head.
Sometimes choosing the correct tool has more to do with the craftsman than the craft.
Hiding the complexity doesn't make it irrelevant suddenly. That's how you get code that does the same thing 5 times in 5 different branches of highly nested call tree "just to be sure".
I noticed this quite some time ago. This is also a major source of bugs that I write. That is, until I decided to stop copy-pasting more than a word at all, and retype everything character by character when I need it again. Interestingly enough, this saves a lot of time because the bugs I would generate otherwise cost way more time than a bit of typing.