Documentation also (can) tell you why the code is a certain way. The code itself can only answer "what" and "how" questions.
The simplest case to show this is a function with two possible implementations, one simple but buggy in a subtle way and one more complicated but correct. If you don't explain in documentation (e.g. comments) why you went the more complicated route, someone might come along and "simplify" things to incorrectness, and the best case is they'll rediscover what you already knew in the first place, and fix their own mistake, wasting time in the process.
Some might claim unit tests will solve this, but I don't think that's true. All they can tell you is that something is wrong, they can't impart a solid understanding of why it is wrong. They're just a fail-safe.
You start saying you did X because of Y, and Y is weird because of Z, and so X is the way it is because you can’t change Z... hold on. Why can’t I change Z? I can totally change Z.
Documentation is just the rubber duck trick, but in writing and without looking like a crazy person.
Also, writing it down lets someone else, later, take the role of the duck and benefit from your explanation to yourself.
I've done this to myself. It sucks. Revisiting years old code is often like reading something someone else entirely wrote, and you can be tempted to think when looking at an overly complex solution that you were just confused when you wrote it and it's easily simplified (which can be true! We hopefully grow and become better as time goes on), instead of the fact that you're missing the extra complexity of the problem which is just out of sight.
Every step of the process seemed like a local improvement, and yet I ended up where I started. It was like the programming equivalent of the Escher staircase: https://i.ytimg.com/vi/UCtTbdWdyOs/hqdefault.jpg.
Yes. Tests will solve this. Your point is perfect for tests.
If another experienced coder cannot comprehend from the tests why something is wrong, then improve the tests. Use any mix of literate programming, semantic names, domain driven design, test doubles, custom matchers, dependency injections, and the like.
If you can point to a specific example of your statement, i.e. a complex method that you feel can be explained in documentation yet not in tests, I'm happy to take a crack at writing the tests.
Sometimes you just have to pick the right tool for the job, and sometimes that tool is prose. I think if you get too stuck on using one tool (e.g. unit tests), you sometimes get to the point where you start thinking that anything that can't be done with that tool isn't worth doing, which is also wrong.
OTOH, system tests provide a realm where external implicit/explicit requirements may actually be validated. Perhaps.
// This implementation is unnatural but it is needed in order to mitigate a hardware bug on TI Sitara AM437x, see http://some.url
(that's an obvious one; but there are plenty of other cases where documentation is easier than a test)
Combining the two is good, but let's not act like the tests themselves immediately solve the problem.
import cPickle
def transform(data):
with open('my.pkl', 'r') as f:
model = cPickle.load(f)
return model.predict(data) float FastInvSqrt(float x) {
float xhalf = 0.5f * x;
int i = *(int*)&x; // evil floating point bit level hacking
i = 0x5f3759df - (i >> 1); // what the fuck?
x = *(float*)&i;
x = x*(1.5f-(xhalf*x*x));
return x;
}
I can't think of a way to write a test that sufficiently explains "gets within a certain error margin of the correct answer yet is much much faster than the naive way."The only way to test an expected input/output pair is to run the input through that function. If you test that, you're just testing that the function never changes. What if the magic number changed several times during development, do you recalculate all the tests?
You could create the tests to be within a certain tolerance of the number. Well how do you stop a programmer from replacing it with
return 1.0/sqrt(x);
And then complaining when the game now runs at less than 1 frame per second?Here's a commented version of the same function from betterexplained.com.
float InvSqrt(float x){
float xhalf = 0.5f * x;
int i = *(int*)&x; // store floating-point bits in integer
i = 0x5f3759df - (i >> 1); // initial guess for Newton's method
x = *(float*)&i; // convert new bits into float
x = x*(1.5f - xhalf*x*x); // One round of Newton's method
return x;
}
It's still very magic looking to me, but now I get vaguely that it's based on Newton's method and what each line is doing if I needed to modify them.I actually just found this article [0] where someone is trying to find the original author of that function, and no one on the Quake 3 team can remember who wrote it, or why it was slightly different than other versions of the FastInvSqrt they had written.
> which actually is doing a floating point computation in integer - it took a long time to figure out how and why this works, and I can't remember the details anymore
This made me chuckle. The person eventually tracked down as closest to having written the original thing had to rederive how the function works the first time, and can't remember exactly how it works now.
I think the answer is both tests and documentation. Sometimes you do need both. Sometimes you don't, but the person after you will.
Sometimes it gets worse still: you can have different theories according to (a) scientists doing basic research into physics or human perception/cognition, (b) computer science researchers inventing publishable papers/demos, (c) product managers or others making executive product decisions about what to implement, (d) low-level programmers doing the implementation, (e) user interface designers, (f) instructors and documentation authors, (h) marketers, (h) users of the software, and finally (i) the code itself.
Unless a critical proportion of the people in various stages of the process have a reasonable cross-disciplinary understanding and effective communication skills, models tend to diverge and software and its use go to shit.
There are some great comments about this buried in https://hn.algolia.com/?query=naur%20theory&sort=byPopularit....
The OP makes excellent points concerning the relative independence of design and code in the context of the "extreme programming" paradigm having become very common if not dominant.
-- Linus Torvalds
Isn't this definition circular, using "programmed" in defining "programming"?
[1] You could have probably guessed that the name was going to be "programming", but it might not have been.
It matters more when designing libraries/frameworks than one-off apps.
Switching to a new framework/platform/language at the point the one you were on before finally matured enough that it was hard to ignore the need for good design doesn't actually help. you'll still be back eventually.
"So you update the code, a test fails, and you think “'Oh. One of the details changed.'"
Some of the concerns they raise about writing tests are covered by Uncle Bob here: http://blog.cleancoder.com/uncle-bob/2017/10/03/TestContrava... and here: http://blog.cleancoder.com/uncle-bob/2016/03/19/GivingUpOnTD...
From my own limited experience it can make explaining a program to someone new almost trivial. You just use the various flows defined as almost visual guides to what is happening. I don't want to say FBP is a silver bullet, but I think it points to the idea that it is possible capture much more of the theory and design of the program in the code.
Basically our philosophy is this: a small system like a booking system which gets designed with service-design, and developed by one guy won’t really need to be altered much before it’s end of life.
We document how it interfaces with our other systems, and the as-is + to-be parts of the business that it changes, but beyond that we basically build it to become obsolete.
The reason behind this was actually IoT. We’ve installed sensors in things like trash cans to tell us when they are full. Roads to tell us when they are freezing. Pressure wells to tell us where we have a leak (saves us millions each year btw). And stuff like that.
When we were doing this, our approach was “how do we maintain these things?”. But the truth is, a municipal trash can has a shorter lifespan than the IoT censor, so we simply don’t maintain them.
This got us thinking about our small scale software, which is typically web-apps, because we can’t rightly install/manage 350 different programs on 7000 user PCs. Anyway, when we look at the lifespan of these, they don’t last more than a few years before their tech and often their entire purpose is obsolete. They very often only serve a single or maybe two or three purposes, so if they fail it’s blatantly obvious what went wrong.
So we’ve stopped worrying about things like automatic testing. It certainly makes sense on systems where “big” and “longevity” are things but it’s also time consuming.
And that's the problem. We need ways to make those higher level designs (~architecture) code.
I love them, I can express some things very concisely and even clearly. But there's no direct connection to the code and so keeping things synchronized (like keeping comments synchronized with code) is nigh impossible.
We need the details of these higher level models encoded in the language in a way that forces us to keep them synced. Type driven development seems like one possible route for this, and another is integrating the proof languages as is done with tools like Spark (for Ada).
This will reduce the speed of development, in some ways, but hopefully the improvement in reliability and the greater ability to communicate purpose of code along with the code will also improve maintainability and offset the initial lost time.
And by keeping it optional (or parts of it optional) you can choose (has to be a concious choice) to take on the technical debt of not including the proofs or details in your code (like people who choose to leave out various testing methodologies today).
My admittedly very brief experience with formal methods was that they were actually less close to the "design" of the software than the code. So not sure that's a direction that will get us anywhere we need to go.
>in a way that forces us to keep them synced.
Why "synced"? Wouldn't it be better if those higher level designs were actually coded up and simply part of the implementation, but at a level of abstraction that is appropriate to the design.
We used to increase our level of abstraction, but now we appear to have been stuck for the last 30-40 years or so. At least I don't see anything that's as much of a difference to, let's say Smalltalk as Smalltalk is to assembly language.
I think if it took something like JSdoc and have it more teeth you could do something like this in just about any of the dynamically typed languages.
https://fsharpforfunandprofit.com/series/designing-with-type...
> We need model based editing environments that will allow us to have a much richer set of software building blocks.
And looking at rust, I can start to imagine a future where macros are powerful enough to support a lot of declarative coding.
When coding javascript today I write code like:
// can be imported, and api.router() mounted in express
let api = new API(...);
module.exports = api;
api.declare({
method: 'get',
route: '/hello-world',
description: `bla bla bla...`,
scopes: {AnyOf: ['some-permission-string']},
// (more properties)
}, (req, res) => {...});
Effectively making large parts of the app declarative. It's still far from powerful enough. But I'm not sure giving up text is the way to get more powerful building blocks.Declaring JSON + function is super powerful in JS. In rust macros might allow us to make constructs similar to my "API" creator, but with static typing. And who knows maybe macros can expose meta-information to the IDE...
As far as we can tell, the technology that can create a piece of exact code from a vague specification is called strong AI.
Heck, we don't even have a language to describe vague specifications without loss of fidelity. We don't know if such a language can exist.
Of course I could be wrong.
This is not unlike the domains of philosophy, morality, ethics, and law. Attempting to express or enforce philosophy and morality via legalism is an exercise in futility, and even ethics which appears to be on the same level as law actually isn't since the presumption of ethics is behavior even in the absence of a law.
How do you get the product you want when you don’t know what you want?
Wouldn't it be better to use data abstraction instead of abusing primitive types?
For instance dates are often abstracted as a Date type instead of directly manipulating a bare int or long, which can be used internally to encode a date.
So, age, which isn't an int conceptually (should age^3 be a legal operation on an age?), could be modelled with an Age type. This, on top of preventing nonsense operations, also allows automatic invariant checking (age > 0), and to encapsulate representation (for instance changing it from an int representing the current age to a date of birth).
Would be better than
return x >= ASCII_A;
surely. ASCII_A could be set incorrectly, or have a dumb type, and is more verbose anyway. By using the character directly, the code speaks its purpose.
I disagree. ASCII_A speaks it's purpose (we purposefully want an ASCII A stored here). And one can check the constant's definition, and immediately tell if it's correct. E.g.
const ASCII_A = 'A' // correct
const ASCII_A = 'E' // wrong
So: return x >= ASCII_A
tell us the intention of the code's author.Whereas:
return x >= ‘A’;
only tells us what the code does, which might nor might not be correct (and we have no way of knowing, without some other documentation).So, by those two lines:
const ASCII_A = 'E';
(...)
return x >= ASCII_A;
We know what the code is meant to do, AND that it does it wrongly (and thus, we know what to fix).These line, on the other hand:
return x >= ‘A’;
tells us nothing. Should it be 'A'? Should it be something else? We don't know.(If you say it's because it's written twice, well, that's only a valid clue if ASCII_E doesn't happen to be defined too.)
Gets the whole message across in one line, as does using 65 with the comment.
Of course, all of this is likely overkill for your specific example. If I'm writing a to_hex routine, I'm not going to extract those constants as the context & commonplaceness of the algorithm makes it redundant. For the same reason that one might write i++ in a for loop instead of i += ONE. However, extracting inline constants to named variables is frequently something I look out for in code review, especially the more frequently the same constant appears in multiple places, the more difficulty a reader might have trying to understand why that value is the way it is (or if there's any discussion at all), or if it's a value that will potentially change over time. The negative drawbacks of extracting constants is typically minimal & with modern-day refactoring it's a very small ask of the contributor.
> ASCII_A
It comes down to naming and purpose.
The example, ASCII_A, is terrible because it doesn't describe the purpose with its name.
What will end up happening in any large codebase is ASCII_A will get reused in dozens of different places for dozens of different reasons.
If it was named minValidLetterForAlgorithmX it would convey intent and its more likely to be used correctly.
I'm not so sure it's a straw man, I often see defining constants like this cargo culted even if there are only one or two uses. In that case 'A' is great because it's value is right there, I don't have to look at the assignment and then go look up what the actual value is, so it's more readable.
When it's used in several disparate places then ASCII_A is better and your arguments about correctness should take precedence, we sacrifice some readability but it's worth it.
But you’re channeling some crazy madness suggesting that someone would use ‘A’ to mean 65. Shudder. I guess we’ve all seen some horrors over the years.
> ASCII_A (usually spelled just 'A')
Of course, they are not the same thing. In the last 6 months I've worked on a very old system that uses not-quite-ASCII. 'A' was 65 but '#' wasn't 35.
If A signifies something else, use that name; otherwise just use plain 'A': it already gives us as much information as needed, and has one less place where the programmer can screw up.
As an aside, if someone changes the constant value of ‘A’ now, the world will be broken for a while. (But my code would recompile correctly unchanged with the new standard header.)
Fatal mistake? Really? An unrecoverable failure?
So, none of the software I've written in the last decade worked, despite all evidence to the contrary?
Right.