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

Add rule to prefer for loops over functional forEach { ... } method #234

Merged
merged 5 commits into from
Aug 8, 2023

Conversation

calda
Copy link
Member

@calda calda commented Aug 1, 2023

Summary

This PR proposes a new rule to prefer using for loops over the functional forEach { ... } method, unless using forEach as the last element in a functional chain.

Autocorrect is implemented in the new forLoop SwiftFormat rule, which was added in nicklockwood/SwiftFormat#1490.

// WRONG
planets.forEach { planet in
  planet.terraform()
}

// WRONG
planets.forEach {
  $0.terraform()
}

// RIGHT
for planet in planets {
  planet.terraform()
}

// ALSO FINE, since forEach is useful when paired with other functional methods in a chain.
planets
  .filter { !$0.isGasGiant }
  .map { PlanetTerraformer(planet: $0) }
  .forEach { $0.terraform() }

Reasoning

For loops are more idiomatic than the forEach method, and are typically familiar to all developers who have experience with C-family languages.

For loops are also more expressive than the forEach method. For loops support the return, continue, and break control flow keywords, while forEach only supports return (which has the same behavior as continue in a for loop).

Please react with 👍/👎 if you agree or disagree with this proposal.

README.md Show resolved Hide resolved
@calda
Copy link
Member Author

calda commented Aug 4, 2023

Adding a link to a comment about forEach that I just found from Ben Cohen (core team member who leads work on the standard library): https://forums.swift.org/t/do-we-want-foreach/56929/2. It's maybe a bit harsh, but it's interesting to see his perspective on this (and that he wishes they could deprecate forEach!).

@bachand
Copy link
Contributor

bachand commented Aug 5, 2023

Adding a link to a comment about forEach that I just found from Ben Cohen (core team member who leads work on the standard library): https://forums.swift.org/t/do-we-want-foreach/56929/2. It's maybe a bit harsh, but it's interesting to see his perspective on this (and that he wishes they could deprecate forEach!).

Thanks for sharing this. It seems like our style guide is aligned with Ben's perspective, which is validating IMHO.

@@ -1542,6 +1542,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
}
```

</details>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIce catch.

README.md Outdated Show resolved Hide resolved
README.md Outdated
Comment on lines 1594 to 1598
// ALSO FINE, since forEach is useful when paired with other functional methods in a chain.
planets
.filter { !$0.isGasGiant }
.map { PlanetTerraformer(planet: $0) }
.forEach { $0.terraform() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asking to learn (and I apologize for the lazy Q, as I know I could try this out...): at what point does the SwiftFormat rule implementation allow forEach(…)? Is it once the expression is multiple lines? I'd love if you want to link me to the code that makes this decision, if that's straightforward to do.

Copy link
Member Author

@calda calda Aug 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default the SwiftFormat rule avoids applying the rule in two cases:

  1. If the subject of the forEach call includes line breaks (handled here with an explicit check). For example, it would be weird to convert:
planets
  .filter({ !$0.isGasGiant })
  .map({ PlanetTerraformer($0) })
  .forEach({ $0.terraform() })

to a for loop formatted something funny-looking like

for planet in planets
  .filter({ !$0.isGasGiant })
  .map({ PlanetTerraformer($0) })
{
  print(planet)
}
  1. If the subject of the forEach call uses trailing closure syntax, like planets.filter { !$0.isGasGiant }.forEach { print($0) } (handled here by virtue of } not being handled in one of the switch cases -- should realistically be called out more explicitly).

We exclude this case because converting this to a for loop causes a new warning to be emitted:

// warning: trailing closure in this context is confusable with the body of the statement; pass as a parenthesized argument to silence this warning
for planet in planets.filter { !$0.isGasGiant } {
  print(planet)
}

A typical use case of chained functional operations hits both of these rules.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the subject of the forEach call uses trailing closure syntax, like planets.filter { !$0.isGasGiant }.forEach { print($0) } (handled here by virtue of } not being handled in one of the switch cases -- should realistically be called out more explicitly).

I improved how this is handled in the code here: nicklockwood/SwiftFormat@4d92ba2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for explaining @calda and for that follow up!

@@ -1562,6 +1564,42 @@ _You can enable the following settings in Xcode by running [this script](resourc
planets.first { $0.isGasGiant }
```

</details>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
calda and others added 2 commits August 4, 2023 21:20
Co-authored-by: Michael Bachand <michael.bachand@airbnb.com>
@swiftal64
Copy link
Contributor

swiftal64 commented Aug 8, 2023

I would prefer to give the developer the option of choosing the best tool for the job.

For example, occasionally I group things in logical chunks, and one-liners can sometimes help with organization:

let stars = StarFactory.generate(1)
let planets = PlanetFactor.generate(8)
let moons = MoonFactory.generate(290)

view.addSubview(stars)
view.addSubview(planets)
view.addSubview(moons)

stars.forEach { $0.orbit() }
planets.forEach { $0.orbit() }
moons.forEach { $0.orbit() }

@calda
Copy link
Member Author

calda commented Aug 8, 2023

@swiftal64, perhaps for loops can still be used in this way:

for star in stars { star.orbit() }
for planet in planets { planet.orbit() }
for moon in moons { moon.orbit() }

This sort of usage would still be consistent with the style guide.

@calda calda enabled auto-merge (squash) August 8, 2023 15:39
@calda calda merged commit b408d36 into master Aug 8, 2023
3 checks passed
@calda calda deleted the cal--forLoop branch August 8, 2023 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants