-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add IterableOps#tapEach method #7124
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
Conversation
| * @tparam U the return type of f | ||
| * @return The same logical collection as this | ||
| */ | ||
| def tapEach[U](f: A => U): C = fromSpecific(new View.TapEach(this, f)) |
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.
you could potentially optimise this for strict collections by doing
def tapEach[U](f: A => U): C = knownSize match {
case 0 => this
case n if n < 0 => fromSpecific(new View.TapEach(this, f))
case n if n > 0 =>
foreach(f)
thisThere 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 not perfect (e.g. it misses List), but it might be better than nothing?
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 think that this size optimization could result in surprising behaviour if the underlying collection is mutated but lazy. In that case, when the empty collection has elements inserted, and then the lazy collection forces, f will not be called. I do not know if there's actually any collections like this however...
| * @tparam U the return type of f | ||
| * @return The same logical collection as this | ||
| */ | ||
| def tapEach[U](f: A => U): C = fromSpecific(new View.TapEach(this, f)) |
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'm also not sure if we really need a View.TapEach, or can just do map(a => { f(a); a }) instead of fromSpecific(new View.TapEach(this, f))
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.
You're right, I'll change this
|
@NthPortal ready for re-review |
|
I think this will need to be specialised for LazyList, since LazyList overrides |
|
@javax-swing Added some tests here for LazyList#tapEach |
|
The fact that you need a specialization for |
|
I'm wondering if it would be a good idea to have a trait StrictTapEach[A] {
self: IterableOps[A, AnyConstr, _] =>
override def tapEach[U](f: A => U): this.type = {
foreach(f)
this
}
}(if |
Actually I just put the implementation in |
| // Optimization avoids creation of second collection | ||
| override def tapEach[U](f: A => U): C = { | ||
| foreach(f) | ||
| coll |
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.
can't return this?
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.
Unfortunately, StrictOptimizedSeqOps[..., C] does not conform to C. I could play around with introducing a self-type to it, self: C => and see if there are any issues.
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.
Ok, It seems that this solution does solve it, as long as you also enforce the self: C => type on all extending StrictOptimized{Seq,Set,Map,SortedMap, SortedSet}Ops. I'd like to get @julienrf 's opinion though if he forsees any issues with that constraint, maybe it's intended to be extendable by non-C things too
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.
The exceptions used to be StringOps and ArrayOps. All regular collection types extend their C parameter. Can we make any further simplifications if we always enforce this? I don't think it's worth making this change specifically for tapEach. There are plenty of other methods that return C. In most cases you abstract over consuming a collection where this is not an issue. If you also need to produce a more specific collection type you need to use the proper abstraction anyway (unless we can change it in a more generic way).
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.
@szeiger I can't think of any other improvements or simplifications that could be made if it had { self: C => unfortunately.
| // Optimization avoids creation of second collection | ||
| override def tapEach[U](f: A => U): self.type = { | ||
| foreach(f) | ||
| this |
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 think it would be simpler to return coll here (and keep using C as the return type).
2e778c1 to
fc4b230
Compare
| assertEquals(Vector(1, 2), ll.take(2).to(Vector)) | ||
| assertEquals(ListBuffer(1, 2), lb) | ||
| assertEquals(4, ll(3)) | ||
| assertEquals(ListBuffer(1, 2, 3, 4), lb) // this should change to Listbuffer(1,2,4) if LazyList becomes independently lazy |
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.
you can make the LazyList independently lazy by doing LazyList.tabulate(5)(_ + 1)
|
@NthPortal your suggestions are implemented now |
febe3e4 to
562131c
Compare
NthPortal
left a comment
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.
Nice!
|
IMO, this is ready to merge (once the conflict is resolved). @julienrf do you agree? |
|
I think an argument could be made to manually override each strict
collection with an implementation that returns `this.type`, what do you
think?
…On Sun., Sep. 16, 2018, 4:04 p.m. Nth, ***@***.***> wrote:
IMO, this is ready to merge (once the conflict is resolved). @julienrf
<https://github.com/julienrf> do you agree?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#7124 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AE4rVw15m3hYKhERvQEABGS7alNo6Nn2ks5ubq7ggaJpZM4WIzDe>
.
|
|
@joshlemer I'm not sure if I'm behind increasing the maintenance burden by adding a whole bunch of overrides that just call super. If there's a way to do it generically so that you only need at most a few overrides, that sounds good to me, but I'm not sure it's worth it to do it for every strict collection. |
| // Optimization avoids creation of second collection | ||
| override def tapEach[U](f: A => U): C = { | ||
| foreach(f) | ||
| coll |
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.
The exceptions used to be StringOps and ArrayOps. All regular collection types extend their C parameter. Can we make any further simplifications if we always enforce this? I don't think it's worth making this change specifically for tapEach. There are plenty of other methods that return C. In most cases you abstract over consuming a collection where this is not an issue. If you also need to produce a more specific collection type you need to use the proper abstraction anyway (unless we can change it in a more generic way).
|
@szeiger do I read your review correctly as "not convinced yet?" |
|
Is there a reason for this not to be merged? |
|
@NthPortal I don't believe so |
|
ping @szeiger |
szeiger
left a comment
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.
Other than the missing knownSize, this looks good.
| */ | ||
| def inits: Iterator[C] = iterateUntilEmpty(_.init) | ||
|
|
||
| override def tapEach[U](f: A => U): C = fromSpecific(new View.Map(this, { a: A => f(a); a })) |
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.
Could I get others' opinions on whether or not this should be implemented as a call to map instead of fromSpecific(new View.Map)? I think that the former simplifies/reduces the work for those implementing collections which need to override map, but I am curious to hear others' thoughts.
I don't think the decision should hold up the PR - it's an easy change to make in its own PR if needed.
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 think using map is better, indeed.
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.
Unfortunately, calling map { a => f(a); a } will only return a CC[A], rather than the more specific C.
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.
That is interesting and unfortunate. I'm torn on whether or not it would still be better to use map and cast the result. I'm leaning towards not.
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 asked @szeiger to look at this and explain it to me.
Josh's point is that we can't typecast our way out of this. CC and C[A] can be different collection types entirely:
scala 2.13.0-M5> (Map(1 -> "foo"): Iterable[(Int, String)]).map(x => x)
res5: Iterable[(Int, String)] = List((1,foo))
scala 2.13.0-M5> (collection.immutable.BitSet(0, 5, 9): Iterable[Int]).map(_ + 1)
res0: Iterable[Int] = Set(1, 6, 10)
(in an earlier edit, I'd included a String example where I "upcast" it to Iterable[Char], but that's not really the same, because it's an implicit conversion, not an upcast)
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.
@SethTisue Wow! Very sneaky! Some downsides (which may have workarounds):
- filter can (as far as the compiler is concerned) change the length of the
Iterable. So the consequences of this are that, perhaps non-strict Seq's like SeqView and LazyList will have to then go through each element in order to fulfill a call toapply(100), whereas before thefiltercall, they may have been able to independently evaluate/access element100immediately. - The above point means that
IndexedSeqView#filtercannot return anIndexedSeqView
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.
To add on one more point, filter also destroys independent laziness of elements (currently only a thing for LazyList, but could potentially be a thing for some lazy IndexedSeq in the future).
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.
@joshlemer okay, sounds like it’s better as-is then
@NthPortal I don’t follow?
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.
scala> def list = LazyList.tabulate(10)(i => i)
list: scala.collection.immutable.LazyList[Int]
scala> def f(i: Int) = println(i)
f: (i: Int)Unit
scala> list.map(i => {f(i); i})(5)
5
res0: Int = 5
scala> list.filter(i => {f(i); true})(5)
0
1
2
3
4
5
res1: Int = 5There 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 retract all complaints, and I'm happy with fromSpecific(new View.Map(...))
|
ping @joshlemer |
|
Strange compiler failure in the Travis build: https://travis-ci.org/scala/scala/jobs/449897364. I've triggered a new build. |
|
Travis should be ignored for the time being, scala/scala-dev#570 |
|
I see. In this case it just needs to be squashed because the first commit on its own failed. |
|
@joshlemer rebase and squash? |
|
Done! @SethTisue |
|
gah, sorry Josh, needs one more rebase. I tried to use this the other day and was surprised to find it hadn't been merged yet :-) |
Better unit test for Vector.tapEach tapeach returns specific iterable type Add comments in vectortest Fix ViewTest.tapEach Add tapEach to iterableOnce, optimizations for strict collections Add tests for LazyList.tapeach Remove git merge artifacts Fix LazyList.tapEach test Fix LazyList#tapEach, add more and better tests Better tests in IteratorTest.tapEach Use 'locally' in IteratorTest#tapEach Miscellaneous cleanup related to IterableOnce#tapEach Remove copy of scaladoc for tapEach Remove '(typically)' from tapEach scalado override knownSize in IterableOnce#tapEach
This closes scala/bug#11098
This adds
tapEachmethod toIterableOpswhich allows you to write:Or