Unless I’m confused, all of the “ranged” overloads can already be achieved by 
composing ranged subscripts with the indexOf, etc., methods:

let ar = [0, 1, 2, 3, 4, 5]
let ind = ar[3...5].indexOf { n in n % 2 == 0 }
ar[ind!] // 4

> On 5 Jan 2016, at 01:00, Vincent Esche via swift-evolution 
> <[email protected]> wrote:
> 
> There are a couple of things I’m not 100% happy with/sure about:
> 
> 1.
> I don’t like how it’s
> “<index/range/count>Of(element:range:)” but 
> “<index/range/count>Of(range:predicate:)”.
> The reason I went for “<index/range/count>Of(range:predicate:)” was to go 
> with the standard pattern of putting closures last and thus allowing for 
> trailing closure syntax.
> 
> The current order allows for this:
> “<index/range/count>Of(0..10)  { $0.name == “Foobar" }”
> which I like syntax-wise.
> It however makes it look like one was looking for a range ("indexOf(0..10, 
> …)”), not the predicate.
> 
> While the alternative requires this:
> “<index/range/count>Of(predicate: { $0.name == “Foobar" }, range: 0..10)”
> 
> I’m actually leaning towards the latter now. Dang!
> 
> 2.
> I’m unsure about the name of “lensView”. I first went with “transform”, then 
> with “mappingFunction”, but found that neither of those made it clear that 
> the closure should provide a specific view into the element.  Both 
> “transform" and “mappingFunction” imply the transformation from one data type 
> to another. It’s not about transforming. It’s about accessing.
> Thus looked for names of similar patterns and found “keypaths” (kind of) in 
> ObjC and lenses in Haskell. To people familiar with functional programming 
> the name should be clear. The closure passed to “lensView” is basically the 
> getter part of a functional lens.
> Anyway, I’m not too attached to the name and more than open to suggestions.
> 
> 3.
> “BinarySearchView.init(base:validateAgainst:)” currently asserts. This should 
> probably be split into two init functions. One assuming the base to be sorted 
> (“BinarySearchView.init(base:)”, O(1)). And one allowing for a check, 
> returning nil on failure (“BinarySearchView.init?(base:validateAgainst:)”, 
> O(self.count)). Or should at least throw an error, instead of panicking.
> 
> Note: I made some minor documentation changes/fixes.
> See https://gist.github.com/regexident/2b7531bd748f57679671 
> <https://gist.github.com/regexident/2b7531bd748f57679671> for up-to-date 
> RFC/source code (vs. Markdown-dump at bottom of OP).
> 
> - Vincent
> 
>> On 04 Jan 2016, at 16:13, Vincent Esche <[email protected] 
>> <mailto:[email protected]>> wrote:
>> 
>> After having had this code laying around on my Mac since 6/26/15 (waiting 
>> for Swift to go open source)
>> I figured it’s about damn time I submit the actual RFC for it. So without 
>> further ado…
>> 
>> One of the areas where Swift’s stdlib is still quite lacking is searching.
>> Which is a shame as searching (along with indexing) might be among
>> the most important tasks of computer science/software development.
>> One might not need fast collection searches for writing a banal fart or 
>> flashlight app,
>> but almost every other app is likely to benefit from having a proper 
>> searching API.
>> 
>> So I’d like to fix that.
>> 
>> Attached you find a full RFC along with the full and functional source code 
>> to make it happen.
>> 
>> I’d love to hear your opinion on this and will be more than happy to answer 
>> questions.
>> 
>> Rendered Version + Full Implementation: 
>> https://gist.github.com/regexident/2b7531bd748f57679671 
>> <https://gist.github.com/regexident/2b7531bd748f57679671>
>> (The code is tested using Quick/Nimble. Tests would thus have to be ported 
>> to Swift’s XCTest.)
>> 
>> - Vincent
>> 
>>> Markdown-dump for
>>> 
>>> # Improved Collection Search
>>> 
>>> * Proposal: 
>>> [SE-NNNN](https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md
>>>  
>>> <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>)
>>> * Author(s): [Vincent Esche](https://github.com/regexident 
>>> <https://github.com/regexident>)
>>> * Status: **Review**
>>> * Review manager: TBD
>>> 
>>> ## Introduction
>>> 
>>> This RFC proposes an extension to the currently rather limited and linear 
>>> searching API of `CollectionType`, that is `indexOf(element:)` and 
>>> `indexOf(predicate:)`.
>>> It proposes the backwards-compatible refactoring of those existing methods 
>>> as well as the introduction of a view-based ensemble of methods for 
>>> efficient binary-search.
>>> 
>>> Swift-evolution thread: [link to the discussion thread for that 
>>> proposal](https://lists.swift.org/pipermail/swift-evolution 
>>> <https://lists.swift.org/pipermail/swift-evolution>)
>>> 
>>> ## Motivation
>>> 
>>> Indexing of and searching in data is a big deal in software development. As 
>>> such it is crucial to have capable means of doing so in a language that 
>>> aspires to be a highly performant programming language.
>>> 
>>> Swift's current API for searching is basically limited to these two methods 
>>> on `CollectionType`:
>>> 
>>> > ```swift
>>> > func indexOf(element: Self.Generator.Element) -> Self.Index?
>>> > ```
>>> > Returns the first index where value appears in self or nil if value is 
>>> > not found.
>>> >
>>> > **Complexity**: `O(self.count)`.
>>> 
>>> and:
>>> 
>>> > ```swift
>>> > func indexOf(@noescape predicate: (Self.Generator.Element) throws -> 
>>> > Bool) rethrows -> Self.Index?
>>> > ```
>>> > Returns the first index where predicate returns true for the 
>>> > corresponding value, or nil if such value is not found.
>>> > 
>>> > **Complexity**: `O(self.count)`.
>>> 
>>> The author sees a couple of issue with these two methods:
>>> 
>>> 1. They do not provide an option for restricting the "haystack" to a 
>>> certain `range` of `self`.
>>> 2. They do not provide a fast (`O(log2(self.count))`) path for sorted 
>>> collections.
>>> 3. They should not be limited to `CollectionType` but instead be moved to 
>>> `Indexable`.
>>> 
>>> In many situations it is desirable to perform fast searches on sorted 
>>> collections instead of having to fall back to naïve linear searches.
>>> 
>>> Further more while a single `index` might be the most common subject of 
>>> interest, there are many scenarios where a `range` or `count` of matches 
>>> are of more use.
>>> 
>>> And anyone who is writing a custom ordered collection type will eventually 
>>> want to find the insertion index for a given element and will thus end up 
>>> writing some variant of `lowerBoundOf(…)` or `upperBoundOf(…)` (upon which 
>>> all the other methods can be implemented, actually).
>>> 
>>> ## Proposed solution
>>> 
>>> The author proposes:
>>> 
>>> 1. A backwards-compatible refactoring of `CollectionType.indices`, moving 
>>> it to `Indexable`.
>>> 
>>> 2. A backwards-compatible refactoring of `indexOf(…)` (adding optional 
>>> `range:` and moving it to `Indexable`).
>>> 
>>> 3. The addition of `rangeOf(…)`, `countOf(…)` and `isSorted(…)` to 
>>> `Indexable` with a TIME complexity of `O(self.count)`.
>>> 
>>> 4. The introduction of a `BinarySearchView` on `Indexable`, allowing for 
>>> fast (`O(log2(self.count))`) searches on `Indexable` via `indexOf(…)`, 
>>> `rangeOf(…)`, `countOf(…)`, `lowerBoundOf(…)`, `upperBoundOf(…)` without 
>>> cluttering `Indexable`'s interface.
>>> 
>>> ## Detailed design
>>> 
>>> ### `CollectionType.indices`:
>>> 
>>> The author proposes the relocation of `.indices` from `CollectionType` to 
>>> `Indexable`:
>>> 
>>> ```swift
>>> extension Indexable {
>>>     /// Return the range of valid index values.
>>>     ///
>>>     /// The result's `endIndex` is the same as that of `self`.  Because
>>>     /// `Range` is half-open, iterating the values of the result produces
>>>     /// all valid subscript arguments for `self`, omitting its `endIndex`.
>>>     public var indices: Range<Self.Index> { get }
>>> }
>>> ```
>>> 
>>> After all all it does is provide a convenience method for generating a 
>>> Range from its `startIndex` and `endIndex`. There is no need for `self` to 
>>> be a `CollectionType` for this.
>>> 
>>> This change should not break any existing code as it generalizes the 
>>> property.
>>> 
>>> ### `Indexable` of `Comparable` elements:
>>> 
>>> The author proposes the addition of the following method on `Indexable 
>>> where Element : Comparable`:
>>> 
>>> ```swift
>>> extension Indexable where Index: RandomAccessIndexType, _Element: 
>>> Comparable {
>>>     /// Return true iff all elements in `self` within `range` are sorted 
>>> according to `isOrderedBefore`.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func isSorted(range: Range<Index>? = nil) -> Bool
>>> }
>>> ```
>>> 
>>> This method would perform a linear scan of the `Indexable` with a TIME 
>>> complexity of `O(self.count)`.
>>> 
>>> An actual implementation of these methods can be found in `Indexable.swift`.
>>> 
>>> ### `Indexable` of `Equatable ` elements:
>>> 
>>> The author proposes the addition of the following method on `Indexable 
>>> where Element : Equatable`:
>>> 
>>> ```swift
>>> extension Indexable where Index : RandomAccessIndexType, _Element : 
>>> Equatable {
>>> 
>>>     /// Returns the first index where `element` appears in `self`
>>>     /// within range `range` or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func indexOf(element: _Element, range: Range<Index>? = default) 
>>> -> Self.Index?
>>> 
>>>     /// Returns the first range where `element` appears in `self`
>>>     /// within range `range` or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func rangeOf(element: _Element, range: Range<Index>? = default) 
>>> -> Range<Self.Index>?
>>> 
>>>     /// Returns the number of `element`s that appear in `self`
>>>     /// within range `range` or `nil` if such an element is not found.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func countOf(element: _Element, range: Range<Index>? = default) 
>>> -> Int
>>> }
>>> ```
>>> 
>>> These methods would perform a linear scan of the `Indexable` with a TIME 
>>> complexity of `O(self.count)`.
>>> 
>>> An actual implementation of these methods can be found in `Indexable.swift`.
>>> 
>>> ### `Indexable` of any elements:
>>> 
>>> The author proposes the addition of the following methods on `Indexable`:
>>> 
>>> ```swift
>>> extension Indexable where Index : RandomAccessIndexType {
>>> 
>>>     /// Returns the first index where an element appears in `self`
>>>     /// within range `range` for which `lensView(element) == value` or 
>>> `nil` if such an element is not found.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func indexOf(range: Range<Index>? = default, @noescape 
>>> predicate: (_Element) throws -> Bool) rethrows -> Self.Index?
>>> 
>>>     /// Returns the first range where an element appears in `self`
>>>     /// within range `range` for which `lensView(element) == value` or 
>>> `nil` if such an element is not found.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func rangeOf(range: Range<Index>? = default, @noescape 
>>> predicate: (_Element) throws -> Bool) rethrows -> Range<Self.Index>?
>>> 
>>>     /// Returns the number of `element`s that appear in `self`
>>>     /// within range `range` or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`self.count`)
>>>     public func countOf(range: Range<Index>? = default, @noescape 
>>> predicate: (_Element) throws -> Bool) rethrows -> Int
>>> 
>>>     /// Return true iff all elements in `self` within `range` are sorted 
>>> according to `isOrderedBefore`.
>>>     /// 
>>>     /// - Complexity: O(`self.count`)
>>>     public func isSorted(range: Range<Index>? = default, @noescape 
>>> isOrderedBefore: (_Element, _Element) throws -> Bool) rethrows -> Bool
>>> }
>>> ```
>>> 
>>> These methods would perform a linear scan of the `Indexable` with a TIME 
>>> complexity of `O(self.count)`.
>>> 
>>> An actual implementation of these methods can be found in `Indexable.swift`.
>>> 
>>> **Note**: For explanation of and reasoning behind the `lensView:` argument 
>>> as well as the distinction between `T` and `Base._Element`, see below.
>>> 
>>> ### `Indexable.binarySearchView(validateAgainst:?)`:
>>> 
>>> The author proposes the addition of a `binarySearchView(validateAgainst:?)` 
>>> method on `Indexable` for obtaining a specialized search view for 
>>> performing searches with O(`log2(self.count)`) complexity:
>>> 
>>> ```swift
>>> extension Indexable where Index: RandomAccessIndexType {
>>>     /// Create a view into a sorted indexable that allows access within 
>>> `bounds`.
>>>     ///
>>>     /// - Complexity: O(`self.count`) if a validation closure is provided, 
>>> otherwise O(`1`).
>>>     public func binarySearchView(validateAgainst isOrderedBefore: 
>>> ((_Element, _Element) -> Bool)? = nil) -> BinarySearchView<Self>
>>> }
>>> ```
>>> 
>>> ### `BinarySearchView<T: Indexable>`:
>>> 
>>> The author proposes the introduction of a `BinarySearchView` struct with 
>>> the following interface:
>>> 
>>> ```swift
>>> public struct BinarySearchView<Base : Indexable where Base.Index : 
>>> RandomAccessIndexType> : Indexable {
>>>     public typealias Index = Base.Index
>>> 
>>>     public var startIndex: Base.Index { get }
>>> 
>>>     public var endIndex: Base.Index { get }
>>> 
>>>     public subscript (position: Index) -> Base._Element { get }
>>> 
>>>     /// Create a view into a sorted collection `base` that allows access 
>>> within `bounds`.
>>>     ///
>>>     /// - Complexity: O(1).
>>>     public init(base: Base, validateAgainst isOrderedBefore: 
>>> ((Base._Element, Base._Element) -> Bool)? = default)
>>> 
>>>     /// Returns first index of the first `element` in `self` within range 
>>> `range` for which
>>>     /// `lensView(element) < value` evaluates to false or `nil` if 
>>> `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func lowerBoundOf<T : Comparable>(value: T, range: Range<Index>? 
>>> = default, @noescape isOrderedBefore: (T, T) -> Bool = default, @noescape 
>>> lensView: Base._Element -> T) -> Base.Index
>>> 
>>>     /// Returns first index of the first `element` in `self` within range 
>>> `range` for which
>>>     /// `value < lensView(element)` evaluates to true or `nil` if `element` 
>>> is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func upperBoundOf<T : Comparable>(value: T, range: Range<Index>? 
>>> = default, @noescape isOrderedBefore: ((T, T) -> Bool) = default, @noescape 
>>> lensView: Base._Element -> T) -> Base.Index
>>> 
>>>     /// Returns the first index where an element appears in `self`
>>>     /// within range `range` for which `lensView(element) == value` or 
>>> `nil` if such an element is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func indexOf<T : Comparable>(value: T, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (T, T) -> Bool = default, @noescape 
>>> lensView: Base._Element -> T) -> Base.Index?
>>> 
>>>     /// Returns the range where an element appears in `self`
>>>     /// within range `range` for which `lensView(element) == value` or 
>>> `nil` if such an element is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func rangeOf<T : Comparable>(value: T, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (T, T) -> Bool = default, @noescape 
>>> lensView: Base._Element -> T) -> Range<Base.Index>?
>>> 
>>>     /// Returns the number of `element`s that appear in `self`
>>>     /// within range `range` for which `lensView(element) == value` or 
>>> `nil` if such an element is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func countOf<T : Comparable>(value: T, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (T, T) -> Bool = default, @noescape 
>>> lensView: Base._Element -> T) -> Int
>>> 
>>>     internal let _base: Base
>>> }
>>> ```
>>> 
>>> As well as the following extension for `Indexable where Element: 
>>> Comparable`:
>>> 
>>> ```swift
>>> extension BinarySearchView where Base._Element : Comparable {
>>>     /// Returns first index of the first `element` in `self` within range 
>>> `range` for which
>>>     /// `… < element` evaluates to false or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func lowerBoundOf(element: Base._Element, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (Base._Element, Base._Element) -> Bool 
>>> = default) -> Base.Index
>>> 
>>>     /// Returns first index of the first `element` in `self` within range 
>>> `range` for which
>>>     /// `element < …` evaluates to true or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func upperBoundOf(element: Base._Element, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (Base._Element, Base._Element) -> Bool 
>>> = default) -> Base.Index
>>> 
>>>     /// Returns the first index where `element` appears in `self`
>>>     /// within range `range` or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func indexOf(element: Base._Element, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (Base._Element, Base._Element) -> Bool 
>>> = default) -> Base.Index?
>>> 
>>>     /// Returns the range where `element` appears in `self`
>>>     /// within range `range` or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func rangeOf(element: Base._Element, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (Base._Element, Base._Element) -> Bool 
>>> = default) -> Range<Base.Index>?
>>> 
>>>     /// Returns the number of `element`s that appear in `self`
>>>     /// within range `range` or `nil` if `element` is not found.
>>>     ///
>>>     /// - Complexity: O(`log2(self.count)`).
>>>     public func countOf(element: Base._Element, range: Range<Index>? = 
>>> default, @noescape isOrderedBefore: (Base._Element, Base._Element) -> Bool 
>>> = default) -> Int
>>> }
>>> ```
>>> 
>>> These methods would perform a linear scan of the `Indexable` with a TIME 
>>> complexity of `O(log2(self.count))`.
>>> 
>>> An actual implementation of these methods can be found in 
>>> `BinarySearchView.swift`.
>>> 
>>> ### Why `lensView`?
>>> 
>>> Let's assume one had a collection of Persons like this:
>>> 
>>> ```swift
>>> struct Person {
>>>     let name: String
>>>     let age: Int
>>> }
>>> 
>>> let persons: [Person] = [ … ]
>>> ```
>>> 
>>> Now let's assume one wanted to find the first person that's called `John 
>>> Doe`. One could either change `Person` to conform to `Equatable`, allowing 
>>> `indexOf(element:)` to be called on `persons`. Now that was easy, wasn't it?
>>> 
>>> Fine, but what if one wanted to search `Person`s by both their `name` or 
>>> their `age` depending on the given use case? You're out of luck now.
>>> 
>>> Luckily there is a variant of `indexOf(…)` called `indexOf(predicate:)` 
>>> that is available for any type of `Element` regardless of its conformance 
>>> to `Equatable`:
>>> 
>>> ```swift
>>> let searchView = persons.binarySearchView()
>>> let index = persons.indexOf { $0.name == "John Doe" }
>>> ```
>>> 
>>> Unfortunately however such a `predicate` cannot be used when searching for 
>>> an element's property in a sorted(!) collection or a collection of 
>>> non-`Comparable` elements within a TIME complexity of `O(log2(self.count))` 
>>> as binary search uses `<` instead of `==` internally and as the "needle" 
>>> gets passed as both the first (in `indexOf(…)` itself) and second argument 
>>> (in `lowerBoundOf(…)` which gets called by `indexOf(…)`) to the operator, 
>>> one cannot simply change `predicate: (E) -> Bool` to `isOrderedBefore: (V, 
>>> E) -> Bool` as that would result in a type mismatch (`(V, E)` vs. `(E, V)`).
>>> 
>>> The use of `lensView` however allows one to do just that without making a 
>>> mess.
>>> 
>>> ```swift
>>> let searchView = persons.binarySearchView() // sorted!
>>> let index = searchView.indexOf("John Doe") { $0.name }
>>> ```
>>> 
>>> The `lensView` works similarly to how keypaths work in Objective-C, just 
>>> type-safe.
>>> 
>>> ## Impact on existing code
>>> 
>>> The author proposes:
>>> 
>>> 1. Moving `CollectionType.indices` to `Indexable` should not cause any 
>>> issues with existing code.
>>> 
>>> 2. Changing `indexOf(element:)` to `indexOf(element:range:?)` should not 
>>> cause any issues with existing code as the `range` argument is optional and 
>>> the default behaves like it used to.
>>> 
>>> 2. Changing `indexOf(predicate:)` to `indexOf(range:?predicate:)` should 
>>> not cause any issues with existing code either, as the `range` argument is 
>>> optional and the default behaves like it used to. Swift's trailing-closure 
>>> magic allows `indexOf(predicate: …)` to still properly map to 
>>> `indexOf(range: nil, predicate: …)`.
>>> 
>>> 3. The addition of `rangeOf(…)`, `countOf(…)` and `isSorted(…)` to 
>>> `Indexable` do not conflict with any existing API.
>>> 
>>> 4. The introduction of a `BinarySearchView` on `Indexable` does not 
>>> conflict with any existing API.
>>> 
>>> ## Alternatives considered
>>> 
>>> The author's initial approach was to implement all methods on Indexable. 
>>> This however required all methods to either be provided in two variants 
>>> (one for unsorted, one for sorted `Indexable`s):
>>> 
>>> ```swift
>>> final public func indexOf(element: _Element, range: Range<Index>? = nil) -> 
>>> Index? {
>>>     return …
>>> }
>>> 
>>> final public func sortedIndexOf(element: _Element, range: Range<Index>? = 
>>> nil) -> Index? {
>>>     return …
>>> }
>>> ```
>>> 
>>> Or require the introduction of an additional argument:
>>> 
>>> ```swift
>>> final public func indexOf(element: _Element, range: Range<Index>? = nil, 
>>> isSorted: Bool = true) -> Index? {
>>>     if isSorted {
>>>         return …
>>>     } else {
>>>         return …
>>>     }
>>> }
>>> ```
>>> 
>>> It also would have cluttered `Indexable` with methods such as 
>>> `lowerBoundOf` and `upperBoundOf`, which are only ever applicable when 
>>> `self` is sorted accordingly. The introduction of a dedicated 
>>> BinarySearchView fixes these issues.
>>> 
>>> One also would have had to switch from `predicate: (T) -> Bool` to 
>>> `isOrderedBefore: (T, T) -> Bool` for all methods for the second unified 
>>> API alternative.
>> 
> 
>  _______________________________________________
> swift-evolution mailing list
> [email protected] <mailto:[email protected]>
> https://lists.swift.org/mailman/listinfo/swift-evolution 
> <https://lists.swift.org/mailman/listinfo/swift-evolution>
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to