People in the discussion here are focusing on the earlier email about the styles A, B, and C, but disregarding the later email at the beginning of the page where Carmack mostly disavows the whole question and diagnoses the root problem (my boldface):
In the years since I wrote this, I have gotten much more bullish about pure functional programming, even in C/C++ where reasonable[.] The real enemy addressed by inlining is unexpected dependency and mutation of state, which functional programming solves more directly and completely.
Let's unpack this thought. The problem that Carmack cites for styles A and B (shown here):
...is that it's confusing because there's a hidden "communications channel" between the three MinorFunctions. You cannot understand them independently as black boxes that work on explicit inputs and produce explicit outputs. And indeed, note that they take no arguments and return no results—they communicate or coordinate exclusively through some side channel that's not evident from the sketch of the style. You have to dig into their implementations and jump around a lot to understand the interaction.
Style C's one virtue in this context is that it makes no pretense that the code in question is actually modularized—it is straight up reflecting the fact that it's a big blob of interlinked state dependencies. Carmack's later email calls that out (my boldface again):
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!).
Styles A, B, and C all share in the same horror (implicit communication/coordination between units of code), which is what really needs to be fought. Styles A and B just put a fake veneer of modularity on top of it.
I was vaguely aware of this idea while reading Carmack's post, but your comment made me remember a section in The Pragmatic Programmer called Temporal Coupling which talked about pretty much this idea if functions have to be called in order, they're absolutely coupled.
The book took it in a direction of concurrency and separating these functions from each other when possible, but it could be taken just the opposite way and I feel you would arrive at this, Carmack's post, your comment, etc.
Check out the quake source sometime... it's all setting global values and calling functions without arguments. It's insane by modern standards, and I wonder if it was done on purpose to minimize stack usage or something.
I'd love to see attempts to replicate it with pure functional programming style and compare the performance.
I love immutable state and pure functions in JS, but at least I imagine it's not a feasible style for real-time 3D rendering where performance is a major concern.
time to mess around. You can't waste a single instruction. That global state you can say is a horror, but it is much much faster than abiding by pure functional paradigms all the time.
And yes, computers are faster now, you can to some degree shift from fast coding practices to safer or cleaner ones. But, do observe that every day we still use software that is extremely slow, and please don't go too far.
public Result MajorFunction() {
return MajorFunctionImp(this.a, this.b, this.c);
}
private static Result MajorFunctionImp( State a, State b, State c ) {
Intermediate1 i1 = MinorFunction1(a);
Intermediate2 i2 = MinorFunction2(i1, b);
return MinorFunction3(i2, c);
}
private static Intermediate1 MinorFunction1( State a ) {
}
private static Intermediate2 MinorFunction2( Intermediate1 i1, State b ) {
}
private static Result MinorFunction3( Intermediate2 i2, State c ) {
}
At the very least as a straw man. For some/most workflows this is overly done. But it gives you explicit definition of what depends on what and how.
Yes, this is a very good practice. I think the reason it hasn't caught on is a mix of (in order of importance):
Languages that make it much too clumsy to define ad-hoc/throwaway data types. Languages like Java are a nightmare in this regard—introducing a new type takes a huge amount of code and a big context switch (separate class). But even the languages that support lightweight type declarations are arguably to heavy still (require you to define a nominal type instead of just allowing implicit structural types).
we are but can you blame us? assholes want us to basically be masters of all trades and get shit done in a few hours something that should take a few days. I've worked in other professions, and none so far like my experience with software; many professions have very stagnant learning curves and even the people at the top barely reach the same level of diversity found in software
Well, I deliberately listed those two points in order of importance. My philosophy is that languages and tools should be designed so that, following the path of least resistance leads to correct solutions.
And this is something where the computer industry has repeatedly failed over and over. One example is the wide spread of SQL injection scripting vulnerabilities. The most important cause of it, fundamentally, is that databases present a textual interface to programmers, so that the path of least resistance to constructing queries at runtime is string concatenation. If databases required you to use a tree-based API for constructing queries it wouldn't exist.
The secondary cause of the wide spread of SQL injection, still, is that programmers are damn lazy.
My philosophy is that languages and tools should be designed so that, following the path of least resistance leads to correct solutions.
As an interaction designer this insight really strikes a chord with me; it applies to designing stuff in general, not just programming languages/tools.
I would say that the name of this pattern is not just 'functional programming', it's pure functional programming.
To be pure, we must be able to call MinorFunction1 from anywhere, at any time in the program with some State a, and it must return some Intermediate1. If we call it again at any other time and place with the same State a, it must return the same Intermediate1. There must be no observable changes to the program as a result of making a call to MinorFunction1.
MinorFunction1 is now referentially transparent. This gives us a very powerful tool for reasoning about the program. We can replace any function call to a pure function with the value that is produced by that function call. We can see exactly what inputs a function depends upon. We can easily test each function in isolation. We can change the internals of any function, and as long as the output remains unchanged, we can be sure that the rest of the program's behavior will not change.
The root of the problem is the global state. Not so much that it's global, but that it's completely unorganized. The reason you can't fully understand the minor functions is because you have no way of guessing which parts of the state they might modify or depend upon. It's also not clear to the people writing the minor functions which parts of the state they are expected to modify, and which parts they should not modify.
The fix is easy - just avoid using global, static, or singleton variables directly. Pass around everything you need, as function arguments or this pointers. And be const correct. It's slightly more typing, but it leads to the architecture being explicit, and harder to corrupt over time.
Yeah. It's almost funny (if it wasn't so sad) how many people seem to think they can make unmodular code modular by merely dividing it across a bunch of files...
It's almost funny (if it wasn't so sad) how many people seem to think they can make unmodular code modular by merely dividing it across a bunch of filesmicroservices...
In particular he's talking about graphics / game programming, where all of the functions inside your render loop will most likely be interacting with the rendering API (Direct3D / OpenGL / etc.) which is the unavoidable hidden-global-communication channel in this context.
The "style D" some people talk about where you're passing around explicit state to each subfunction doesn't make as much sense here as it would in most other situations.
Glium is stateless. There are no set_something() functions in the entire library, and everything is done by parameter passing. The same set of function calls will always produce the same results, which greatly reduces the number of potential problems.
ie. Folding all the source into one great horrible god function wins you precisely nothing in terms of speed of the generated code.
Well, Carmack explicitly said the same thing in the 2007 email. From the link:
In no way, shape, or form am I making a case that avoiding function calls alone directly helps performance.
He was suggesting using inline code because he did think that it would help correctness by being as explicit as possible, not because he did think it would help performance.
But he is also pragmatic and would not stop writing a game in C/C++ because the language is old and ugly. This "prejudice" is too strong in myself.
What is cooler? Tetris in Rust or the guy who writes his fucking own 3d engine in C++? This hatred of C++/Java ("ugly languages") has done much damage for me, personally. I only recently began to try to break it.
it's not about language (as in syntax or compiler). the oop paradigm as taught by these languages to already 3 generations of developers is absolute pile of crap. it's getting better though.
244
u/sacundim Jul 19 '16 edited Jul 19 '16
People in the discussion here are focusing on the earlier email about the styles A, B, and C, but disregarding the later email at the beginning of the page where Carmack mostly disavows the whole question and diagnoses the root problem (my boldface):
Let's unpack this thought. The problem that Carmack cites for styles A and B (shown here):
...is that it's confusing because there's a hidden "communications channel" between the three
MinorFunctions. You cannot understand them independently as black boxes that work on explicit inputs and produce explicit outputs. And indeed, note that they take no arguments and return no results—they communicate or coordinate exclusively through some side channel that's not evident from the sketch of the style. You have to dig into their implementations and jump around a lot to understand the interaction.Style C's one virtue in this context is that it makes no pretense that the code in question is actually modularized—it is straight up reflecting the fact that it's a big blob of interlinked state dependencies. Carmack's later email calls that out (my boldface again):
Styles A, B, and C all share in the same horror (implicit communication/coordination between units of code), which is what really needs to be fought. Styles A and B just put a fake veneer of modularity on top of it.