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 who work in assembly and other low-level languages. But for many of us, these complexities are abstracted away. Our job, is to focus on what is readable for humans and let the machines interpret our code.
This consideration is never more apparent than a situation in which identical code can be written in numerous ways. So today, I want to talk less about how something works, and more about how it reads. There is another post in here somewhere about functional JavaScript, but let's assume we're talking about map
.
map
is a function available for arrays in JavaScript. Think of it as for each
. It takes a function as an argument and runs each element in the array through that function. The difference is that it doesn't alter the original array at all. The result is a new array.
Example
const arr = [1,2,3]
let multipliedByTwo = arr.map(el => el*2)
// multipledByTwo is [2,4,6]
Ok, so we know what map does. But look at the code snippet above. An incredibly terse function that multiplies a variable by two.
So let's take a look at all the different ways we could write that same logic.
Optional Parentheses
The first optional addition we can make is to add parentheses to the parameter definition of the internal function. This makes that piece of code start to look more like a typical function definition.
const arr = [1,2,3]
let multipliedByTwo = arr.map((el) => el*2)
What's interesting about this is that the only reason we don't need them is because we're only passing one argument.
const arr = [1,2,3]
let multipliedByTwo = arr.map((el, index) => el*2)
In cases where we pass more than one argument, the parens are not optional. Our example is map
, if it were reduce
we would always use the parentheses.
So let's take stock for a moment. Do we lose anything by adding the parentheses? Do we gain anything? We're adding two characters, what information does that convey? These are the things we need to ask ourselves as we develop code for our teammates and future selves to maintain and read.
Curly braces and return
We can go a step further with making that internal function adhere to official function syntax. Doing so requires curly braces and the return
keyword.
const arr = [1,2,3]
let multipliedByTwo = arr.map((el) => { return el*2})
How do we feel about this code now? It certainly reads more clearly as a function. Do the braces and return
add more bulk? Does our view of this change depending on the logic being returned?
As it turns out, this is again non-optional if our function logic is more than one line.
const arr = [1,2,3]
let multipliedByTwo = arr.map(
(el) => {
if(el%2 === 0) {
return el*2
} else {
return el+1
}
})
Interesting. Does our opinion of the extra characters change based on the use case? What does that mean for consistency throughout our code?
Use a separate function
As we know and have seen, map
takes a function as an argument and passes each element in our array into it. Perhaps we could, or should, define our internal logic outside of the map
. As it stands, it looks a bit like pyramid code.
const arr = [1,2,3]
const timesTwo = (el) => el*2
let multipliedByTwo = arr.map((el) => timesTwo(el))
What do we think? Realistically it's almost the same number of characters as the original version. But what about our example from above with more complex logic?
const arr = [1,2,3]
const timesTwoOrPlusOne = (el) => {
if(el%2 === 0) {
return el*2
} else {
return el+1
}
}
let multipliedByTwo = arr.map((el) => timesTwoOrPlusOne(el))
Did this change your view? Or does it look cluttered and repetitive?
Just a function
Functional programming is an interesting paradigm. In part because of the way it allows us to write code. Again we're reminded that map
takes a function as an argument. So why not give it a function.
const arr = [1,2,3]
const timesTwo = (el) => el*2
let multipliedByTwo = arr.map(timesTwo)
Yes, this is valid. map
knows to pass the element it gets to the function and use the result. We can get even more in the weeds by determining what form our timesTwo
function could take. Right now it's a terse one-liner.
And note that map
is really smart. We can pass the same function even if that function now uses both the element and the index to arrive at a return value!
const arr = [1,2,3]
const timesTwoPlusIndex = (el, index) => (el*2) + index
let multipliedByTwo = arr.map(timesTwoPlusIndex)
Does this seem readable? multipledByTwo
is certainly pleasant to read, but where is timesTwoPlusIndex
located in our codebase? Is it hard to track down? If someone is looking at this for the first time do they know it's a function? Or do they assume it's an object or array variable?
Functions are objects in JavaScript, but ignore that duplication for the moment.
How do we determine what is readable
There is no one size fits all syntax. Who is your audience? Polyglots or JavaScript experts? Who is maintaining your code? How many people work in this codebase? All of these things matter.
It entirely depends on the use case, and consistency is important. However, seeing all the different representations of the same functionality is eye-opening. All of these examples will be built into the same minified code. So the decision for us, as developers, is based on human readability. It's completely absent of machine performance and functionality considerations.
I've posed a lot of questions and not a lot of answers. I have my own opinions but would love to hear yours. Which of these are the most readable? Are there versions you prefer to write? Let's discuss it below!
Latest comments (46)
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.
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.
Loved your examples, makes it very simple to compare and think about the different choices.
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:
I would prefer using ternary, and no optional syntax characters such as parenthesis or curly braces:
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.
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.
my team decided to adopt typescript. Bye-bye readability 😭
Exactly! Even simple TypeScript can take some pause.
ES6 ♥
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 indexor
.map((a, i) => add(a, i))
but that creates another functionWhat 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.
functional programming variant