Create useful abstractions. Don't create abstractions for the sake of creating abstractions.
Create useful abstractions. Don't create abstractions for the sake of creating abstractions.
Five years later when I come in to debug the monster you’ve lost track off, however, I’ll curse you to the death as I go through endless loops of “go to definition”.
Need to chunk the data? Function. API Call? Function. Parse the response? Function.
So I asked: “Are you ever going to use these internal functions for anything outside of this function?” And their response was: “No, but it makes debugging easier.”
It does not. It makes it exponentially more difficult. Maybe I just work with too many terrible programmers...
Or just throw some big comment headers in.
And yeah, I agree. Even Carmack had something to say on this: http://number-none.com/blow/john_carmack_on_inlined_code.htm...
function do_task(a, b, c) {
d = subtask1(a, b);
e = subtask2(b, c, d);
f = subtask3(a, d);
}
instead of one long function. Of course, this only makes sense if the subtasks are indeed different conceptual steps in the computation.As an example, I remember writing finite elements code that was structured something like this:
function solve_fem_problem(problem_description) {
basis_functions = generate_basisfunctions(basisfunctions_type.b_spline);
quadrature_rule = generate_quadrature(quadrature_type.gaussian_quadrature);
A = assemble_matrix(problem_description.differential_equation, basis_functions, quadrature_rule);
b = assemble_righthandside(problem_description.differential_equation, basis_functions, quadrature_rule);
apply_boundary_conditions(problem_description.boundary_conditions, A, b);
solution = linsolve(A, b);
}
which I think is very pleasing to read, and preferable to a single huge function. This also makes it easy to use another linear solver, for example. On the other hand, the functions that are called inside solve_fem_problem are probably larger.Note: During work, I haven't actually encountered a lot of code that is structured in this way.
It looks much better, and is more understandable due to the repeating pattern, as you have it, but some people I've collaborated on open source code with would insist upon this:
function solve_fem_problem(problem_description) {
basis_functions = generate_basisfunctions(basisfunctions_type.b_spline);
quadrature_rule = generate_quadrature(quadrature_type.gaussian_quadrature);
A = assemble_matrix(problem_description.differential_equation, basis_functions, quadrature_rule);
b = assemble_righthandside(problem_description.differential_equation, basis_functions,
quadrature_rule);
apply_boundary_conditions(problem_description.boundary_conditions, A, b);
solution = linsolve(A, b);
}
(For this example, I'm assuming an arbitrary maximum line length of between the A = and b = lines.)You should realise you are falling victim of your own assertions here. You're cargo-culting the idea that abstracting functions makes debugging exponentially more difficult, without having provided any substance to qualify.
Imagine having a function with 5 sub functions, and since this is your style, those functions have 5 internal functions.
If I want to trace an error, first I have to find it in the main function, then the sub function, and maybe even within its sub functions. Then I have to deal with several different scopes as well.
I don’t mind this if a function is a useful abstraction, that is it does something well and is used repeatedly. My gripe is with functions that are written just for the sake of writing functions.
is not implicit. That is very much an explicit assertion.
I'm simply pointing out that in your pointing out of cargo-cult, you've done the exact same thing of drawing a conclusion with no basis.
> Imagine having a function with 5 sub functions, and since this is your style, those functions have 5 internal functions.
is unrealistic. There's plenty of room in between "no private functions at all" and "5^n functions for every layer". It's hyperbole and bordering rhetorical.
As for tracing bugs in the way you mention - nobody in my sphere of influence has needed to do that in years, at least not on a regular basis. Tests demonstrating IPO are far more valuable and also far more quickly than tracing, narrow the scope of the problem to the point that you'll now be within one small function to read than a potentially much larger one.
Go grab a Vulkan tutorial, and try to understand the logical flow. Good luck.
All the Vulkan tutorials atomize things into functions because it "encapsulates" actions into "easier to understand" pieces.
And then creates a mutable uber-object that holds a whole bunch of Vulkan handles that needs to get passed to every function so you don't know who is updating what and when.
But, hey, writing that as a 1500 line single function where you only expose the variables as you need them is far too complex for us mere mortals to read. <rolls eyes>
The problem is that Vulkan requires about 1500 lines of boilerplate to put a simple triangle on the screen. The issue is that you have to wire as much as possible up statically so the graphics hardware can fly once you start feeding it vertex information. However, for someone who doesn't need maximum performance, the setup boilerplate is a nuisance as Vulkan doesn't do the idea of "sensible default".
So, everybody tries to "encapsulate" this boilerplate.
The problem is that this obscures the fact that you are setting up things which have distinct dependency orders. And sometimes those orders are different depending upon what you are trying to do.
If you already understand advanced graphics card hardware, dependency encapsulation is great. If you really don't yet have the ideas of things like subpasses, pipelines, graphics queues, and queue synchronization really crystal clear, you don't want things abstracted as you are still experimenting and prodding the code in weird ways.
The first fallacy is only considering the size of the nodes and ignoring the number of edges. This is like micro-optimising without paying attention to the Big-O factor.
The second fallacy is failing to consider the difference between what I will call static branches and runtime branches. An if statement or a for loop are static branches. Other forms of indirection happen at runtime (with "goto" being famously difficult to analyse). Static indirection is easy to analyse, runtime indirection is hard.
The code graph (I use this term to mean something slightly simpler than the call-graph, which includes things like recursion) tends to get considerably more complex at runtime. One of the best ways to make your runtime code graph more complex is to add more functions, especially ones that are not private to the current file.
As soon as this happens, you have no immediate way of knowing how many runtime edges there will be going in and out of each one of those functions. So hypothetically, let's say you take a 21-line function and break it into 3, 7-line functions. While each of those is easy to "read", you still have to understand 21 lines of interdependent code, but now the total complexity has potentially increased exponentially.
How bad this problem is depends on the scope of the functions (can dramatically increase how many call-sites can potentially form new runtime edges - something you cannot tell from looking at the code in front of you) and whether that code has any side-effects. Certain types of runtime indirection like DI frameworks can completely thwart attempts to statically analyse this runtime graph.
If you are only dealing with pure functions then it can seem like this isn't a problem because of referential transparency. However, this ignores two problems: the name of the function must precisely describe what it does, otherwise it will mislead "distant code readers" (people reading the call but not the source). That must also never change in the future, otherwise you will still have to deal with all that extra complexity if you ever want to refactor safely.
This is not to say that abstractions are bad or that long functions are good. Premature abstraction however tends to produce bad abstractions, which are worse by far than no abstraction.
maybe, but it should still be more widespread
- keep functions simple and single purpose, which makes them easy to understand quicky - avoid long lines of code since they don't fit uniformly on one screen - make functions as testable as is reasonable
Whenever you introduce hard rules, you get weird workarounds for cases that don't fit nicely. Dogma is always bad, concepts are immortal.
So yeah, if this helps teach a concept, use it, but don't force everything to adhere to a strict code style.