DEV Community

loading...
Cover image for 10 rules to code like NASA (applied to interpreted languages)

10 rules to code like NASA (applied to interpreted languages)

Rémy 🤖 on July 20, 2019

Foreword — Dear beginner, dear not-so-beginner, dear reader. This article is a lot to take in. You'll need perspective for it to make sense. Once ...
pic
Editor guide
Collapse
juancarlospaco profile image
Juan Carlos

You got some things wrong about it.

switch gets compiled to if usually, being compiled to machine code or bytecode but still.

About the assertions it refers to assert whatever the language is, and is also referring to Design by Contract.

About the preprocessor is referring to Macros and Metaprogramming, not about Transpilers.

I prefer a 100x100 maximum, 100 line length per 100 lines per function.

Collapse
xowap profile image
Rémy 🤖 Author

Those are interesting points, to reply in order:

  • The way switches are compiled is not-so-relevant to this point, because it is more about human perception of code flow than technical reasons. But I've stated it in other comments, the switch ban is really not my strongest take.
  • In the original paper, it refers very specifically to macros returning a boolean value which can be used in a if that can be used to exit the flow and return an error code, which is then taken care of by rule 7 (emit exceptions and let them bubble up).
  • That's also the understanding that I've had for a long time about the preprocessor rule and in fact I actually wrote a dual-axis section speaking about both transpiling and metaprogramming. However, I thought about metaprogramming in terms of harm. Yes you can do crazy stuff with it but it's so complicated that I never see anybody doing anything bad with it. The only use I see commonly is Django's models, forms and serializers which are actually awesome. I figured that C macros are easy and nasty while metaprogramming is usually hard but used sparringly and for good reasons.
  • Regarding the size, that looks reasonable as well. The JPL 60-lines feels right to me since most of my functions are much shorter anyways. Regarding the width, I guess there is some language-specific parameters to account for but the nice people behind black did some research and found that 88 is the best. Which is fine by me, it allows to sit two files side by side on a laptop screen at a readable font size. But honestly, as long as the numbers stay consistent project-wise, knock yourself out.
Collapse
morgenpeschke profile image
Morgen Peschke

IMO, the ability to display two bits of code side by side is by far the most useful outcome of restricting line lengths.

Collapse
vlasales profile image
Vlastimil Pospichal • Edited

60x80 is enough for a class.

Collapse
andrewharpin profile image
Andrew Harpin

I respect Remy wishes to avoid the switch compilation, there are typically 2 outputs from the compiler, depending on the code and optimization settings:

  1. If else as mentioned, this is typically used when there are few options in the switch statement or there is a large Delta between the switch enumerals.

  2. Jump table if there are a large number of cases and they are mostly sequential the assembler can be a jump table where you have a start and the enumeral is an offset, this is more performance efficient, but not always space efficient and this is where your optimization settings come in.

There are edge cases which result in some quirky behaviour, but these are typically compiler specific

Collapse
aminmansuri profile image
hidden_dude • Edited

These rules don't make sense for all types of software. NASA is working with embedded systems that are limited and don't use GCs. Also, I believe they aren't OO.

An anti-pattern in GC languages is to hold on to memory for a long time. If you aren't allocating and (implicitly) freeing objects rapidly in modern OO languages, then you're holding on to many objects for a long time. And that will cause the GC to work much harder in some cases. GCs today are optimized to get rid of short-lived objects, and perform poorly when too much data is escalated to become long lived.

Of course, the principle that you should limit your code to use O(1) RAM when possible is a good one. Because if you don't you'll get out of memory errors. Of course, the impact of that showing up in a web app is not the same as your robot crashing right before it lands on Mars (thus causing a physical crash and a loss of 1 billion dollars and 20 years of work).

My point is software is different. And rules need to reflect those differences. NASA for example is not known to be very good with staying within timelines and budgets. For commercial software that is probably much more important that avoiding an improbably crash now and then.

Collapse
andrewharpin profile image
Andrew Harpin

These rules are taken from MISRA, which is typically used for safety critical embedded software.

Yes they aren't always applicable for all software and languages, but they are good things to consider.

Collapse
xowap profile image
Rémy 🤖 Author

All I'm saying, in essence, is that you need to be accountable for the resources that your program uses. Otherwise you risk blowing things. More than once I've seen a dev app blowing up when reaching production volumes and I'm certainly not talking about Google-scale.

So the advice is more a O(whatever(n)) complexity in RAM and time but with a bound on n and thus a bound on whatever(n).

If you work on data, work by pages. If you work on a stream, work on N items at once. If you do big data, do Map/Reduce.

Also NASA was notoriously on time to put a man on the Moon, so I'm guessing that their methodology to push the boundaries of science and Humankind won't be a hindrance for more conventional tasks. At least in my case, these rules help me on a daily basis to deliver quality code on time.

But yeah first time I read that I was like "bollocks nobody needs that". I guess denial is a first step :)

Collapse
mikeschinkel profile image
Mike Schinkel

NASA also had a huge budget relative to the time period when they were putting a man on the moon. Something that most conventional tasks do not have the luxury of having.

Thread Thread
xowap profile image
Rémy 🤖 Author

Funnily enough there was no budget allocated to software on the Apollo program.

But you need the budget to prove everything, not to apply things in best effort mode. In my case, applying those rules saves time and not the opposite.

Thread Thread
mikeschinkel profile image
Mike Schinkel

"there was no budget allocated to software on the Apollo program."

In those days software was an afterthought for bean counters. Back then it was just rolled into "Development & Operations." And with $28.7 billion in inflation adjusted dollars for that line item, let's just say they had enough money to get it right. Which is rare for software projects today.

"applying those rules saves time and not the opposite"

Let me first say that I wrote a long comment that was subsequently eaten by my browser's interaction with this website and its lack of maintain a cookie-based copy of comments in progress. And that comment started out by saying that your article was great and that it had a lot of really good advice. Unfortunately I was weary of typing it in again so sadly it was lost to the ether.

But your article also had a few points of opinion, the nature of which is impossible to prove is time saving in its application. To assert otherwise would just be hubristic and would illustrate nothing more than confirmation bias. #fwiw

Collapse
willvincent profile image
Will Vincent

Many of these strike me as only relevant if you're passing off code to someone who is either very junior, or not familiar with the language the code is written in.

The switch argument being the best example of this.

if (foo === 'bar') {
  // do something
} else if (foo === 'baz') {
  // do something else
} else {
  // do a default thing
}

Really isn't any more readable than

switch(foo) {
  case 'bar':
    // do something
    break;
  case 'baz':
    // do something else
    break;
  default:
    // do a default thing
}

To anybody with a modicum of experience, or half a brain :D

As for the recursion argument, one could easily limit recursion depth by keeping track of how deeply you've recursed, and stopping when you hit the depth limit. Sure iteration might seem easier, until you run across a nested tree you need to parse that has varying levels of depth on each branch, and you want to write efficient code to parse it. Recursion definitely has it's place -- as do virtually every other thing you're arguing against.

Collapse
xowap profile image
Rémy 🤖 Author

If you need half a brain to understand and check a switch statement that's already half a brain you're not spending on other things.

It's like these boxes full of things that "might be useful later" that some people keep in their garage, in the hope that one day this piece of handheld barcode scanner will yield any utility. But as you might know the day rarely comes and in the meantime it's holding a LOT of space.

So regarding the switch, two things:

  • Either you use it as a if/else if/else, in which case the switch syntax itself is simply useless because totally identical to the other one
  • Either you use it for its weird control flow characteristics, in which case it's mind-bending and dangerously close to goto

Basically, either it's bad either it's useless. So that's not something I want to bother about.

Collapse
willvincent profile image
Will Vincent • Edited

That's a hell of a twist of my words. Never did I say it takes half your brain to understand, but that someone with half a brain CAN understand..

Let me put it another way since you clearly didn't understand. Switch statements are stupid simple to grasp, unless the person looking at it is a complete idiot -- or, as I also said, completely unfamiliar with the syntax of the language, in which case they oughtn't be poking around the code in the first place.

Obviously we're not going to agree, and that's fine, I don't have to work with you, so your desire to eliminate perfectly useful easily understood elements from code doesn't affect me :)

Collapse
jjtriff profile image
jjtriff

I agree, except on the being cruel part.

Switches are as readable as ifs if not more.

Recursion has its very needed place.

Collapse
temporal profile image
Jacek Złydach

Wouldn't overdo it. As others pointed out, those guidelines are tactical-level rules for working with a GC-free, memory-unsafe language in context of embedded realtime systems. This environment requires you to have total control over what the code does, how much time it spends on it, and how much memory it uses. Many of the points are making a trade-off for that control, against increased code complexity.

Some notes:

Point 1b - avoiding recursion. I'd chill out with this a bit; recursion ain't scary once you familiarize itself with it. It's rarely the best thing to do (unless you work with trees a lot), but for some problems, a recursive solution is the cleanest, most readable one. If your language supports Tail Call Optimization, it may even (in some cases) be as fast as iteration and not pose a risk of stack overflow.

Point 3 - avoiding heap allocations - is very much applicable to dynamic languages for performance reasons. Dynamic allocation costs performance (not as much as in C/C++ if your language runtime optimizes memory management for allocation, but still).

You can learn writing so-called "non-consing", i.e. not constructing, not allocating code, but it's tricky - such code may become less readable than the "regular" alternative, and definitely needs to be isolated because it can corrupt your data if references leak. The trick is to learn your language and standard library (and libraries you use), and pay attention to operations that modify its arguments, instead of returning a new value. They're sometimes called "destructive operations".

Consider e.g. following REPL session in Common Lisp:

