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!