-
Notifications
You must be signed in to change notification settings - Fork 441
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
Add reductions(_:_:) #46
Conversation
I think you should make this an eager/lazy pair of methods. The eager version returns an But the important part is that this facility cannot be a collection, only a sequence, since dereferencing imposes an O(n) time penalty. |
I was under the impression that others don't include the initial value. I think C++ has the ability to do either with an inclusive and exclusive variants – but I'm not a C++ person, I was just going on what I could make of their documentation. Most of my uses in stuff like the advent of code puzzles generally require the initial value though. In drafting this, I had another function which used
Yep, after @CTMacUser's comment I went and took a look at the implementation. I got stuck with |
Edit: I accidentally deleted my previous comment, but thankfully you quoted the entire thing 🙂
From what I can see, the exclusive variant takes an explicit initial value which is included in the result, and the inclusive variant uses the first element of the iterator as the initial value, which is then also included in the result. The initial value is included in the result either way, and the functions I checked in other languages seem to behave in the same way.
It seems reasonable to me to have a variant of [1, 2, 3].reductions(+) // [1, 3, 6]
Speaking of reduce, should
Without |
The reactive version of |
Apparently ReactiveX does not output the seed either, except for RxJava 😅 But I have yet to come across a non-reactive scan that does not output the seed. |
For a non-reactive stream, I don't think it makes sense to discard the seed because it's so easy to skip by calling Defining Off-topic: For reactive streams if the result is also a stream of events, it probably makes sense to have the result of |
These latest commits improve the implementation of the In its current state the API looks like the following: [1, 2, 3].reductions(excluding: 10, +) // [11, 13, 16]
[1, 2, 3].reductions(including: 10, +) // [10, 11, 13, 16]
[1, 2, 3].reductions(+) // [1, 3, 6] I quite like @pyrtsa's point that if reductions included the provided initial value by default and the first API was removed, it is easy to produce the functionality of that removed function by calling [1, 2, 3].reductions(10, +) // [10, 11, 13, 16]
[1, 2, 3].reductions(+) // [1, 3, 6] |
I agree this would be ideal: [1, 2, 3].reductions(10, +) // [10, 11, 13, 16]
[1, 2, 3].reductions(+) // [1, 3, 6] |
And potentially: [1, 2, 3].reductions(into: 10, +=) // [10, 11, 13, 16]
|
Yup! I was trying to summarise the comments so far and I failed, but I agree this should be added too. 🙂 |
While it might be nice for symmetry, I don't see the eager The lazy version might be good though (because iterating over it could benefit from copy-on-write), and maybe the eager one indeed just for symmetry. |
The eager one can still have ergonomic advantages! Just no performance ones 🙂 |
Eager versions can also use throwing closures. |
let a = [1, 2, 3, 4].lazy.prefix(2).map { $0 }
let b = [1, 2, 3, 4].lazy.reductions(0, +).map { $0 }
print(type(of: a))
// LazyMapSequence<Slice<LazySequence<Array<Int>>>, Int>
print(type(of: b))
// Array<Int> The current implementation loses the "laziness" information requiring one to apply |
Thanks @odmir, the conditional conformances to LazySequenceProtocol and LazyCollectionProtocol was missing. Using your example now yields the following. 🙂 let a = [1, 2, 3, 4].lazy.prefix(2).map { $0 }
let b = [1, 2, 3, 4].lazy.reductions(0, +).map { $0 }
print(type(of: a))
// LazyMapSequence<Slice<LazySequence<Array<Int>>>, Int>
print(type(of: b))
// LazyMapSequence<Reductions<Int, LazySequence<Array<Int>>>, Int> |
Sources/Algorithms/Reductions.swift
Outdated
public func reductions( | ||
_ transform: (Element, Element) throws -> Element | ||
) rethrows -> [Element] { | ||
|
||
guard let first = first else { return [] } | ||
return try dropFirst().reductions(first, transform) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to add this to Sequence
without having to duplicate any logic, you could take the iterator, call next
on it to get the seed, and then turn the iterator back into a sequence using IteratorSequence
(which hopefully optimizes well).
I'm writing the lazy version of reductions and hit a question with the empty case if you fine people could guide me. 🙂 Given that |
Of course that's the correct way of doing it, sorry for the noise! 🤦 |
Yes, and this appears to be how it behaves in other languages too. The version that takes an initial value would always have one more element in the result than are in the input collection, and the version that doesn’t, wouldn’t. |
@odmir: No noise at all, I appreciate you raising the concern! 🙂 |
Thanks for confirming that @xwu! |
7cbff5c
to
b8bd047
Compare
Would it be worth adding a |
Wow, I just remembered that I had this in flight. I moved house and have been really busy with that so haven't had much time to pursue this stuff. Does anyone have any thoughts about @benlings' thought to include |
+1 from me — I actually stumbled upon this PR by searching around for “scan.” Thanks for driving — and following up on — this PR, @danielctull! 🙏🏽 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's nice to see this getting close to the finish line! I've left some more feedback. It would also be great to mark public methods with @inlinable
and internal methods with @usableFromInline
like we do in the rest of the package.
Let's indeed also add a deprecated scan
method for discoverability purposes — we do not currently have a rigorous policy in place on when we want to add deprecated methods with names from prior art, but in this case scan
is such a widely known name for this operation that it's worthwhile doing this.
Sources/Algorithms/Reductions.swift
Outdated
extension Sequence { | ||
|
||
public func reductions<Result>( | ||
_ initial: Result, | ||
_ transform: (Result, Element) throws -> Result | ||
) rethrows -> [Result] { | ||
|
||
var result = initial | ||
return try reductions(into: &result) { result, element in | ||
result = try transform(result, element) | ||
} | ||
} | ||
|
||
public func reductions<Result>( | ||
into initial: inout Result, | ||
_ transform: (inout Result, Element) throws -> Void | ||
) rethrows -> [Result] { | ||
|
||
var output = [Result]() | ||
output.reserveCapacity(underestimatedCount + 1) | ||
output.append(initial) | ||
|
||
for element in self { | ||
try transform(&initial, element) | ||
output.append(initial) | ||
} | ||
|
||
return output | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These still need documentation, you can probably reiterate parts of the guide here. It's also a good idea to have a look at the reduce
doc comments, since they're so related.
Sources/Algorithms/Reductions.swift
Outdated
enum Representation { | ||
case start | ||
case base(index: BaseIndex, result: Result) | ||
case end | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've done some experiments and it doesn't seem like we need the start
case at all, we can instead represent the start index by wrapping base.startIndex
in the base
case. Then you should be able to pretty much remove any code that currently uses .start
— let me know if you run into any problems doing that.
Sources/Algorithms/Reductions.swift
Outdated
extension LazySequenceProtocol { | ||
|
||
public func reductions( | ||
_ transform: @escaping (Element, Element) -> Element | ||
) -> InclusiveReductions<Self> { | ||
InclusiveReductions(base: self, transform: transform) | ||
} | ||
} | ||
|
||
extension Sequence { | ||
public func reductions( | ||
_ transform: (Element, Element) throws -> Element | ||
) rethrows -> [Element] { | ||
var iterator = makeIterator() | ||
guard let initial = iterator.next() else { return [] } | ||
return try IteratorSequence(iterator).reductions(initial, transform) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are missing documentation as well. Might also be worth pointing out the relation to the version of reductions
that takes an initial result.
Cheers for those @timvermeulen! Will look at them over the next day. 😊 |
b8bd047
to
6b01737
Compare
This will allow for future changes to either implementation without causing a breakage for the other.
Hey @danielctull, this is to let you know we'll soon be landing this addition and making the minor changes necessary in order to ship this in the next release. 🚀 Thank you for all your work on this, and feel free to make any changes you still want to make in the meantime. |
Cheers for the prompt @timvermeulen, I actually have some time tonight to get the rest of the modifications and documentation changes made that you suggested if that's alright? |
Totally! |
The doc comments are there, but I know they need some re-working. The guide will also needs some tweaking to align with these revised comments. I'm planning to look at these tomorrow morning British time. 🙂 @timvermeulen: I'm personally not convinced about removing |
@swift-ci please test |
@danielctull The last changes look great! The |
@timvermeulen: Thanks for that. Agreed, I'm all done with the changes for now. :) |
Description
This addition allows the caller to get a sequence of the results of reduce applied to up to and including the element of a sequence.
This adds an implementation for reductions as discussed in issue #25.
Detailed Design
To achieve this, a new sequence type called Reductions is added to the library.
For ease of use, a new method is added to sequence to provide the reductions:
The implementation of
subscript(position:)
callsreduce
each time, so there's likely some improvements to be made around the Collection conformance.Documentation Plan
As the
reductions(_:_:)
method is the main entry point, this is documented with an example of use. TheReductions
sequence type itself is lightly documented, which I believe follows the trend of the current APIs in the library.In the guide, I have also gathered some quotes to show why the name
reductions
was chosen overscan
.Test Plan
Arrays and sequences are used to test each code path, calling
reductions(_:_:)
on each. The empty sequence is also tested to ensure it yields an empty sequence.Source Impact
This is a purely additive change.
Checklist