Skip to content

[stdlib][NFC] Simplify partition(by:)’s implementation #34814

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

Merged
merged 1 commit into from
Nov 30, 2020

Conversation

lorentey
Copy link
Member

Getting rid of the labeled breaks makes things a little easier to follow for me.

Also, fix a case in the description of the loop invariant
@lorentey
Copy link
Member Author

@swift-ci test

@NevinBR
Copy link
Contributor

NevinBR commented Nov 22, 2020

I think the implementation can be simplified further, so it only has two loops instead of three:

var lo = startIndex
var hi = endIndex

while lo < hi {
  if try belongsInSecondPartition(self[lo]) {
    repeat {
      formIndex(before: &hi)
      guard lo < hi else { return lo }
    } while try belongsInSecondPartition(self[hi])
    
    swapAt(lo, hi)
  }
  
  formIndex(after: &lo)
}

return lo

Edit:

Or perhaps even simpler with only one loop:

var lo = startIndex
var hi = endIndex

while lo < hi {
  if try belongsInSecondPartition(self[lo]) {
    hi = try self[lo..<hi].lastIndex{ try !belongsInSecondPartition($0) } ?? lo
    if hi == lo { return lo }
    swapAt(lo, hi)
  }
  formIndex(after: &lo)
}

return lo

@lorentey
Copy link
Member Author

lorentey commented Nov 24, 2020

I like the way the original follows easily understandable steps; it also matches the way Hoare originally described this algorithm.

Reducing the number of loops does not seem to increase readability for me; in fact, I find it's quite the opposite.

@NevinBR
Copy link
Contributor

NevinBR commented Nov 24, 2020

Perhaps it’s just a personal preference, but I find the one-loop version significantly easier to understand than the three-loop version. So much so that the explanatory comments struck me as unnecessary and redundant, so I removed them.

Maybe adding some of them back might be to your liking?

var lo = startIndex
var hi = endIndex

while lo < hi {
  // Invariants at this point:
  //
  // * all elements in `startIndex ..< lo` belong in the first partition
  // * all elements in `hi ..< endIndex` belong in the second partition

  if try belongsInSecondPartition(self[lo]) {
    
    // Find next element down from `hi` that we can swap `lo` with.
    hi = try self[lo..<hi].lastIndex {
      try !belongsInSecondPartition($0)
    } ?? lo
    
    if hi == lo { return lo }
    swapAt(lo, hi)
  }
  
  formIndex(after: &lo)
}

return lo

it also matches the way Hoare originally described this algorithm.

I do not see any sample code in the article you linked.

The written prose description of the algorithm in the article matches the one-loop version just as well as the three-loop version, as of course it must since both implementations do the same thing.

@lorentey
Copy link
Member Author

FWIW, I still find my boring version easier to follow; there is also a symmetry to it that is lost in the two-loop lastIndex(where:) variant.

To be fair, I find both of these to be more accessible than the original one with the goto statements. ¯_(ツ)_/¯

@lorentey lorentey merged commit c51b10a into swiftlang:main Nov 30, 2020
@lorentey lorentey deleted the partitioning-cleanup branch November 30, 2020 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants