From b9d1ed04171af2474bb397331c3f698e34d9ab10 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 17 May 2016 14:23:46 -0500 Subject: [PATCH 1/6] Add proposal for last[Index](where:) methods --- proposals/0000-add-last-methods.md | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 proposals/0000-add-last-methods.md diff --git a/proposals/0000-add-last-methods.md b/proposals/0000-add-last-methods.md new file mode 100644 index 0000000000..e875cccb04 --- /dev/null +++ b/proposals/0000-add-last-methods.md @@ -0,0 +1,100 @@ +# Add `last(where:)` and `lastIndex(where:)` Methods to Bidirectional Collections + +* Proposal: [SE-0000]() +* Author: [Nate Cook](https://github.com/natecook1000) +* Status: **Awaiting review** +* Review manager: TBD + +## Introduction + +The standard library should include methods for finding the last element of a bidirectional collection that matches a predicate, along with the index of that element. + +Swift-evolution thread: [Discussion](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160509/017048.html) + +## Motivation + +The standard library currently has methods that perform a linear search from the beginning of a collection to find an element or the index of an element that matches a predicate: + +```swift +let a = [20, 30, 10, 40, 20, 30, 10, 40, 20] +a.first(where: { $0 > 25 }) // 30 +a.index(of: 10) // 2 +a.index(where: { $0 > 25 }) // 1 +``` + +Unfortunately, there are no such methods that search from the end of a bidirectional collection. Finding the last of a particular kind of element has multiple applications, particularly with text, such as wrapping a long string into lines of a maximum length or trimming whitespace from the beginning and end of a string. + +You can work around this limitation by using the methods above on a reversed view of a collection, but the resulting code is truly dreadful. For example, to find the corresponding last index to `a.index(where: { $0 > 25 })`, this unholy incantation is required: + +```swift +(a.reversed().index(where: { $0 > 25 })?.base).flatMap({ a.index(before: $0) }) +``` + +## Proposed solution + +Bidirectional collections should include three new methods for symmetry with the existing forward-searching APIs: `last(where:)`, `lastIndex(where:)`, and `lastIndex(of:)`. + +These additions remove the need for searching in a reversed collection and allow code like the following: + +```swift +a.last(where: { $0 > 25 }) // 40 +a.lastIndex(of: 10) // 6 +a.lastIndex(where: { $0 > 25 }) // 7 +``` +Much better! + +## Detailed design + +The three new methods will be added to the standard library in extensions to `BidirectionalCollection`. The implementation is straightforward: + +```swift +extension BidirectionalCollection { + /// Returns the index of the last element of the collection that satisfies + /// the given predicate, or `nil` if no element does. + func lastIndex(where predicate: @noescape (Iterator.Element) throws -> Bool) + rethrows -> Index? + { + var i = endIndex + while i != startIndex { + formIndex(before: &i) + if try predicate(self[i]) { + return i + } + } + return nil + } + + /// Returns the last element of the collection that satisfies the given + /// predicate, or `nil` if no element does. + func last(where predicate: @noescape (Iterator.Element) throws -> Bool) + rethrows -> Iterator.Element? + { + if let i = try lastIndex(where: predicate) { + return self[i] + } + return nil + } +} + +extension BidirectionalCollection where Iterator.Element: Equatable { + /// Returns the index of the last element equal to the given element, or + /// `nil` if there's no equal element. + func lastIndex(of element: Iterator.Element) -> Index? { + var i = endIndex + while i != startIndex { + formIndex(before: &i) + if element == self[i] { + return i + } + } + return nil + } +} +``` +## Impact on existing code + +This change is strictly additive and should have no impact on existing code. + +## Alternatives considered + +For consistency, one suggestion was to rename `index(of:)` and `index(where:)` to `firstIndex(of:)` and `firstIndex(where:)`, respectively. That change is outside the scope of this proposal. From 80e90393c9d06e1cf496243412046769edfd4fc8 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 17 May 2016 14:43:12 -0500 Subject: [PATCH 2/6] Add link to related Swift bug --- proposals/0000-add-last-methods.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proposals/0000-add-last-methods.md b/proposals/0000-add-last-methods.md index e875cccb04..27d691dba5 100644 --- a/proposals/0000-add-last-methods.md +++ b/proposals/0000-add-last-methods.md @@ -9,7 +9,8 @@ The standard library should include methods for finding the last element of a bidirectional collection that matches a predicate, along with the index of that element. -Swift-evolution thread: [Discussion](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160509/017048.html) +* Swift-evolution thread: [\[swift-evolution\] (Draft) Add last(where:) and lastIndex(where:) methods](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160509/017048.html) +* Related Bug: [\[SR-1504\] RFE: index(of:) but starting from end](https://bugs.swift.org/browse/SR-1504) ## Motivation From 7213432acf2496e880f6617ece9efdb0ab1fd425 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Sat, 21 May 2016 03:43:48 -0500 Subject: [PATCH 3/6] Incorporate more feedback from list --- proposals/0000-add-last-methods.md | 69 ++++++++++++++---------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/proposals/0000-add-last-methods.md b/proposals/0000-add-last-methods.md index 27d691dba5..e4bec6c777 100644 --- a/proposals/0000-add-last-methods.md +++ b/proposals/0000-add-last-methods.md @@ -25,7 +25,7 @@ a.index(where: { $0 > 25 }) // 1 Unfortunately, there are no such methods that search from the end of a bidirectional collection. Finding the last of a particular kind of element has multiple applications, particularly with text, such as wrapping a long string into lines of a maximum length or trimming whitespace from the beginning and end of a string. -You can work around this limitation by using the methods above on a reversed view of a collection, but the resulting code is truly dreadful. For example, to find the corresponding last index to `a.index(where: { $0 > 25 })`, this unholy incantation is required: +You can work around this limitation by using the methods above on a reversed view of a collection, but the resulting code is truly dreadful. For example, to find the corresponding last index to `a.index(where: { $0 > 25 })`, something lik this unholy incantation is required: ```swift (a.reversed().index(where: { $0 > 25 })?.base).flatMap({ a.index(before: $0) }) @@ -33,7 +33,7 @@ You can work around this limitation by using the methods above on a reversed vie ## Proposed solution -Bidirectional collections should include three new methods for symmetry with the existing forward-searching APIs: `last(where:)`, `lastIndex(where:)`, and `lastIndex(of:)`. +The `Collection` protocol should include three new methods for symmetry with the existing forward-searching APIs: `last(where:)`, `lastIndex(where:)`, and `lastIndex(of:)`. In addition, the two forward-searching methods `index(of:)` and `index(where:)` should be renamed to `firstIndex(of:)` and `firstIndex(where:)`. This renaming would link these methods with the new `first(where:)` method, disambiguate them from index manipulation methods like `index(after:)`, and set up a consistent relationship between the `first...` and `last...` methods. These additions remove the need for searching in a reversed collection and allow code like the following: @@ -46,56 +46,51 @@ Much better! ## Detailed design -The three new methods will be added to the standard library in extensions to `BidirectionalCollection`. The implementation is straightforward: +`lastIndex(where:)` and `last(where:)` will be added to the standard library as `Collection` protocol requirements with default implementations in both `Collection` and `BidirectionalCollection`, which can provide a more efficient implementation. `lastIndex(of:)` while be in an extension constrained to equatable elements. The new and renamed APIs are shown here: ```swift -extension BidirectionalCollection { +protocol Collection { + // New methods: + + /// Returns the last element of the collection that satisfies the given + /// predicate, or `nil` if no element does. + func last(where predicate: @noescape (Iterator.Element) throws -> Bool) + rethrows -> Iterator.Element? + /// Returns the index of the last element of the collection that satisfies /// the given predicate, or `nil` if no element does. func lastIndex(where predicate: @noescape (Iterator.Element) throws -> Bool) rethrows -> Index? - { - var i = endIndex - while i != startIndex { - formIndex(before: &i) - if try predicate(self[i]) { - return i - } - } - return nil - } - /// Returns the last element of the collection that satisfies the given - /// predicate, or `nil` if no element does. - func last(where predicate: @noescape (Iterator.Element) throws -> Bool) - rethrows -> Iterator.Element? - { - if let i = try lastIndex(where: predicate) { - return self[i] - } - return nil - } + // Renamed method: + + /// Returns the index of the first element of the collection that satisfies + /// the given predicate, or `nil` if no element does. + func firstIndex(where predicate: @noescape (Iterator.Element) throws -> Bool) + rethrows -> Index? } -extension BidirectionalCollection where Iterator.Element: Equatable { +extension Collection where Iterator.Element: Equatable { + // New method: + /// Returns the index of the last element equal to the given element, or /// `nil` if there's no equal element. - func lastIndex(of element: Iterator.Element) -> Index? { - var i = endIndex - while i != startIndex { - formIndex(before: &i) - if element == self[i] { - return i - } - } - return nil - } + func lastIndex(of element: Iterator.Element) -> Index? + + // Renamed method: + + /// Returns the index of the first element equal to the given element, or + /// `nil` if there's no equal element. + func firstIndex(of element: Iterator.Element) -> Index? } ``` + +Implementations of these methods can be explored in [this Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/fc545dd5bcafa352ceac5494fe17421f6391685e1edd70bc1dfa196b6d77dd88). + ## Impact on existing code -This change is strictly additive and should have no impact on existing code. +The addition of the `last...` methods is strictly additive and should have no impact on existing code. The migration tools should be able to provide a fixit for the simple renaming of `index(of:)` and `index(where:)`. ## Alternatives considered -For consistency, one suggestion was to rename `index(of:)` and `index(where:)` to `firstIndex(of:)` and `firstIndex(where:)`, respectively. That change is outside the scope of this proposal. +An earlier proposal limited the proposed new methods to the `BidirectionalCollection` protocol. This isn't a necessary limitation, as the standard library already has methods on forward collections with the same performance characteristics. That earlier proposal also did not include renaming `index(of:)` and `index(where:)` to `firstIndex(of:)` and `firstIndex(where:)`, respectively. From 46d75657512e24da82ed4264b32292a98ec3242d Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 24 May 2016 11:44:02 -0500 Subject: [PATCH 4/6] Take out remaining BidirectionalCollection limits --- proposals/0000-add-last-methods.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/0000-add-last-methods.md b/proposals/0000-add-last-methods.md index e4bec6c777..d512a57efd 100644 --- a/proposals/0000-add-last-methods.md +++ b/proposals/0000-add-last-methods.md @@ -1,4 +1,4 @@ -# Add `last(where:)` and `lastIndex(where:)` Methods to Bidirectional Collections +# Add `last(where:)` and `lastIndex(where:)` Methods to Collections * Proposal: [SE-0000]() * Author: [Nate Cook](https://github.com/natecook1000) @@ -7,7 +7,7 @@ ## Introduction -The standard library should include methods for finding the last element of a bidirectional collection that matches a predicate, along with the index of that element. +The standard library should include methods for finding the last element of a collection that matches a predicate, along with the index of that element. * Swift-evolution thread: [\[swift-evolution\] (Draft) Add last(where:) and lastIndex(where:) methods](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160509/017048.html) * Related Bug: [\[SR-1504\] RFE: index(of:) but starting from end](https://bugs.swift.org/browse/SR-1504) @@ -23,7 +23,7 @@ a.index(of: 10) // 2 a.index(where: { $0 > 25 }) // 1 ``` -Unfortunately, there are no such methods that search from the end of a bidirectional collection. Finding the last of a particular kind of element has multiple applications, particularly with text, such as wrapping a long string into lines of a maximum length or trimming whitespace from the beginning and end of a string. +Unfortunately, there are no such methods that search from the end. Finding the last of a particular kind of element has multiple applications, particularly with text, such as wrapping a long string into lines of a maximum length or trimming whitespace from the beginning and end of a string. You can work around this limitation by using the methods above on a reversed view of a collection, but the resulting code is truly dreadful. For example, to find the corresponding last index to `a.index(where: { $0 > 25 })`, something lik this unholy incantation is required: From 0c00adbe50795ecf91d144b9df96ce395123bd03 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 27 May 2016 09:51:55 -0500 Subject: [PATCH 5/6] Fix spelling error, update link --- proposals/0000-add-last-methods.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/0000-add-last-methods.md b/proposals/0000-add-last-methods.md index d512a57efd..f0cf571bc6 100644 --- a/proposals/0000-add-last-methods.md +++ b/proposals/0000-add-last-methods.md @@ -25,7 +25,7 @@ a.index(where: { $0 > 25 }) // 1 Unfortunately, there are no such methods that search from the end. Finding the last of a particular kind of element has multiple applications, particularly with text, such as wrapping a long string into lines of a maximum length or trimming whitespace from the beginning and end of a string. -You can work around this limitation by using the methods above on a reversed view of a collection, but the resulting code is truly dreadful. For example, to find the corresponding last index to `a.index(where: { $0 > 25 })`, something lik this unholy incantation is required: +You can work around this limitation by using the methods above on a reversed view of a collection, but the resulting code is truly dreadful. For example, to find the corresponding last index to `a.index(where: { $0 > 25 })`, something like this unholy incantation is required: ```swift (a.reversed().index(where: { $0 > 25 })?.base).flatMap({ a.index(before: $0) }) @@ -85,7 +85,7 @@ extension Collection where Iterator.Element: Equatable { } ``` -Implementations of these methods can be explored in [this Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/fc545dd5bcafa352ceac5494fe17421f6391685e1edd70bc1dfa196b6d77dd88). +Implementations of these methods can be explored in [this Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/e812a36cfa66647e1dbd7ab5be5376f78c769924262178d62c25aa0124c45810). ## Impact on existing code From 5bb14845c35773b4a5e72d40fc2a632251c64637 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 10 Jun 2016 15:34:17 -0500 Subject: [PATCH 6/6] Updated link to sandbox implementation. --- proposals/0000-add-last-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-add-last-methods.md b/proposals/0000-add-last-methods.md index f0cf571bc6..702b157937 100644 --- a/proposals/0000-add-last-methods.md +++ b/proposals/0000-add-last-methods.md @@ -85,7 +85,7 @@ extension Collection where Iterator.Element: Equatable { } ``` -Implementations of these methods can be explored in [this Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/e812a36cfa66647e1dbd7ab5be5376f78c769924262178d62c25aa0124c45810). +Implementations of these methods can be explored in [this Swift sandbox](http://swiftlang.ng.bluemix.net/#/repl/575b24124a73177a6fa59273). ## Impact on existing code