RFC: proposed linting rules for arrow functions
See original GitHub issueProposal:
We’re currently running ESLint against metaphysics on every commit, where commits will fail if the lint finds errors it’s unable to resolve.
We have the option to specify whether or not we want our linter to have opinions about how arrow functions should be written. There are two independently-tracked variables to consider:
arrow-parens
allows us to specify how we feel about the optional parens around an arrow function’s arguments when there is exactly one argument. The supported values are"always"
and"as-needed"
and their usage is documented in the link.arrow-body-style
allows us to specify whether we want to wrap the logic of arrow functions in braces or not. Supported values are"always"
,"as-needed"
and"never"
.
Currently, as of this PR which updates ESLint and the AirBNB rules to their latest versions and adds linting around promises, we inherit the AirBNB rules for arrow functions for arrow-parens
.
But arrow-body-style
is more complicated, and is the subject of this RFC. Before my PR, we were using a version of AirBNB rules that had no opinion about arrow functions. I had to upgrade ESLint to support a promise plugin, which required me to upgrade AirBNB while I was at it. When I did that, we got all of AirBNB’s updated rules, which included as-needed
for arrow-body-style
.
Unfortunately, @damassi pointed out that this turned some previously braced functions with explicit- or no-return into braceless functions with implicit returns. So I overrode arrow-body-style
to use always
, instead, but as @damassi points out to me this isn’t the same thing as going back to how things were.
My proposal is that we fall back to AirBNB’s default around arrow-body-style
, which is to enforce it “as-needed” - this means that any arrow function whose body contains only a single expression will lose the curly braces and the explicit return.
This is a change from how things were, where sometimes arrow functions used an implicit return and sometimes they didn’t, and that decision was up to the individual programmer rather than an algorithm. I feel - and I argue below - that using the default AirBNB rule makes for more readable and more easily maintainable code.
Reasoning
Arrow functions with no braces and an implicit return value provide immediate visual feedback that the function in question is ‘merely’ a map from some input to some output. The absence of braces suggests an absence of internal functional state - we have a single expression and it gets returned, and we know at a glance that there’s nothing else happening in this function.
This is incredibly powerful for apprehending a large codebase at a glance, but it also makes normal code a lot more readable. Consider the difference between:
const current = [1, 2, 3].map(item => { return item * 2 })
const proposed = [1, 2, 3].map(item => item *2)
The minute your eyes see those braces on current
they have to enter the scope that the braces define. Sure, it’s just a return, and quickly looking at it makes it obvious that there isn’t anything else in the braces that you have to be aware of.
Contrast that to proposed
where the string item => item*2
is itself a pure functional relation with zero moving parts. You don’t have to look ‘into’ the block because there is no block.
This kind of at-a-glance scanning works when you have large blocks of code as well - just consider this example from our source code:
const User = {
type: UserType,
name: "User",
args: {
email: {
type: GraphQLString,
description: "Email to search for user by",
},
},
resolve: (root, option, request, { rootValue: { userLoader } }) => {
return userLoader(option)
.then(result => {
return result
})
.catch(err => {
if (err.statusCode === 404) {
return false
}
})
},
}
Look at that resolve function on the User schema - it’s returning a single expression, but that’s not immediately obvious. We’re looking at a wall of text and a bunch of parens and brackets, and if we don’t notice that return
keyword in all of that noise we may think that this function body is doing something more complex than merely returning a single expression.
I’d argue that reframing it like this reduces cognitive overhead and makes the code easier to read:
const User = {
type: UserType,
name: "User",
args: {
email: {
type: GraphQLString,
description: "Email to search for user by",
},
},
resolve: (root, option, request, { rootValue: { userLoader } }) => userLoader(option)
.then(result => result)
.catch(err => {
if (err.statusCode === 404) {
return false
}
})
}
The signal the eye receives when presented with => {
is “this function has a body”. The signal it receives when presented with => anythingElse
is “this function returns a pure expression”. This distinction is meaningful and helpful.
Exceptions:
Not all functions whose body consists of a single expression actually care about returning that value. When a function’s body is represented by a single expression valuable for its side-effects the fact that it becomes an implicit return is semantically tricky. In those cases we don’t care about the return value, and seeing => something
tricks us into thinking that we care about something
rather than the side effects of something
’s invocation.
There are two places where we might find this:
- In tests we often have a signature that looks something like this:
it ('asserts something', () => {
assert(foo)
});
If we apply my proposed rule changes to our tests then we get this, which while technically valid and isomorphic to the above still feels really wrong:
it('asserts something', () => assert(foo))
My solution here is simple: we don’t apply our lint rules to our tests.
- The other place, though, is in our code. Sometimes we write functions that we just invoke for their side effects, and sometimes those functions only have a single expression inside. I don’t have as-good an answer here, except to say that this is a choice. I’d rather implicitly return a call to
console.log
than need explicit braces andreturn
statements in my map functions, right?
Additional Context:
Ultimately I don’t think there’s a ‘right’ answer here. Both sides have merit and it’s a matter of personal preference. I suspect this will boil down to which personal preference is better represented here at Artsy, and that’s fine! 😃
Issue Analytics
- State:
- Created 5 years ago
- Reactions:1
- Comments:14 (14 by maintainers)
Top GitHub Comments
There was a misunderstanding in the framing of this issue. When I wrote this I was under the impression that our choice was between a draconian enforcement of brackets or a draconian enforcement of their absence, and I wanted to advocate for their absence.
I see now though that I had misunderstood the initial state of things. Not having a rule makes the most sense, so I’m going to retract this issue. If @ashfurrow or @anandaroop, who voted for it, think it’s worth bringing back up then let’s do a separate RFC that frames it more accurately.
Thanks all!
I’m 👍 for it, but I think we should apply it to our tests, too.
it('asserts something', () => assert(foo))
is a little weird, but not weird enough to give up linting our tests imo.