Human Readable JavaScript

Laurie on September 17, 2019

For a long time, humans needed to "speak" like machines in order to communicate with them. And that's still true, we still have the need for people... [Read Full]
markdown guide
 

Love the point free (tacit programming) put in there. I am getting more into functional programming but have to consider my peers and juniors coming in when writing my code... Usually, I ask myself "could I have understood or figured this out when I first finished my coding Bootcamp...? If no I refactor."

 

That's a really good principle to work by.

Refactor only if:

  • It has bugs
  • A junior dev would be stressed out trying to read it
 

I think you should fix it, not refactor if it has bugs.
By definition, refactoring does not change behavior.

True, but when we encounter bugs, don't we ask what caused the bug? Too many moving parts? Unclear data-flow? In the postmortem of a bug-fix, the topic of refactoring doesn't come up?

Well said Ezell, I agree that refactoring during a bugfix rewrite is a good idea for sure!

 

What a great barometer!

Do you think you're losing sight of that level at all? That's always my fear. That I overestimate my past self.

 

Sometimes I do. I generally will have mentees or people I know learning code take look. If they have trouble understanding it ill have them describe why to be sure it's not just a knowledge issue but an over-complicated issue. 😆 So far it seems somewhat successful. I still write my own esoteric code on personal stuff lmao 🤣😅

 

The refactor from

const arr = [1,2,3]

const timesTwo = (el) => el*2

let multipliedByTwo = arr.map((el) => timesTwo(el))

to

const arr = [1,2,3]

const timesTwoPlusIndex = (el, index) => (el*2) + index

let multipliedByTwo = arr.map(timesTwoPlusIndex)

is one of my favourites.

Interestingly (well, interesting to me anyway), it's an example of η-conversion (eta-conversion), one of the three basic reduction steps in the Lambda calculus.

 

The most common pitfall though with this approach is when, for example, converting an array of strings into integers.

["1", "2", "3"].map(parseInt) = [1, NaN, NaN]

["1", "2", "3"].map(x => parseInt(x)) = [1, 2, 3]