CL-USER> (remove-if #'evenp (list 1 2 3 4 5 6 7))
(1 3 5 7)
CL-USER> (delete-if #'evenp (list 1 2 3 4 5 6 7))
(1 3 5 7)

The results are seemingly the same; the difference is that remove-if returns a fresh list, while delete-if modifies its list argument. In non-performance-critical places, you should prefer to use the remove-if variant, because it's safer, and GC can get rid of unused data just fine. delete-if is faster, because it only rebinds pointers in a list to drop some nodes, but if that (list 1 2 3 4 5 6 7) was passed in via reference, and someone else would hold that reference, that someone would discover the data changed under them.

To write non-allocating code, you should also use arrays more - they can be allocated once to required size and reused without reallocation. It's especially handy if your language supports unboxed primitives - for things like numbers, you can then rewrite your critical code to perform operations "in place", using preallocated arrays as temporary buffers for calculations.

Point 9b is subtly uncovered (avoid function pointers). In dynamic languages, it would translate to "avoid using functions as values" - avoid higher-order functions, avoid lambda expressions, etc. Applying it would be a huge loss to code clarity at no real win in context of web development (or really any non-embedded development).

Collapse
matthd profile image
MATHIAS D
  • The rule n°1 is very strange, if switch & continue exists it's for a reason, using them for what they was thought for is the best way of coding; a top layer for conditions. Yes you can instead use if-else because of .. security ?😅 But maybe you can use if only & not else because of security too ? 🥴
  • The 9.1 do not use reference, ouch, why ? same as above use them for what they are best for, if you only use copy variables what about scalability for very large element like some json parsed result ? Furthermore javascript for eg use reference only for object elements ..😄
Collapse
visum profile image
Benjamin

The fact that a feature exists in a language doesn't mean it's a feature worth using. These languages are designed by mortals and often inherit patterns and ideas from previous (and flawed) languages.

JavaScript, while beloved, was originally designed in a hurry and is famous for having "bad parts" which experienced developers not only avoid using but purposefully don't teach to newbies.

You say we should use these features for what they are best for. I agree that we should certainly not abuse any language feature, but the point here is that there are almost always better ways to solve the problems these features address. By "better" I mean "easier for you and others to understand next year" and "offering fewer places for bugs to hide."

Collapse
xowap profile image
Rémy 🤖 Author

Excellent questions, the article was already too long to dive into this.

Regarding the switch issue, it's because the syntax is like this:

$foo = "1";

switch ($foo) {
    case "1":
        print("1");
    case "2":
        print("2");
    case "3":
        print("3");
}

// Prints "123"

Were it like this

$foo = "1";

switch ($foo) {
    case ("1") {
        print("1");
    } case ("2") {
        print("2");
    } case ("3") {
        print("3");
    }
}

// Prints "1"

I wouldn't mind, but unfortunately it's not so the safest option is still a series of if/elseif/else.

Regarding your point on references, basically what they do is not just pass an object (which is a shared point of anchorage and can be used to retrieve subsequent data) but also make in sort that if you change the value of that object in the called function then the change bubbles up to the calling function (see the example in the article). Fortunately this feature just does not exist in Javascript so you don't have to worry about it :)

Collapse
mikeschinkel profile image
Mike Schinkel • Edited

Based on your dislike of switches because they do not have intermediate ending braces, my guess is you really hate Python? #justasking

Thread Thread
xowap profile image
Rémy 🤖 Author

Oh no I love Python, actually it makes these rules easier to apply and also it does not have switch.

But it's about the confusing control flow rather than the syntax (which I really don't care about). Forget a break and you're toast. Stack cases and you're confusing anybody reading your code.

Thread Thread
mikeschinkel profile image
Mike Schinkel • Edited

Ironically just a few weeks ago I read an article by Dave Cheney — the bard of Go — entitled "Clear is better than clever" that advocates for using switch instead of if / else.

His article does mention that the switch in Go does not fall-through by default and your objection to the switch — which falls-through by default (a language decision I lament) — seems to be solely based on the potential to omit a break. But his article does give other reasons for switch to be superior to if / else none of which are mentioned in your post.

Personally, I think a belief that in-all-cases either if or switch is superior to the other is simply allowing oneself to overindulge in one's own unsubstantiated opinion.

(Notice I left off else; I concur with @DoMiNeLa10 and agree with the advice of Mat Ryer: Avoid else, when you can.)

Earlier this week I refactored some if / else code to be a switch statement in PHP. I did so thoughtfully and with the sole goal of adding clarity to the code someone else wrote who is no longer involved. The code was used to take action based on number of path segments in a URL. It had one if and three (3) else ifs where each condition tested count($parts) for equality with 1, 2, 3 or 4.

I needed to add logic for 5 so I changed from if / else if / else to a using a switch so the code would not need to call count($parts) five (5) times (nor need to set a temp var) and so that the logic was clearly spelled out that it was working on a use-case that had 1 of 5 options.

Further, when formatted the code was much more readable, the breaks were obvious, and the nature of the code meant it was unlikely someone would ever add both a 6 and a 7 and forget to include a break on the 6.

My point with this anecdote is that almost all coding "rules" really should be applied with an analysis of the use-case at hand and not be considered as a pedantic absolute. Clinging to dogma really does not benefit anyone.

Unless of course we are dealing with junior programmers, and then all bets are off. :-D

Collapse
elmuerte profile image
Michiel Hendriks

The generations that came out after goto was considered harmful did indeed avoid using it. We're at the stage where we're debating if continue is goto and thus should be banned.

This not what Dijkstra was talking about. Dijkstra wrote that essay before we had structured programming. Where you did not call a function or method. But you had to jump to places. Make sure you prepared the stack correctly, and hope the target location did not change.

None of the "modern" languages support these types of jumps. The continue or break to a specific label is not the same, as it is still strictly scoped.

Collapse
xowap profile image
Rémy 🤖 Author

I'm not sure if you've already used goto but the few times where I thought that Dijkstra was wrong it felt like having main brains smashed in by a slice of lemon wrapped round a large gold brick. Whichever the language.

Collapse
elmuerte profile image
Michiel Hendriks

I have programmed in Basic. I have written code with line numbers. I have messed up that code.

Jumping to the wrong line was so easy, and so difficult to figure out.

Thread Thread
xowap profile image
Rémy 🤖 Author

Those were simpler times 😢

Collapse
paddy3118 profile image
Paddy3118 • Edited

Avoid complex flow constructs,

Always good to avoid unnecessary complexity, but I don't agree with arbitrarily avoiding recursion.

There are recusions whose depths are easily calculated, and whose code is easily reasoned about. Remember, the speed of a solution is often better governed/adjusted at the higher level. (millisecond shaving by using iteration instead of recursion vs saving seconds by changing algorithm and/or data structure).

Collapse
xowap profile image
Rémy 🤖 Author

Definitely, control structures are a debate that is faaar from over and also highly specific to each language. I've put here general observations but far from me to be definitive on that specific matter. It's more guiding ideas.

Regarding the variable re-assignation, I do it as little as possible. But sometimes in JS with block-scoping you need to use a let here and there to assign a value from inside a condition. Also there is the case of counters for loop iterations. If we were to make a rule like that it would require a lot of fiddling I think.

Collapse
bosepchuk profile image
Blaine Osepchuk

Great article!

I showed these NASA rules to my team several years ago and we discussed how we could apply their wisdom to our development efforts. We eventually ended up with something like “avoid dangerous practices and language features.”

I’ll share your article instead of the NASA rules next time we need a refresher on these concepts.

Collapse
xowap profile image
Rémy 🤖 Author

That's quite right. Also Python makes a lot of the rules in this guide quite natural. By example a lot of people are complaining about the switch rule but in fact that construct does not even exist in Python :)

morgenpeschke profile image
Morgen Peschke

Early return might not be language specific, but they are paradigm specific.

While returning early can make sense in a statement-oriented language, they aren't nearly as popular in expression-oriented languages.

In at least one (Scala), an explicit return of any kind, but especially an early return, is explicitly an anti-pattern.

Of course, most of these languages have pattern matching, which is a great alternate to both switch and complex if...else blocks.

Collapse
paddy3118 profile image
Paddy3118

All loops must have fixed bounds

... But don't introduce arbitrary bounds. Many C code exploits were based on exceeding array bounds. Scripting languages should use their automatic memory management. I should now argue that it is better to have a slower program that gives the correct result eventually, rather than faster code giving wrong results arbitrarily. Hmm...

Collapse
paddy3118 profile image
Paddy3118

Restrict functions to a single printed page

Update that to half a laptops screen so you can show diffs in meetings.

Collapse
xowap profile image
Rémy 🤖 Author

That makes a lot of sense

Collapse
nickhodges profile image
Nick Hodges

This is an outstanding article.

Collapse
paddy3118 profile image
Paddy3118

Use a minimum of two runtime assertions per function.

Don't go to silly limits with this though: why not check the ranges of x, y, and z in the one function of three assertions?

Collapse
jjtriff profile image
jjtriff

Also Coffeescript is not obscured JS, is safer and more humane to read. I'm really sad to see it's being left aside by TS

Collapse
dipubd profile image
Sudipto Chandra

So many rules! It gave me headace, but I can not find any bad points to refute against them.

Collapse
johnphamous profile image
John Pham

Great job! I didn't even know JPL had the Power of 10 rules having worked there.

Collapse
jjtriff profile image
jjtriff

You used a goto in Rule 5 of your own rules. ;-)

Collapse
paddy3118 profile image
Paddy3118 • Edited

Use the preprocessor sparingly.

This kinda maps to the Python good practices of

  1. Use eval/exec sparingly, and
  2. Avoid "magic" code that alters the standard operation of statements.
Collapse
xowap profile image
Rémy 🤖 Author • Edited

Very well spotted, I completely forgot to mention eval(). That's another successor of macros. To be honest I haven't done an eval in so long that I did not even consider it as a threat.