This article is very important. I encourage everyone to read it, as it raises a lot of good points.
In my opinion, the problem it describes come from the vagueness of the concept of elegance, and how counterintuitive it is. It doesn't even have to imply modules or anything like that. In the simplest form it boils down to:
Would you rather write:
if (a) {
doSomething();
if (b) {
doSomethingElse();
}
}
or
if (a && b) {
doSomething();
doSomethingElse();
}
else if (a && !b) {
doSomething();
}
Of course, there is no true answer to that question, and it always depends on the context. But many programmers will never ever consider option 2. And that is for two good reasons: 1, you make one more test, and 2 you duplicate the function call do doSomething(), so your program is larger. So mathematically speaking option 1 is more elegant, it's shorter, it's faster, it's lighter, what's not to like?
Well, multiply the ifs and the elses, and you will soon find out that option 2 is much more readable and changeable, which is the more elegant solution to anyone who's an engineer rather than a mathematician.
The tension it describes is that in every programmer is a mathematician and an engineer endlessly conflicting. This should be a good guidance for which style to use. Is this code a math code, or is it engineer's code. When you can answer that question, you can decide which style to write your code in.
"Changeable" is the key here. We coders are trained for abstracting things, but real world requirements sometimes change and your perfectly abstract module needs some non-generic functionality. Of course, there are software engineering techniques to solve these problems, but hey you didn't foresee these changes and now need to bill your client for these adjustments.
So the trick is achieving a good balance between redundancy and dependencies. This is probably what seniority means in software development.
Option 2 is less changeable if you now also have to consider e.g. `a && c`.
I'd argue it was less readable too (two `doSomething()` to consider vs one is bad) but this is largely personal prejudice - I'd always spell things out "noddy fashion" like Option 1 because I am a simple brained developer.
Of course it depends on the case. But the point is that if you chose Option 1, and you have to change doSomething() in to doYetSomethingElse() in the case where a && !b, then you _will_ have to change the structure to Option 2. So in many cases, it's better to go to Option 2 directly.
It's _vital_ to know what a, b, doSomething and doSomethingElse actually stand for, and what is the context. This is not a technical problem, it's a people problem (like almost all problems).
Take this, for example:
if (userLoggedIn) {
setCookie();
if (itsANiceDay) {
tellUSerToGoOutAndPlay();
}
}
And this:
if (itsSunny && itsCold) {
putOnSunglasses();
putOnCoat();
} else if (itsSunny && !itsCold) {
putOnSunglasses();
}
They both kind of make sense! Now let's see the other way around:
if (userLoggedIn && itsANiceDay) {
setCookie();
tellUSerToGoOutAndPlay();
} else if (userLoggedIn && !itsANiceDay) {
setCookie();
}
This one is just silly. We are mixing logic about two completely different things. Nobody would do that (many people would do that, I know. Too many)
if (itsSunny) {
putOnSunglasses();
if (itsCold) {
putOnCoat();
}
}
This one seems ok. Until you wonder what happens if it rains and/or it's windy. Then you'll refactor it to a "switch/case" for clarity.
And it's all about the domain specific content! You cannot reason only about the syntax tree in isolation and come to a meaningful conclusion!
The problem with these examples is that putOnCoat() should not depend in any way on itsSunny, so there should just be two separate if blocks. We should be discussing a scenario in which one condition is a subset of the other.
>Well, multiply the ifs and the elses, and you will soon find out that option 2 is much more readable and changeable, which is the more elegant solution to anyone who's an engineer rather than a mathematician.
Disagree. In the first case I know only one block (and its parents) can execute. When all the ifs are isolated, it takes much more reading to know that only one will trigger, and that's after I make sure 'doSomething' won't change a or b.
The real way to write that is probably a switch statement.
I would consider that doing so makes it a different problem, and so what becomes the better solution changes. If you need very many different combinations, you should probably use a table-based approach instead (produce an integer from each of the booleans, index into a table of function pointers or similar; you can often reduce its size if you put "don't cares" into the more significant bits.) I think the second option is more difficult to understand since you now have to consider together all 4 different combinations of a and b to determine what code gets executed for each one. For the first option, it is immediately obvious that none of those functions execute if a is false, whereas in the second you have to inspect each if-condition.
I don't like repeating function calls, if logic calls for complex conditions (which I don't think the example does) I find it better to express them in new booleans before the if, keeping the logic contained there, and call them on a:
bool shouldDoSomethingElse = a && b;
if(a) doSomething();
if(shouldDoSomethingElse) doSomethingElse();
I like this technique for avoiding comments in code as the variable name describes what the condition is checking but I don't see it used often for some reason.
I like it too, but I assume it's probably not used very often just because it forces you to write more boilerplate code, which some may see as "verbose".
I understand what you're saying, but I can see certain devs having a problem with introducing extra variables only to use them in a single conditional. Not that I agree with that, but that's the only reason I can come up with.
I think adding extra variables (functions, modules, …) with meaningful names for exposition purposes is qualitatively different to adding boilerplate.
The extra variable can establish a meaningful new concept, making the code more self-documenting and thus improving readability and maintainability. The value added is similar to grouping related concepts in a namespace rather than making everything global, or to adding well-chosen comments at key points in the code for a tricky algorithm. Most experienced programmers follow the same principle routinely when it comes to naming constants instead of writing literal values all over their code.
In contrast, I think “boilerplate” has connotations of something verbose that you must write every time you want to express a certain concept, even though it has no inherent value itself.
I generally find that if statements with multiple tests are a frequent source of bugs; because it's easy for someone to add another term to one test without realizing they need to update the other. (Especially if the function is long. Yeah you should write short functions, but long functions happen.)
Your second example is much harder to reason about if someone comes in and adds:
if ( (a && b) || c)
So then what happens with the second clause? (Or what even should happen with the second clause?)
On a meta note, any time I see multiple tests in an if statement, I try to pull it into local booleans that describe the intent instead. At the very least then if you add more conditions to the if statement, you have to update the names of the boolean for it to make sense, and so the code stays implicitly commented.
The article makes some very important points and it's certainly worth a read for every programmer.
What the article misses to address explicitly however is that the whole redundancy vs. dependency conflict is caused by modularization. Without modules there would be no conflict.
So the real questions to answer here are: When do you need modules or do you need them at all? What should be modularized? And, most importantly, how to choose smart boundaries? Good answers to these questions will save a project from a world of pain down the road.
The classic OOP / software engineering education these days lacks critical debate about software modularization. Modularization is almost always presented as a good thing. What nobody tells you however is that in real world engineering, on real world teams, modularization can cause a lot of trouble if not done the right way.
Code is basically a dependency graph. Each piece of code depends on a number of other pieces of code. (Dead code is an isolated island in this dependency graph.)
You want two things out of that graph: less nodes, because less code is simpler, better, cheaper. And less edges, because understanding, modifying, or troubleshooting a piece of code requires knowing about its dependencies (hopefully, only the direct ones).
When the unit of organisation is the function, you kinda state that each function is a node, and the call graph are part of the edges (the call graph would cover everything in a purely functional settings, but side effect produces implicit dependencies). Trouble is, in any significant system, you're gonna have a lot of nodes and edges. How to make sense of that?
That's why we have module. When you look at your dependency graph, you will most certainly note that parts of your graph are denser than others. Those clusters are the natural modules. If you formalise that, and draw module boundaries around those clusters, you can now have a two-level view: inside a module, you have a small dependency graph, with a few outbound edges. Outside, you can visualise a coarser graph of module dependencies. Again, fewer nodes and edges, because you have grouped them.
Now the real benefits of module is, once you start drawing boundaries, you have an incentive to make small interfaces, to minimise inter-module dependencies. Additionally, visualising the module dependency graph directly helps you spot spooky dependencies that probably shouldn't be there. You can then cut some dependencies out, simplifying your graph in the process.
Without modules, I don't see how you would manage this kind of scale. Oh and by the way, some monstrosities are so big that they effectively requires a third level. But I've never worked on such beasts.
Yes, that's a very good picture of things. Now the dependency vs redundancy issue comes in when two clusters are made modules.
If they are mostly separated but still have some connections to each other the question is what to do with these. Cut them off means having to replicate so ultimately redundancy. Leaving them in means dependency.
I guess the way modularization should be done is therefore as a min-cut through the dependency graph.
A very good article that aptly shows (some of) the hard & sticky questions we are confronted with all the time. How you answer these questions will determine the quality & stability of your code to a large extent.
I agree with the OP that commonly, “dependencies are worse”. Redundancy will increase the quantity of your code, but dependencies increase its complexity. And quantity is always conquered easier than complexity.
It's kind of funny how, rightfully, the author paints a picture where the "horrible, enlightened external dependency" itself is antimodular to the T. Given that all modules supposedly have stable interfaces, documentation, tests, reasonable size, yada yada then one might expect that each of their dependencies takes advantage of these properties to maintain light and wonderful themselves.
Of course, this is a situation that's highly incompatible with C. Let's ditch that.
In ML modules are king. You probably make hundreds in any non-trivial program and the compiler will beat your ass if you muck up their interfaces. Anywhere. Packages are just sets of 3 public modules wrapped up in twine and a README file (coincidentally this is where "ownership and lifecycle" are managed, but, sorry, I'm going to ignore those for a moment).
This could be every bit as bad as I described before, but ML also realized that modules which just form a big dependency tree are actually quite annoying. The whole reason we define public APIs is so that there can be multiple satisficing inplementors, but this cannot be in 99% of module technologies today.
So ML has functors (not Haskell functors, certainly certainly certainly not C++ functors) which are "parameterized modules that actually work". One could distribute their command line parsing module with a pluggable serialization and a pluggable help display. See MirageOS for a giant example of this kind of system working out.
Does it really work?
Probably not. It's not in most maintainers DNA to functor-ize everything. It's even a significant challenge to do so since you need to define sufficient external and internal public APIs and it's a significant community effort to standardize these sufficiently so that there is significant chance of re-use.
But at least it's a way forward. Fight the heavy module trees. Let's use some higher order reusability.
I've had some success with the object-oriented equivalent of the pattern. Perversely, I find it to be most effective as a political tool.
It's useful when someone doesn't like my minimalist solution to some problem, and starts peppering me with feature requests that will complicate the module and which I perceive to be of marginal utility. So I make that chunk of functionality pluggable, keep my minimalist implementation as the default, and publish some instructions for how to drop in a more complicated behavior. Then all I have to do is sit back and watch the original requester realize that they only think the stuff they were asking for is worth the effort if they can get someone else to be the one putting out the effort.
This depends greatly on your platform. Java projects accept dependencies much easier than C++ ones because in Java it's much harder to cause trouble and also coding styles aren't radically different for different dependencies.
Perl&Ruby are even more eager, which should be strange since they're actually less safe.
Excellent article. Just one quibble: he claims a module shouldn't be over 30k lines. Counterexamples: Linux, Postgres, Boost, LLVM, V8, all in the million line range. To be sure, each of these has an internal module structure, but that's irrelevant from the perspective of someone deciding whether to incur a dependency on one of them - the answer to which may very well be yes because they do enough to make it worthwhile.
If anything, larger modules like the ones I listed are more likely to be worth depending on because they do more. It's no coincidence that the author chooses command line parsing as a negative example - something trivial enough that the overhead of tracking a dependency may well outweigh the effort of implementing it yourself.
I generally agree with this. However, sometimes using a module is not adding a dependency, it is making an already existing implicit dependency explicit.
E.g. we have client and server code. Serialization configuration between the two is implicitly dependent upon each other - if the client expects dates in a different format than the server, things don't work. To make that implicit dependency explicit, we use a module, which also has the affect of making sure the two don't get out of synch.
42 comments
[ 2.9 ms ] story [ 97.2 ms ] threadIn my opinion, the problem it describes come from the vagueness of the concept of elegance, and how counterintuitive it is. It doesn't even have to imply modules or anything like that. In the simplest form it boils down to:
Would you rather write:
or Of course, there is no true answer to that question, and it always depends on the context. But many programmers will never ever consider option 2. And that is for two good reasons: 1, you make one more test, and 2 you duplicate the function call do doSomething(), so your program is larger. So mathematically speaking option 1 is more elegant, it's shorter, it's faster, it's lighter, what's not to like?Well, multiply the ifs and the elses, and you will soon find out that option 2 is much more readable and changeable, which is the more elegant solution to anyone who's an engineer rather than a mathematician.
The tension it describes is that in every programmer is a mathematician and an engineer endlessly conflicting. This should be a good guidance for which style to use. Is this code a math code, or is it engineer's code. When you can answer that question, you can decide which style to write your code in.
So the trick is achieving a good balance between redundancy and dependencies. This is probably what seniority means in software development.
I'd argue it was less readable too (two `doSomething()` to consider vs one is bad) but this is largely personal prejudice - I'd always spell things out "noddy fashion" like Option 1 because I am a simple brained developer.
It's _vital_ to know what a, b, doSomething and doSomethingElse actually stand for, and what is the context. This is not a technical problem, it's a people problem (like almost all problems).
Take this, for example:
And this: They both kind of make sense! Now let's see the other way around: This one is just silly. We are mixing logic about two completely different things. Nobody would do that (many people would do that, I know. Too many) This one seems ok. Until you wonder what happens if it rains and/or it's windy. Then you'll refactor it to a "switch/case" for clarity. And it's all about the domain specific content! You cannot reason only about the syntax tree in isolation and come to a meaningful conclusion!Disagree. In the first case I know only one block (and its parents) can execute. When all the ifs are isolated, it takes much more reading to know that only one will trigger, and that's after I make sure 'doSomething' won't change a or b.
The real way to write that is probably a switch statement.
I would consider that doing so makes it a different problem, and so what becomes the better solution changes. If you need very many different combinations, you should probably use a table-based approach instead (produce an integer from each of the booleans, index into a table of function pointers or similar; you can often reduce its size if you put "don't cares" into the more significant bits.) I think the second option is more difficult to understand since you now have to consider together all 4 different combinations of a and b to determine what code gets executed for each one. For the first option, it is immediately obvious that none of those functions execute if a is false, whereas in the second you have to inspect each if-condition.
PS: The real question is does doSomethingElse depend on doSomething.
For example, I prefer:
To this: I find comments frequently become inaccurate compared to function and variable names.The extra variable can establish a meaningful new concept, making the code more self-documenting and thus improving readability and maintainability. The value added is similar to grouping related concepts in a namespace rather than making everything global, or to adding well-chosen comments at key points in the code for a tricky algorithm. Most experienced programmers follow the same principle routinely when it comes to naming constants instead of writing literal values all over their code.
In contrast, I think “boilerplate” has connotations of something verbose that you must write every time you want to express a certain concept, even though it has no inherent value itself.
Your second example is much harder to reason about if someone comes in and adds:
So then what happens with the second clause? (Or what even should happen with the second clause?)On a meta note, any time I see multiple tests in an if statement, I try to pull it into local booleans that describe the intent instead. At the very least then if you add more conditions to the if statement, you have to update the names of the boolean for it to make sense, and so the code stays implicitly commented.
What the article misses to address explicitly however is that the whole redundancy vs. dependency conflict is caused by modularization. Without modules there would be no conflict.
So the real questions to answer here are: When do you need modules or do you need them at all? What should be modularized? And, most importantly, how to choose smart boundaries? Good answers to these questions will save a project from a world of pain down the road.
The classic OOP / software engineering education these days lacks critical debate about software modularization. Modularization is almost always presented as a good thing. What nobody tells you however is that in real world engineering, on real world teams, modularization can cause a lot of trouble if not done the right way.
You want two things out of that graph: less nodes, because less code is simpler, better, cheaper. And less edges, because understanding, modifying, or troubleshooting a piece of code requires knowing about its dependencies (hopefully, only the direct ones).
When the unit of organisation is the function, you kinda state that each function is a node, and the call graph are part of the edges (the call graph would cover everything in a purely functional settings, but side effect produces implicit dependencies). Trouble is, in any significant system, you're gonna have a lot of nodes and edges. How to make sense of that?
That's why we have module. When you look at your dependency graph, you will most certainly note that parts of your graph are denser than others. Those clusters are the natural modules. If you formalise that, and draw module boundaries around those clusters, you can now have a two-level view: inside a module, you have a small dependency graph, with a few outbound edges. Outside, you can visualise a coarser graph of module dependencies. Again, fewer nodes and edges, because you have grouped them.
Now the real benefits of module is, once you start drawing boundaries, you have an incentive to make small interfaces, to minimise inter-module dependencies. Additionally, visualising the module dependency graph directly helps you spot spooky dependencies that probably shouldn't be there. You can then cut some dependencies out, simplifying your graph in the process.
Without modules, I don't see how you would manage this kind of scale. Oh and by the way, some monstrosities are so big that they effectively requires a third level. But I've never worked on such beasts.
If they are mostly separated but still have some connections to each other the question is what to do with these. Cut them off means having to replicate so ultimately redundancy. Leaving them in means dependency.
I guess the way modularization should be done is therefore as a min-cut through the dependency graph.
I agree with the OP that commonly, “dependencies are worse”. Redundancy will increase the quantity of your code, but dependencies increase its complexity. And quantity is always conquered easier than complexity.
Of course, this is a situation that's highly incompatible with C. Let's ditch that.
In ML modules are king. You probably make hundreds in any non-trivial program and the compiler will beat your ass if you muck up their interfaces. Anywhere. Packages are just sets of 3 public modules wrapped up in twine and a README file (coincidentally this is where "ownership and lifecycle" are managed, but, sorry, I'm going to ignore those for a moment).
This could be every bit as bad as I described before, but ML also realized that modules which just form a big dependency tree are actually quite annoying. The whole reason we define public APIs is so that there can be multiple satisficing inplementors, but this cannot be in 99% of module technologies today.
So ML has functors (not Haskell functors, certainly certainly certainly not C++ functors) which are "parameterized modules that actually work". One could distribute their command line parsing module with a pluggable serialization and a pluggable help display. See MirageOS for a giant example of this kind of system working out.
Does it really work?
Probably not. It's not in most maintainers DNA to functor-ize everything. It's even a significant challenge to do so since you need to define sufficient external and internal public APIs and it's a significant community effort to standardize these sufficiently so that there is significant chance of re-use.
But at least it's a way forward. Fight the heavy module trees. Let's use some higher order reusability.
It's useful when someone doesn't like my minimalist solution to some problem, and starts peppering me with feature requests that will complicate the module and which I perceive to be of marginal utility. So I make that chunk of functionality pluggable, keep my minimalist implementation as the default, and publish some instructions for how to drop in a more complicated behavior. Then all I have to do is sit back and watch the original requester realize that they only think the stuff they were asking for is worth the effort if they can get someone else to be the one putting out the effort.
Perl&Ruby are even more eager, which should be strange since they're actually less safe.
If anything, larger modules like the ones I listed are more likely to be worth depending on because they do more. It's no coincidence that the author chooses command line parsing as a negative example - something trivial enough that the overhead of tracking a dependency may well outweigh the effort of implementing it yourself.
E.g. we have client and server code. Serialization configuration between the two is implicitly dependent upon each other - if the client expects dates in a different format than the server, things don't work. To make that implicit dependency explicit, we use a module, which also has the affect of making sure the two don't get out of synch.