The reason for this is that parseInt actually takes 2 arguments (string and radix). Map accepts methods that take up to 3 arguments (value, index, and array). So, when passed to map directly, string is getting value (what we expect!), but index is being passed as the radix. Meaning, you try to parse the 0th element in a natural way (base 10), then you try to parse the 1st element as base one (which it's not a valid base 1 number, so NaN), parse the 2nd element as base two (again, 3 is not valid base 2, so NaN), and so on...

I've been bitten by this bug quite a few times. When using map, bypassing the anonymous function and passing a named function should generally only be used if the function takes a single argument.

 

The default behaviour of Array.map is unintuitive given that it returns the index and the original array as the second and third arguments to the callback function respectively.

I would approach the parseInt problem by writing a map function that takes two args, supplied one at a time (to facilitate partial application).

  • the first arg, a function f that will be supplied only one value at a time, that is, the current iterated value
  • the second arg, a 1-dimensional array of values to apply the function f on

It may appear complicated seeing it for the first time, but come back to the example and mull it over and it will start to click.

/* 
// alternative map implementation
const mapAlt = f => xs => Array.prototype.map.call(xs, currentValue => f (currentValue));


// unterse
function map(f) {
  return function(xs) {
    return xs.map(function(currentValue, index, originalArray) {
      return f(currentValue);
    });
  };
}
*/

const map = f => xs => xs.map(function(currentValue, index, originalArray) {
  return f (currentValue);
});


const xs = [1, 2, 3, 4];
const multiplier = x => x * 6;
const multiplied = map (multiplier) (xs);

const ys = ["1", "2", "3", "4"];
const parser = x => parseInt(x);
const parsed = map (parser) (ys);

console.log("xs:", xs); //=> [1, 2, 3, 4]
console.log("xs multiplied:", multiplied); //=> [6, 12, 18, 24]
console.log("ys:", ys); //=> ["1", "2", "3", "4"]
console.log("ys parsed:", parsed); //=> [1, 2, 3, 4]

But why include index and originalArray parameters when you don't use them?

For that matter, why not make this point-free?

const map = (f) => (xs) => xs .map ((x) => f(x))

(... and around in circles we go!)

That's true, they are superfluous - I left those other args there to make it clearer how the args are moving around.

 

Though it might be a distraction from the example... You could put Number there in place of parseInt.

 

That's a good one! Thanks for pointing it out.

 

Haha it is! I don't think I'd made that connection before.

 

I know! I think it's maybe the only place I've ever found a practical use for lambda calculus!

 

Love this Laurie!

I'm a .NET developer, but always make it my aim to make code read like a story. Things like

var settings = new Settings(this.Configuration)

vs

var settings = Settings.LoadFrom(this.Configuration)

Simple example, but making your code read like a story makes it so much easier to pick up coming back to it.

I think short functions and readable code are fundamentals for code that others can pick up.

 
 

Hi Laurie, great question.

I don't have a 'one-shoe-fits-all' approach to writing the most readable code all the time. At one point I wished for that, but I'm understanding writing code is better off not that way.

The last pattern described particularly useful when the associated callback function for array.map is so long, I forget that it's a callback. In that case I may have something like this:

// Either import it or placed elsewhere in the same file if not used anywhere else
import {crazyLongCallback} from './crazyCallbacks'

const bigDataList = [...]
const parsedDataList = bigDataList.map(crazyLongCallback) 

So I find it useful when discovering different design patterns and ways to handle problems. I tend to gravitate toward the solution in which I hope me I and my colleagues can read after a day, week, or month's time.

Thanks for the read!

 

That's awesome. Do you find yourself staying consistent in the same codebase?

 

Yep, but I just thought of another question. Does the length of a code-block have a big impact on readability? I'd imagine this isn't the case for everyone.

I don't know. Does it? I'd argue that it absolutely does. But you can shorten a code block and make it less readable at the same time. It matters how you do these things.

 

How about splitting things up?

timesTwo = e => e * 2;
add = (a, b) => a + b;

const result = arr
.map(timesTwo)
.map(add) // add index

putting maps on separate lines helps people see it broken in to steps

adding in a comment on the "add" helps because most maps don't usually use index.

you could do .map(addIndex) but I don't like this as the original function can add any two things, not just index

or .map((a, i) => add(a, i)) but that creates another function

 

I think it's important not to simplify the code at a point where you avoid using functionalities which are useful but less readable for a newbie.

For example instead of the following:

    let multipliedByTwo = arr.map(
        (el) => { 
            if(el%2 === 0) {
            return el*2
        } else {
            return el+1
        }
    })

I would prefer using ternary, and no optional syntax characters such as parenthesis or curly braces:

let multipliedByTwo = arr.map(el => el % 2 === 0 ? el * 2 : el + 1)

It may seem less approachable for someone who is not used to ternay, arrow functions and the absence of unnecessary syntax characters, but when used to it, it's actually better to have such a one line operation rather than 8 line of basic if...else.

In other words, I prefer to write a code that requires the maintainer to raise his level of JavaScript understanding but enables code to be shorter, rather than making super simple but verbose JavaScript (I hate to search for bits of code in hundred-lines-long files).

 

Yeah - I wouldn't want to have to figure out what goes wrong in the middle of the night when they call with production issues.

And it is not unnecessary syntax characters except for the computer - for humans, it adds readability and understanding.

And your stuff will break down when the new guy is covering everything while you are on vacation.

 

Interesting. I tend to think of one liners as less readable in most cases. But it’s an interesting perspective!

 

Developers in general agree with you. Most one liners beyond a standard if/else ternary are caught by default linting configs. If someone has to translate the code you write, it's meant for a machine. Given that this already happens once it hits an engine, it is generally not wise to write code in this manner.

“Code is for humans to read and machines to interpret”! Still don’t know who said it first, but it’s a great quote.

 

Very nice article and I love the conclusion "There is no one size fits all syntax". I probably would have written the first now version because I like the "functional programming"-mindset it applies.

For people who struggle with arrow functions I would have included the plain "function" version, too:

const arr = [1,2,3]
let multipliedByTwo = arr.map(function(el) {
    return el*2;
})

I had a pretty hard time in grasping lambdas when they were introduced in Java 8 and it was really irking me that most of the tutorials about this topic did not include a 1:1 non-lambda "translation" :/

 

As it turns out, this is again non-optional if our function logic is more than one line.

I know this isn't at all what you meant, but I thought I'd throw this in the comments for fun.

You can many times write code that would normally need curly braces without them by using parentheses expressions. You can't use statements (like creating new variables or if, else, for, etc), but you can call a few functions and return the last value.

const newArr = myArr.map(x => (console.log(x), changeX(x));

Just a little tidbit I thought I'd throw in. Excellent article nevertheless!

 

Honestly and acording to my experience:
Better less lines and one comentary line than a lot of curly braces and separates functions.
And second rule:
If a function is more than 10 lines, you should do two.
That is my opinion. Like an ass, everybody has one.

 

Everyone can have an opinion, it's good to talk about these things. I don't go by function lines, but by functionality. A function should do one thing. That normally keeps it short.

 

I like to talk about this when people is like you, giving examples. I was trying to make a point about the developers that likes to code in one way and push everybody on that way...only because the guy/girl like it.
Not even with coding reasons, performance or teamworking timing examples on a project.
Maybe now its more clear the point. I hope so. No offense about the ass, its just a funny sentence for me.

 

What a lovely article you've written! Thank you for putting it down in such easy to read and understand text and idea.

I have noticed the same pattern in my coding style over the last 10 years which you describe here, i.e. I truly tend to write for the next human who will read it (which is mostly myself again) later. And again, it might be someone else! Which means somehow this should be like a moral and professional imperative. I. Kant "the programmer" would like that I guess. :-) Or, as Ezell puts it in the comments before, nicely; "A junior dev would be stressed out trying to read it". It is the thing that we are always the junior dev when you meet some new codebase or logic. Isn't it?

I guess it is more up to the "wits" or "IQ" who gets the idea behind some code faster, and on the contrary the common to all of us is "the codebase" and its style. Why not agree on human readable code first then? So from that point it would be lovely if all tend to write human readable code first. Firstly to help ourselves, to be able to even optimize it to next performance level if needed later. A good test for anyone who doubts this is just open any to your grade complex (non-functional language) codebase on github and try figure out the logic in next 5 minutes. You won't in its totality. How ever hard you try there are hidden non-functional style friendly outside mutations and side-effects that you will not grasp from that single e.g. method or function. I am not praising the functional style here as the holy savior, but trying to figure it out a bit e.g. made a better OOP C# developer in terms of sharing my code with others. Again, never take this (or other forum discussion :p) literally as I'd always go with team compromise and policy and in this case with a named function in place of an anonymous one (an that is in cases where the logic spans further then an operation between two operands like a + b).

I always suggest devs I work with where there is smoke in code there will be fire later, let's rewrite or refactor, step by step of course.

 

Really enjoyed reading this. Thanks for your thoughts.

 

excellent article!
brb I'm going to change my prettier config:
"arrowParens": "always"

 

That's some stuff to think about. If you think about books, they are written to a specific audience and their reading level. We should consider who we write code for. I think in most cases that are our colleagues. What level are they at / should they be at? I like Ezell Frazier's metric here in the comments "Would a junior dev be stressed out trying to read it".

In the cases where the code gets complex and can't be any more simplified, you should add comments describing what happens.

 
 

Awesome post! I have to make the subtle point of the performance difference when you use certain approaches though.

let multipliedByTwo = arr.map((el) => timesTwo(el));

vs.

let multipliedByTwo = arr.map((el) => timesTwo);
creates extra unnecessary function around a function. It can be more of a hit when you nest function definitions inside of a callback, but that's a subject for a different post altogether! Great work here!

 

Absolutely true. And given that I was trying to show identical code I struggled over whether to include that one. Thanks for noting it. I likely should have of the original post!

 

WOW, It is a nice and helpful article.

Thank you @Laurie

 
 
 

Loved your examples, makes it very simple to compare and think about the different choices.

 

The book "The Art of Readable Code" g.co/kgs/ZWgHAB is not too thick and easy to read (the opposite would have been a terrible defect for a book on this theme, thinking of it...).
At the beginning I thought "Do I really need to read this?", but I ended up reading it all. Authors are really open-minded (not always the case when people talk about code style). It was even fun at times.

code of conduct - report abuse