Review for: Remove C-style for-loops with conditions and incrementers


(Dmitri Gribenko) #1

Hi,

My biggest concern with the proposal is the lack of data regarding
equivalents for existing C-style for loops. Note that I'm not talking
about code that uses C-style for loops in cases where a superior
construct exists in Swift, for example, `for i in myArray.indices`.
I'm interested in seeing cases not covered by that.

For example, it would be good if someone took a look at "git grep 'for
var' stdlib/" and submitted a PR that converts all those loops. If
that results in readability improvements, it would be a great PR
regardless of the decision on this proposal.

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

Dmitri

···

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Dmitri Gribenko) #2

Thank you for the investigation, Nate!

···

On Mon, Dec 7, 2015 at 2:03 PM, Nate Cook <natecook@gmail.com> wrote:

It looks like the standard library has between ten and twenty "for var" instances (depends on if you count before or after de-gybbing), that fall into four categories:

1) for var i = 0; i < length; i++ { ...

These are the most prevalent (and would be everywhere else), and are easily replaced by "for i in 0 ..< length" or "for i in x.indices".

2) for var i = length; --i >= 0; { ...

This is a "count-down" loop, and isn't handled that well in Swift. The "guessing" translation is both wrong and fails to compile ("for i in length ... 0"), another attempt compiles but is easy to get wrong:

for length.stride(to: 0, by: -1) { // wrong, includes length but not zero
for length.stride(through: 0, by: -1) { // wrong, still includes length
for (length - 1).stride(through: 0, by: -1) { // works, but people have switched back to Java by now

Probably the best practice is to use reverse() on the range, since then you'd be using the same method on a "range literal", the indices range, or a collection:

for (0 ..< length).reverse() { ...

3) for var i = 0; i != n && p != limit; i++ { ...

This style *looks* like #1 but hides a second condition -- it's very easy for a newcomer to come to code with this construct and miss second part of the termination case. Far better to refactor this so the second condition is explicit:

for i in 0 ..< n {
  guard p != limit else { break }
  ...
}

4) for var x = foo(y); x.isNotFinished(); x = foo(x) { ...

These can be refactored into while loops without much trouble. Your concern below about continue statements is well-founded. What we might call a "while-defer" loop solves that (though I'm not crazy about the construct):

var x = foo(y)
while x.isNotFinished() {
  defer { x = foo(x) }
  ...
}

That method has the added benefit of putting the test and the "increment" right in one place at the top of the loop. There's a slight difference in that the deferred increment gets executed after a "break", unlike the last statement of a "for var ; ;" loop.

Nate

On Dec 7, 2015, at 3:07 PM, Dmitri Gribenko via swift-evolution <swift-evolution@swift.org> wrote:

Hi,

My biggest concern with the proposal is the lack of data regarding
equivalents for existing C-style for loops. Note that I'm not talking
about code that uses C-style for loops in cases where a superior
construct exists in Swift, for example, `for i in myArray.indices`.
I'm interested in seeing cases not covered by that.

For example, it would be good if someone took a look at "git grep 'for
var' stdlib/" and submitted a PR that converts all those loops. If
that results in readability improvements, it would be a great PR
regardless of the decision on this proposal.

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

Dmitri

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Nate Cook) #3

It looks like the standard library has between ten and twenty "for var" instances (depends on if you count before or after de-gybbing), that fall into four categories:

1) for var i = 0; i < length; i++ { ...

These are the most prevalent (and would be everywhere else), and are easily replaced by "for i in 0 ..< length" or "for i in x.indices".

2) for var i = length; --i >= 0; { ...

This is a "count-down" loop, and isn't handled that well in Swift. The "guessing" translation is both wrong and fails to compile ("for i in length ... 0"), another attempt compiles but is easy to get wrong:

for length.stride(to: 0, by: -1) { // wrong, includes length but not zero
for length.stride(through: 0, by: -1) { // wrong, still includes length
for (length - 1).stride(through: 0, by: -1) { // works, but people have switched back to Java by now

Probably the best practice is to use reverse() on the range, since then you'd be using the same method on a "range literal", the indices range, or a collection:

for (0 ..< length).reverse() { ...

3) for var i = 0; i != n && p != limit; i++ { ...

This style *looks* like #1 but hides a second condition -- it's very easy for a newcomer to come to code with this construct and miss second part of the termination case. Far better to refactor this so the second condition is explicit:

for i in 0 ..< n {
guard p != limit else { break }
...
}

4) for var x = foo(y); x.isNotFinished(); x = foo(x) { ...

These can be refactored into while loops without much trouble. Your concern below about continue statements is well-founded. What we might call a "while-defer" loop solves that (though I'm not crazy about the construct):

var x = foo(y)
while x.isNotFinished() {
defer { x = foo(x) }
...
}

That method has the added benefit of putting the test and the "increment" right in one place at the top of the loop. There's a slight difference in that the deferred increment gets executed after a "break", unlike the last statement of a "for var ; ;" loop.

Nate

···

On Dec 7, 2015, at 3:07 PM, Dmitri Gribenko via swift-evolution <swift-evolution@swift.org> wrote:

Hi,

My biggest concern with the proposal is the lack of data regarding
equivalents for existing C-style for loops. Note that I'm not talking
about code that uses C-style for loops in cases where a superior
construct exists in Swift, for example, `for i in myArray.indices`.
I'm interested in seeing cases not covered by that.

For example, it would be good if someone took a look at "git grep 'for
var' stdlib/" and submitted a PR that converts all those loops. If
that results in readability improvements, it would be a great PR
regardless of the decision on this proposal.

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

Dmitri

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Lily Ballard) #4

As has been pointed out in other threads on this topic, you can use a
defer statement to avoid duplicating the increment portion:

var i = initial
while cond(i) {
    defer { i = increment(i) }
    // ...
    if frob {
        continue // defer will execute
    }
}

-Kevin

···

On Mon, Dec 7, 2015, at 01:07 PM, Dmitri Gribenko via swift-evolution wrote:

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.


(Jacob Bandes-Storch) #5

> 2) for var i = length; --i >= 0; { ...
>
> This is a "count-down" loop, and isn't handled that well in Swift. The
"guessing" translation is both wrong and fails to compile ("for i in length
... 0"), another attempt compiles but is easy to get wrong:
>
> for length.stride(to: 0, by: -1) { // wrong, includes length but not zero
> for length.stride(through: 0, by: -1) { // wrong, still includes length
> for (length - 1).stride(through: 0, by: -1) { // works, but people have
switched back to Java by now
>
> Probably the best practice is to use reverse() on the range, since then
you'd be using the same method on a "range literal", the indices range, or
a collection:
>
> for (0 ..< length).reverse() { ...
>

Have there been any ideas for addressing this in the standard library?
Perhaps a modification to Range to allow reverse intervals, or perhaps a
ReverseRange type, and a new operator ">.." which would mean the reverse of
"..<" ?

It seems "for i in ReverseCollection(a..<b)" currently works, though it
looks a little weird. Of course Nate's example "(a..<b).reverse()" works
too, but it's far too many parentheses for my taste.


(Michel Fortin) #6

Which is a dangerous substitute, because it also does the wrong thing with break and throw:

  var i = initial
  while cond(i) {
    defer { i = increment(i) }
    // ...
    if frob {
      continue // defer will execute
    } else if blob {
      break // defer will execute!!!
    } else if plob {
      throw MyError // defer will execute!!!
    }
  }

···

Le 7 déc. 2015 à 20:29, Kevin Ballard via swift-evolution <swift-evolution@swift.org> a écrit :

On Mon, Dec 7, 2015, at 01:07 PM, Dmitri Gribenko via swift-evolution > wrote:

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

As has been pointed out in other threads on this topic, you can use a
defer statement to avoid duplicating the increment portion:

var i = initial
while cond(i) {
   defer { i = increment(i) }
   // ...
   if frob {
       continue // defer will execute
   }
}

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(Greg Parker) #7

var i = initial
while cond(i) {
   defer { i = increment(i) }

Strange. Can the compiler optimizer handle that efficiently?

Michel Fortin wrote:

var i = 0
var first = true
while ({ () -> Bool in if first { first = false } else { i = increment(i) }; return cond(i) }()) {
}

I fear that teaching the C-style for loop is easier than teaching that.

The C-style for loop has the great advantage that it can express a large number of loop constructs concisely. Most or all of the loop logic is collected in one place. It is also familiar to many developers via other languages.

Proposal:
1. Keep the C-style for loop.
2. Discourage use of the C-style for loop in simple cases with a compiler warning and fix-it to a for..in loop. This would be analogous to the existing warning of "variable was never mutated; consider changing to 'let' constant".

···

On Dec 7, 2015, at 5:29 PM, Kevin Ballard via swift-evolution <swift-evolution@swift.org> wrote:

--
Greg Parker gparker@apple.com Runtime Wrangler


(Joe Groff) #8

A more accurate (but not particularly pretty) substitution would be to perform the increment as part of the condition:

while { i = increment(i); return cond(i) }() {
}

-Joe

···

On Dec 7, 2015, at 5:58 PM, Michel Fortin via swift-evolution <swift-evolution@swift.org> wrote:

Le 7 déc. 2015 à 20:29, Kevin Ballard via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

On Mon, Dec 7, 2015, at 01:07 PM, Dmitri Gribenko via swift-evolution >> wrote:

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

As has been pointed out in other threads on this topic, you can use a
defer statement to avoid duplicating the increment portion:

var i = initial
while cond(i) {
  defer { i = increment(i) }
  // ...
  if frob {
      continue // defer will execute
  }
}

Which is a dangerous substitute, because it also does the wrong thing with break and throw:

  var i = initial
  while cond(i) {
    defer { i = increment(i) }
    // ...
    if frob {
      continue // defer will execute
    } else if blob {
      break // defer will execute!!!
    } else if plob {
      throw MyError // defer will execute!!!
    }
  }


(Kevin Lundberg) #9

For throw, if incrementing i has no external side effects, then there shouldn’t be any issue since i will be thrown away anyways when the error is thrown.

For break, it can be considered dangerous, but if your while loops are written semantically like the old c-style for loop, the counter shouldn’t be used outside the scope of the loop anyways. If that matters to whomever may write code like this, then manual incrementing at each point where loop scope breaks is necessary (though I imagine such cases are likely rare).

···

--
Kevin Lundberg

On Dec 7, 2015, at 8:58 PM, Michel Fortin via swift-evolution <swift-evolution@swift.org> wrote:

Le 7 déc. 2015 à 20:29, Kevin Ballard via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

On Mon, Dec 7, 2015, at 01:07 PM, Dmitri Gribenko via swift-evolution >> wrote:

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

As has been pointed out in other threads on this topic, you can use a
defer statement to avoid duplicating the increment portion:

var i = initial
while cond(i) {
  defer { i = increment(i) }
  // ...
  if frob {
      continue // defer will execute
  }
}

Which is a dangerous substitute, because it also does the wrong thing with break and throw:

  var i = initial
  while cond(i) {
    defer { i = increment(i) }
    // ...
    if frob {
      continue // defer will execute
    } else if blob {
      break // defer will execute!!!
    } else if plob {
      throw MyError // defer will execute!!!
    }
  }

--
Michel Fortin
michel.fortin@michelf.ca <mailto:michel.fortin@michelf.ca>
https://michelf.ca <https://michelf.ca/>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Michel Fortin) #10

That's buggy (and also does not compile). What you want is this:

var i = 0
var first = true
while ({ () -> Bool in if first { first = false } else { i = increment(i) }; return cond(i) }()) {
}

Interestingly, this behaves correctly with both continue, break, and throw. It's not pretty, but this struct will make a little bit easier on the eye:

/// Provide a suitable condition for recreating the control flow of a C-style `for`
/// loop within a `while` loop.
///
/// var loop = CForLoop()
/// var i = 0
/// while loop.test(i < 2, {i += 1}) { ... }
///
/// - Note: You have to make a new controller each time you start a loop.
struct CForLoop {

  /// Flag to prevent the increment part from executing on the first iteration of
  /// the loop.
  private var first = true

  /// If this is the first call of test, simply evaluate `predicate` and return
  /// its value. Subsequent calls will execute `increment` before evaluating
  /// `predicate`.
  /// - Parameter predicate: The test condition that must be true for the loop to
  /// continue. This is the second statement in a C-style `for` loop.
  /// - Parameter increment: The code to execute just before re-evaluating the
  /// predicate at the end of one loop iteration. This is the third statement
  /// of a C-style `for` loop.
  /// - Returns: The value returned by predicate. This value should be used as
  /// the condition in a `while` loop.
  mutating func test(@autoclosure predicate: () -> Bool, _ increment: () -> ()) -> Bool {
    if first {
      first = false
    } else {
      increment()
    }
    return predicate()
  }
}

···

Le 7 déc. 2015 à 21:03, Joe Groff <jgroff@apple.com> a écrit :

A more accurate (but not particularly pretty) substitution would be to perform the increment as part of the condition:

while { i = increment(i); return cond(i) }() {
}

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(thorsten@portableinnovations.de) #11

Shouldn't there be a reversed, i.e. a computed var returning a new range, so we could write the following with only half the parantheses:

(a..<b).reversed

This would look nice and understandable to me.

-Thorsten

···

Am 07.12.2015 um 23:37 schrieb Jacob Bandes-Storch via swift-evolution <swift-evolution@swift.org>:

Of course Nate's example "(a..<b).reverse()" works too, but it's far too many parentheses for my taste.


(Michel Fortin) #12

True. But if the C-style for loop was not part of the language, would you feel obligated to teach an equivalent like the above?

Here's another idea though. We could have a while loop that looks like this:

  var i = 0
  while i < 5 reloop i += 1 {
  }

The reloop part would be executed upon reaching the end of the loop block, or whenever a continue is encountered: always just before reevaluating the condition. Unlike defer, the reloop part does not execute on break, return or throw.

It works just like a C-style for loop, minus the oddity regarding the scope of the loop variable.

···

Le 9 déc. 2015 à 2:04, Greg Parker via swift-evolution <swift-evolution@swift.org> a écrit :

On Dec 7, 2015, at 5:29 PM, Kevin Ballard via swift-evolution <swift-evolution@swift.org> wrote:

var i = initial
while cond(i) {
  defer { i = increment(i) }

Strange. Can the compiler optimizer handle that efficiently?

Michel Fortin wrote:

var i = 0
var first = true
while ({ () -> Bool in if first { first = false } else { i = increment(i) }; return cond(i) }()) {
}

I fear that teaching the C-style for loop is easier than teaching that.

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca


(thorsten@portableinnovations.de) #13

As the removal of the c-style for-loop is supposed to make things easier to understand I'm a bit worried about the replacements for corner cases of the c-style for-loop.

Defer is simply wrong and dangerous and putting the increment into the condition is even uglier than the c-style for-loop :confused:

Because of that I'd like to repeat my proposal of a modernized syntax for the c-style for-loop which gets rid of the c-style and might be preferable for those corner cases.

for var i=0 while i < limit next i += 1 { ... }

together with its companion

for var i=0 until i == limit next i += 1 { ... }

-Thorsten

···

Am 08.12.2015 um 03:04 schrieb Kevin Lundberg via swift-evolution <swift-evolution@swift.org>:

For throw, if incrementing i has no external side effects, then there shouldn’t be any issue since i will be thrown away anyways when the error is thrown.

For break, it can be considered dangerous, but if your while loops are written semantically like the old c-style for loop, the counter shouldn’t be used outside the scope of the loop anyways. If that matters to whomever may write code like this, then manual incrementing at each point where loop scope breaks is necessary (though I imagine such cases are likely rare).
--
Kevin Lundberg

On Dec 7, 2015, at 8:58 PM, Michel Fortin via swift-evolution <swift-evolution@swift.org> wrote:

Le 7 déc. 2015 à 20:29, Kevin Ballard via swift-evolution <swift-evolution@swift.org> a écrit :

On Mon, Dec 7, 2015, at 01:07 PM, Dmitri Gribenko via swift-evolution >>> wrote:

Another concern of mine is the equivalent of C-style for loops with
'continue' statements in them. The only equivalent based on 'while' I
can think of duplicates the increment portion of the loop.

As has been pointed out in other threads on this topic, you can use a
defer statement to avoid duplicating the increment portion:

var i = initial
while cond(i) {
  defer { i = increment(i) }
  // ...
  if frob {
      continue // defer will execute
  }
}

Which is a dangerous substitute, because it also does the wrong thing with break and throw:

  var i = initial
  while cond(i) {
    defer { i = increment(i) }
    // ...
    if frob {
      continue // defer will execute
    } else if blob {
      break // defer will execute!!!
    } else if plob {
      throw MyError // defer will execute!!!
    }
  }

--
Michel Fortin
michel.fortin@michelf.ca
https://michelf.ca

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution