- Proposal: SE-0045
- Author(s): Kevin Ballard
- Status: Review scheduled for April 28...May 2, 2016
- Review manager: Chris Lattner
Add 3 new Sequence
functions scan(_:combine:)
, prefix(while:)
, and
drop(while:)
, with overrides as appropriate on Collection
,
LazySequenceProtocol
, and LazyCollectionProtocol
, as well as a global
function iterate(_:apply:)
.
Swift-evolution thread: Proposal: Add scan, takeWhile, dropWhile, and iterate to the stdlib
The Swift standard library provides many useful sequence manipulators like
dropFirst(_:)
, filter(_:)
, etc. but it's missing a few common methods that
are quite useful.
Add the following extension to Sequence
:
extension Sequence {
/// Returns an array containing the results of
///
/// p.reduce(initial, combine: combine)
///
/// for each prefix `p` of `self` in order from shortest to longest, starting
/// with the empty prefix and ending with `self`.
///
/// For example:
///
/// (1..<6).scan(0, combine: +) // [0, 1, 3, 6, 10, 15]
///
/// - Complexity: O(N)
func scan<T>(initial: T, @noescape combine: (T, Self.Generator.Element) throws -> T) rethrows -> [T]
}
Modify the declaration of Sequence
with two new members:
protocol Sequence {
// ...
/// Returns a subsequence by skipping elements while `predicate` returns
/// `true` and returning the remainder.
func drop(@noescape while predicate: (Self.Generator.Element) throws -> Bool) rethrows -> Self.SubSequence
/// Returns a subsequence containing the initial elements until `predicate`
/// returns `false` and skipping the remainder.
func prefix(@noescape while predicate: (Self.Generator.Element) throws -> Bool) rethrows -> Self.SubSequence
}
Also provide default implementations on Sequence
that return AnySequence
,
and default implementations on Collection
that return a slice.
LazySequenceProtocol
and LazyCollectionProtocol
will also be extended with
implementations of scan(_:combine:)
, drop(while:)
, and prefix(while:)
that return lazy sequence/collection types. Like the lazy filter(_:)
,
drop(while:)
will perform the filtering when startIndex
is accessed.
Add a global function:
/// Returns an infinite sequence of lazy applications of `apply` to the
/// previous value. For example:
///
/// iterate(1, apply: { $0 * 2 }) // yields: 1, 2, 4, 8, 16, 32, 64, ...
func iterate<T>(initial: T, apply: T -> T) -> IterateSequence<T>
In addition to the above declarations, provide default implementations based on
AnySequence
, similarly to how functions like dropFirst(_:)
and prefix(_:)
are handled:
extension SequenceType {
func drop(@noescape while predicate: (Self.Generator.Element) throws -> Bool) rethrows -> AnySequence<Self.Generator.Element>
func prefix(@noescape while predicate: (Self.Generator.Element) throws -> Bool) rethrows -> AnySequence<Self.Generator.Element>
}
These default implementations produce an AnySequence
that wraps an Array
(as the functions must be implemented eagerly to match expected behavior).
Provide default implementations on Collection
as well:
extension Collection {
func drop(@noescape while predicate: (Self.Generator.Element) throws -> Bool) rethrows -> Self.SubSequence
func prefix(@noescape while predicate: (Self.Generator.Element) throws -> Bool) rethrows -> Self.SubSequence
}
Extend LazySequenceProtocol
with lazy versions of the functions:
extension LazySequenceType {
func scan<T>(initial: T, combine: (T, Self.Generator.Element) -> T) -> LazyScanSequence<Self.Elements, T>
func drop(while predicate: (Self.Generator.Element) -> Bool) -> LazyDropWhileSequence<Self.Elements>
func prefix(while predicate: (Self.Generator.Element) -> Bool) -> LazyPrefixWhileSequence<Self.Elements>
}
The types LazyScanSequence
, LazyDropWhileSequence
, and
LazyPrefixWhileSequence
all conform to LazySequenceProtocol
.
Extend LazyCollectionProtocol
with collection variants for the functions:
extension LazyCollectionType {
func scan<T>(initial: T, combine: (T, Self.Generator.Element) -> T) -> LazyScanCollection<Self.Elements, T>
func drop(while predicate: (Self.Generator.Element) -> Bool) -> LazyDropWhileCollection<Self.Elements>
func prefix(while predicate: (Self.Generator.Element) -> Bool) -> LazyPrefixWhileCollection<Self.Elements>
}
The types LazyScanCollection
, LazyDropWhileCollection
, and
LazyPrefixWhileCollection
conform to LazyCollectionProtocol
.
The type IterateSequence
from the function iterate(_:apply:)
conforms to
Sequence
.
None, this feature is purely additive.
The names here are likely to cause some bikeshedding. Here are some alternatives I've heard proposed:
suffixFrom(firstElementSatisfying:)
instead ofdrop(while:)
– Not only is it rather long, it's also focusing on taking a suffix while the actual expected usage of the function is focused around skipping elements at the start. There's also the potential confusion around whether it's the first element from the beginning or the first element from the end (since the term "suffix" implies working from the end backwards).skip(while:)
instead ofdrop(while:)
– I'm actually partial to this one, but we'd need to renamedropFirst(_:)
as well. The benefit of this is it removes the potential confusion around whether the method is mutating.take(while:)
instead ofprefix(while:)
– This was actually the original name proposed, and it matches precedent from other languages, but I eventually decided that consistency withprefix(_:)
was desired. However, there is an argument to be made thatprefix(while:)
is using the term "prefix" like a verb instead of a noun, and the verb form means something different.prefix(to:)
instead ofprefix(while:)
– The name here doesn't make it obvious that the argument is a predicate, and this also requires inverting the meaning of the predicate which I don't like. The focus of this function is on retaining the initial elements that have a desired characteristic, which suggests that the predicate should describe the characteristic the desired elements have, not the inverse.prefix(having:)
instead ofprefix(while:)
– Reasonable. I choseprefix(while:)
for consistency withdrop(while:)
butprefix(having:)
makes more grammatical sense (since we're using the noun meaning of prefix rather than the verb meaning).reducedPrefixes(_:combine:)
instead ofscan(_:combine:)
– Seems somewhat awkward.
I haven't heard any alternative suggestions for iterate(_:apply:)
.
This function is really a special case of calling scan(_:combine:)
on an
infinite sequence containing the combine
closure repeated forever, but that's
rather awkward and not at all obvious. As an example, the expression
seq.scan(0, combine: { $0 * 2 })
, assuming a function repeatedForever(_:)
,
would look like repeatedForever({ $0 * 2 }).lazy.scan(0, combine: { $1($0) })
.
And even there, the function repeatedForever(_:)
could easily be argued to
actually just be a special case of CollectionOfOne(_:).cycle()
, assuming a
method .cycle()
that repeats a sequence forever. So that turns into
CollectionOfOne({ $0 * 2 }).cycle().lazy.scan(0, combine: { $1($0) })
. Very
awkward and not something you'd really expect anyone to write on their own. But
the pattern expressed by this function actually turns up with reasonable
frequency when doing FP-style processing of sequences, so I think it's
reasonable to have a function to express it.
One FP function that I did not propose, but could be quite useful, is
unfold(_:apply:)
. This is a the dual of reduce(_:combine:)
. It takes an
initial value and a function, and uses it to build a list. The function
signature would look like
func unfold<T,State>(initial: State, apply: State -> (T, State)?) -> UnfoldSequence<T>
The sequence would return each successive initial value from the apply
closure
until it returns nil
. For example:
unfold(10, apply: { $0 == 0 ? nil : ($0,$0-1) })
// returns: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
With this function, iterate(initial, apply: f)
is actually a special case of
unfold(initial, { ($0, f($0)) })
, except that this version of it performs the
closure one step in advance. A more faithful version would look like
unfold({ initial }, { let x = $0(); return (x, { f(x) }) })
, which is rather
awkward and requires creating a bunch of intermediate closures (which suggests
that it wouldn't be as efficient as iterate(_:apply:)
is).
I did not propose this function because I believe it to be more esoteric and less useful than the ones I did propose, and more likely to be rejected by the community. Just because an FP function exists doesn't mean the Swift stdlib has to include it. But I've put it in this Alternatives section because it is related to the functions I am proposing.