Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow for-of #1122

Closed
aboyton opened this issue Oct 13, 2016 · 122 comments
Closed

Disallow for-of #1122

aboyton opened this issue Oct 13, 2016 · 122 comments

Comments

@aboyton
Copy link

aboyton commented Oct 13, 2016

The style guide 11.1 says that you shouldn't use for-of and instead should use forEach (which I completely agree with).

That said, the ESLint rule no-iterator doesn't seem to enforce this, and the entry for no-restricted-syntax contains ForInStatement but not ForOfStatement.

Can we add ForOfStatement to no-restricted-syntax (or is the a better way of restricting for-of statements?

@ljharb
Copy link
Collaborator

ljharb commented Oct 13, 2016

Absolutely, thanks! Want to make a PR? it will be semver-major, so i won't be able to merge it immediately, but having it queued up would be great.

@aboyton
Copy link
Author

aboyton commented Oct 17, 2016

Looks like I was beat to the punch by mere hours. Thanks @SimenB.

@dplusic
Copy link

dplusic commented Nov 11, 2016

What should I do in this case:

async function foo(args) {
  for (const arg of args) {
    await bar(arg);
  }
}

@ljharb
Copy link
Collaborator

ljharb commented Nov 11, 2016

@dplusic in nice, simple ES6:

function foo(args) {
  return args.reduce((prev, arg) => prev.then(() => bar(arg), Promise.resolve());
}

@dplusic
Copy link

dplusic commented Nov 11, 2016

It works. Thanks!

@leebenson
Copy link

@ljharb - what about generators?

I have a function like this that flattens objects recursively, so { user { name: "Lee", location: { city: 'London' } } } becomes {'user.name': 'Lee', 'user.location.city': 'London'}:

function* flatten(obj, str = '') {
  if (obj) {
    for (const name of Object.getOwnPropertyNames(obj)) {
      if (!Array.isArray(obj) && typeof obj[name] === 'object') {
        yield* flatten(obj[name], str ? `${str}.${name}` : name);
      } else {
        yield {
          [`${str}.${name}`]: obj[name],
        };
      }
    }
  }
}

Which is turned into a 'flat' object with Object.assign({}, ...flatten(obj));

I'm probably overlooking an obvious equivalent with map, but a generator solves this quite neatly...

@ljharb
Copy link
Collaborator

ljharb commented Nov 18, 2016

@leebenson generators are not allowed in our guide.

As for an alternative, I'm not sure if this actually works, but perhaps something like:

function flattenObject(obj, prefix = '') {
  return Object.entries(obj).reduce((acc, [key, val]) => {
    const prefixedKey = prefix ? `${prefix}.${key}` : key;
    if (val && !Array.isArray(val) && typeof val === 'object') {
      return Object.assign(acc, flattenObject(val, prefixedKey));
    }
    return Object.assign(acc, { [prefixedKey]: val });
  }, {});
}

@leebenson
Copy link

Thanks @ljharb. This minor tweak works perfectly:

function flattenObject(obj, prefix = '') {
  return Object.entries(obj).reduce((acc, [key, val]) => {
    const prefixedKey = prefix ? `${prefix}.${key}` : key;
    if (val && !Array.isArray(val) && typeof val === 'object') {
      return Object.assign(acc, flattenObject(val, prefixedKey));
    }
    return Object.assign(acc, { [prefixedKey]: val });
  }, {});
}

@minexew
Copy link

minexew commented Dec 5, 2016

@dplusic @ljharb
I don't understand. Where's the advantage in making your code totally unreadable?
Purity is nice, but not when it takes 30 minutes to decipher a simple function.

@ljharb
Copy link
Collaborator

ljharb commented Dec 5, 2016

Of course readability is subjective, but I find loops far more unreadable than a functional alternative.

@minexew
Copy link

minexew commented Dec 5, 2016

That's interesting. Of course I don't have any empirical evidence for this, but I bet it'd take the average programer an order of magnitude more time to understand your snippet vs what @dplusic posted.

I hope this fad of functional-at-all-cost is over soon. There's a reason why Haskell hasn't gone mainstream.

@leebenson
Copy link

leebenson commented Dec 5, 2016

I do think @minexew has a point.

With native async/await landing in V8, I think it's going to be pretty common pattern to 'unwind' functional logic like this. for...of and generators often make the code more readable, and easier to follow. I'm not sure effectively banning swaths of valid JS is the way to go.

Personal preference, of course.

@flying-sheep
Copy link

i’d actually argue that for..of should be used because of functional style:

.forEach() isn’t functional. it exists for its side effects. so if anything, .forEach() should be banned and for..of allowed in order to make clear that side effects are happening here (which isn’t as apparent from a filtermapforEach pipeline)

@ljharb
Copy link
Collaborator

ljharb commented Dec 6, 2016

@flying-sheep it's a functional evolutionary step to map and friends. for..of does not lend itself towards evolving to be a proper map, filter, etc. In addition, .forEach forces side effects (or else nothing really happens) - for..of can also be used not for side effects.

@flying-sheep
Copy link

behold:

function* map(callable) {
	for (const element of this) {
		yield callable(element)
	}
}
function* filter(predicate) {
	for (const element of this) {
		if (predicate(element)) yield element
	}
}

const odd = '12345'
	::map(parseFloat)
	::filter(e => e % 2)

In addition, .forEach forces side effects (or else nothing really happens) - for..of can also be used not for side effects.

both only have side effects (if anything, .forEach returns undefined). what do you mean with “not for side effects”

@ljharb
Copy link
Collaborator

ljharb commented Dec 7, 2016

@flying-sheep lol fair point, both are for side effects since for..of isn't an expression. forget that part.

The :: operator is stage 1, and Airbnb only recommends using stage 3 proposals and above. Similarly, transpiling generators requires regenerator-runtime, which is way too heavyweight to ship to production, so our config also forbids generators at this time.

@SimenB
Copy link
Contributor

SimenB commented Dec 7, 2016

Similarly, transpiling generators requires regenerator-runtime, which is way too heavyweight to ship to production, so our config also forbids generators at this time.

Kinda tangentially related: would you be interested in a config for server-side only projects? I've used airbnb-base at it works fairly fine, but e.g. trailing commas in functions has to be disabled, and generators could be allowed, etc. There's not a lot of rules that needs tweaking, but maybe you could also include https://github.com/mysticatea/eslint-plugin-node.

If you're interested at all, I can create a new issue for it to discuss its merits there? 😄

@ljharb
Copy link
Collaborator

ljharb commented Dec 7, 2016

Server-side only projects should be using babel also.

@SimenB
Copy link
Contributor

SimenB commented Dec 7, 2016

Why? With async/await landing, the only syntax I care about is object spread, and it's OK to use Object.assign. Waiting for trailing function commas until support lands isn't an issue either. Adding the overhead of a build step seems unnecessary.

@ljharb
Copy link
Collaborator

ljharb commented Dec 7, 2016

For one, old syntax is generally faster than new syntax, since it's had time to be optimized. Additionally, there'll always be new syntax you want to use, and if your build process is already in place, it's trivial to add that.

A build step shouldn't add overhead, since you should be running your linter and your tests prior to code going to production anyways - just add the build step into there.

@flying-sheep
Copy link

flying-sheep commented Dec 7, 2016

@ljharb maybe i didn’t understand you: you argued that it’s an evolutionary step towards functional array methods. i showed that this isn’t the case. my use of generators is irrelevant. e.g. here’s a map implementation without anything you mentioned:

Array.prototype.map = function map(callback) {
	const mapped = []
	for (const element of this) {
		mapped.push(callback(element))
	}
	return mapped
}

@ljharb
Copy link
Collaborator

ljharb commented Dec 7, 2016

The given implementation of map - or any other abstraction - is entirely irrelevant. forEach is an array prototype method, just like the functional array prototype methods.

@leebenson
Copy link

I use my babel-preset-node7 for server side use of the airbnb preset. It adds the (very few) missing pieces -- trailing function commas, etc -- and avoids transpiling native features like generators, async/await, etc.

The extra build step makes sense in my case, because I'm writing universal code that targets both the server and the browser, so a build step is necessary anyway. I use generators, async/await, etc freely, and provide a different babel config to each target in webpack. In my use case, adding browser polyfills/runtimes has a negligible effect on the bundle size. My total app is less than 200kb gzipped, with React, Mobx, es2015 polyfills, all app code, etc.

For the most part, I find the airbnb preset to match my own personal stylistic preferences, but I do agree that eliminating for...of is a step too far. I find generators to be very useful for lazy evaluation and for..of (especially coupled with generators and await) makes the code path very easy for others in my team to follow.

In my case, adding a custom rule for no-restricted-syntax fixes my one irritation.

@flying-sheep
Copy link

flying-sheep commented Dec 7, 2016

yeah, for..of and generators can be used e.g. for efficient parsers (you delegate to sub parsers via yield*). some languages run on those features extensively, e.g. python and rust.

JS is always the slow kid on the block. maybe in a year or so, we’ll see “map considered harmful” posts that praise how much awesomer generators and comprehensions are.

@ljharb
Copy link
Collaborator

ljharb commented Dec 7, 2016

Comprehensions are never coming to JS, so I doubt you'll ever see that post :-)

linaGirl pushed a commit to joinbox/eslint-config-joinbox that referenced this issue Sep 6, 2018
I can't figure what's not OK with them in some cases (like when working on async code). Airbnbs argumentation is about immutability. Mine is about readability and ease of use. I like iterators, they are great! See https://github.com/airbnb/javascript#iterators-and-generators and airbnb/javascript#1122 (comment)
@fleksin
Copy link

fleksin commented Oct 25, 2018

Just found this thread. Hope I'm not too late to ask questions.
Say I have a function

someFunc = (array) => {
    for (const item of array) {
       if (someCondition) return false;
    }
    return true;
}

What's the equivalent solution if not using for-of loop ?

@ljharb
Copy link
Collaborator

ljharb commented Oct 25, 2018

@fleksin
Assuming someCondition can better be represented as somePredicate(item), the existing logic gets true if "every item produces false", so:

someFunc = array => array.every(item => !somePredicate(item));

Alternatively, if "any item produces true":

someFunc = array => array.some(item => somePredicate(item));

but you'll want to confirm that the behavior matches your expectations for an empty array.

@maple3142
Copy link

@hmagdy No, they are not same.
The former executes the promises sequentially, while the latter executes them at them same time.

@ljharb
Copy link
Collaborator

ljharb commented Mar 8, 2019

Indeed, you'd need the (not weird - a normal part of JS and functional programming) reduce instead.

@flying-sheep
Copy link

flying-sheep commented Mar 9, 2019

The former executes the promises sequentially, while the latter executes them at them same time.

The former actually results in a “SyntaxError: await is only valid in async function” as expected.

The arrow function isn’t async, and await needs to be in an async function directly. If we make it async, you’re of course right, but that’s not a surprise: Just calling async functions (as forEach does) runs them parallely.


I don’t think this discussion is helpful anymore. Everything’s been said on why the rule is there:

  • The AirBnB JS style is tailored for AirBnB’s use
  • AirBnB targets older browsers without iterator support: Disallow for-of #1122 (comment)
  • The AirBnB guide recommends forEach because for..of relies on iterators, and polyfill support for old browsers here is slow.
  • It’s easy to create your own derived style guide that is tailored to your preferences.

I think the only thing left to discuss is what I said in #1122 (comment), and I filed #2019 for that.

@ljharb
Copy link
Collaborator

ljharb commented Mar 9, 2019

To clarify; we recommend forEach because loops should basically always be avoided.

@flying-sheep
Copy link

People don’t agree with you here, see the discussion in #2019

@MelMacaluso

This comment has been minimized.

@ljharb

This comment has been minimized.

@Arcanorum

This comment has been minimized.

@Ghost---Shadow
Copy link

I want to process an iterator. Is there a functional way of doing this?

let result = iterator.next();
  while (!result.done) {
    console.log(result.value);
    result = iterator.next();
  }

@ljharb
Copy link
Collaborator

ljharb commented Apr 10, 2020

@Ghost---Shadow Array.from(iterator, item => process(item))? There's also https://npmjs.com/iterate-value / https://npmjs.com/iterate-iterator if that's useful for you.

@jakobrosenberg
Copy link

I'm much faster and far less prone to errors if I can understand the code I'm looking at.

All things equal, readability and clarity should be first priority.

for (const person of queue)
  await person.ready()
queue.reduce((prev, person) => prev.then(() => person.ready(), Promise.resolve());

How fast can you decipher what the function does and spot the error?


Furthermore, how would you handle this logic in a readable way?

for (const person of queue){
    await person.isReady()
    queue.push(...person.expectedGuests)
}

or this

for (const [i, person] of queue.entries()){
    await person.isReady()
    queue.push(...person.expectedGuests)
    queue.splice(i, person.accompanyingGuests.length)
}

And what if we decide to add a timer that bumps people to the back if they're too slow? I can add extra logic while keeping things readable and easy to reason about.

@ljharb
Copy link
Collaborator

ljharb commented May 19, 2021

@jakobrosenberg quite fast, but obv that's subjective. However, it's pretty easy to make an abstraction around that reduce like serialize(queue, (person) => person.ready()), which is much more readable since it explicitly declares what it's for.

Same feedback on the others - using a loop requires someone to infer your intention, using a named abstraction doesn't.

@jakobrosenberg
Copy link

using a loop requires someone to infer your intention, using a named abstraction doesn't.

You can abstract a for .. of loop into a named function and then you're even on intention.

As Capone said: You can get much further with clear intentions and readable code than you can with clear intentions alone.

@ljharb
Copy link
Collaborator

ljharb commented May 20, 2021

@jakobrosenberg that's totally true! and if you do that, and you don't transpile such that you'd include regenerator-runtime when using for..of, then you can use an eslint override comment inside that abstraction. The goal is to make most of your code clean - there's nothing wrong with using overrides to paper over well-hidden warts that live inside clear abstractions.

@DominicTobias-b1
Copy link

DominicTobias-b1 commented May 20, 2021

for..of is supported in all browsers except IE without compilation so I doubt there's much if any perf gain in other browsers by not using it (untested). I think it's much easier to understand than reduce, in fact I find for loops are almost always easier to understand than something recursive (and sometimes much more efficient on the stack if the VM doesn't support tail recursion optimisation).

Also if the accumulator is an array or object you might tend to spread the next item in, which is vastly less efficient than writing some more lines of code and mutating accumulator. At that point you haven't really saved any code compared to a loop, just written more opaque and slow code because someone said it's more "FP". So I use reduce if I'm actually reducing stuff or it's really cleaner, in this case I agree with @jakobrosenberg that his code is a lot easier to understand, which is much like try/catch/await being easier to understand than Promise then/catch 🤷‍♂️

@ljharb
Copy link
Collaborator

ljharb commented May 20, 2021

@DominicTobias-b1 in the latest version of all browsers. Despite marketing, no browsers are "evergreen" according to the google analytics of major websites I've been able to review over the last couple years. (Nothing but safari will likely ever support PTC - which is not an optimization - so that's not really relevant to discuss)

Performance isn't important, readability is.

@billnbell

This comment has been minimized.

@ljharb

This comment has been minimized.

@billnbell

This comment has been minimized.

@ljharb

This comment has been minimized.

sodatea added a commit to vuejs/eslint-config-airbnb that referenced this issue Aug 13, 2022
The original intention was to disallow usage of iterators.
airbnb/javascript#1122

Of course we can improve the error message a little bit, but I don't
think it's worth the extra effort for now.
goldentroll added a commit to goldentroll/eslint-config-airbnb that referenced this issue Mar 14, 2023
The original intention was to disallow usage of iterators.
airbnb/javascript#1122

Of course we can improve the error message a little bit, but I don't
think it's worth the extra effort for now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests