Skip to content
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

Make LazyList's empty-or-not status lazy too #7000

Merged
merged 2 commits into from
Aug 10, 2018

Conversation

NthPortal
Copy link
Contributor

@NthPortal NthPortal commented Aug 4, 2018

@NthPortal NthPortal added the WIP label Aug 4, 2018
@scala-jenkins scala-jenkins added this to the 2.13.0-M5 milestone Aug 4, 2018
@NthPortal NthPortal changed the title Make LazyList fully lazy [WIP] Make LazyList fully lazy Aug 4, 2018
@NthPortal NthPortal mentioned this pull request Aug 4, 2018
@NthPortal NthPortal changed the title [WIP] Make LazyList fully lazy [WIP] Make LazyList fully lazy [ci: last-only] Aug 4, 2018
@NthPortal
Copy link
Contributor Author

LazyList.empty uses a cached LazyList.State, so it is only evaluated once (at which point it stops being lazy). Thus, whether or not LazyList.empty.toString returns "LazyList(?)" or "LazyList()" is not locally deterministic.

This makes some of the tests non-deterministic.

@julienrf
Copy link
Contributor

julienrf commented Aug 6, 2018

Thanks @NthPortal! I think it would be better to have LazyList.empty to be strictly evaluated. The important thing is to provide the ability to create a LazyList that we don’t know if it is empty or not until we evaluate it (but that doesn’t mean that all LazyList instances should be like that).


private lazy val state: State[A] = {
stateEvaluated = true
val res = lazyState()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the two lines above be swapped (in case evaluating the state throws)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. stateEvaluated is an indicator of whether or not we can touch state without breaking laziness. IMO, even if evaluating state throws, it should be considered evaluated and safe to touch.


lazy val head: A = {
hdEvaluated = true
val res = hd()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the two lines above be swapped (in case evaluating hd throws)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to keep retrying instead of switching from an exception to nulls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ichoran if it throws, hd won't get set to null, so it will keep retrying

@NthPortal
Copy link
Contributor Author

I'm having trouble figuring out why filter_all_foreach_allows_GC is failing

@SethTisue
Copy link
Member

is this only WIPpy because of the failing test, or is there other known WIPpiness here?

@SethTisue SethTisue self-assigned this Aug 7, 2018
@NthPortal
Copy link
Contributor Author

@SethTisue the WIP was because I was still in the process of removing tailDefined (which no longer corresponds to anything), and hadn't had the chance to rewrite addStringNoForce.

... I have now done that, and will push it shortly. And I will get rid of the WIP label :D

@NthPortal NthPortal changed the title [WIP] Make LazyList fully lazy [ci: last-only] Make LazyList fully lazy Aug 7, 2018
@NthPortal NthPortal removed the WIP label Aug 7, 2018
@SethTisue SethTisue added the prio:blocker release blocker (used only by core team, only near release time) label Aug 7, 2018
@SethTisue
Copy link
Member

tentatively marking as an M5 blocker since if we're going make a basic change like this we need to leave folks time to test it

@NthPortal
Copy link
Contributor Author

This is ready for review

@NthPortal
Copy link
Contributor Author

What should we do about methods like drop, dropWhile, and other methods inherited from and optimised for LinearSeqOps (and potentially from SeqOps?). Currently, they are are implemented eagerly, which may help avoid stack overflow problems, but on the other hand, it seems like drop ought to be lazy.

@dwijnand
Copy link
Member

dwijnand commented Aug 7, 2018

JFYI LazyListTest.filter_all_foreach_allows_GC failed again.

@julienrf
Copy link
Contributor

julienrf commented Aug 7, 2018

What should we do about methods like drop, dropWhile, and other methods inherited from and optimized for LinearSeqOps (and potentially from SeqOps?). Currently, they are implemented eagerly

This is a bug then, they should be implemented using the view-based approach.

@javax-swing
Copy link

javax-swing commented Aug 7, 2018

This was introduced in #6608 to stop a stack safety issue. Just my 2 cents, but I think that drop and dropWhile should still use memory sharing.

perhaps something akin to

def drop(n : Int) : LazyList[A] =  {
  newLL {
    super.drop(n).state
  }
}

@NthPortal
Copy link
Contributor Author

(introduced by me no less)

@julienrf
Copy link
Contributor

julienrf commented Aug 8, 2018

Alternatively, we could move the overridden methods from LinearSeqOps to StrictOptimizedLinearSeqOps, since they are optimizations that really only work for strict collections.

Yes, that’s what we should do.

@NthPortal
Copy link
Contributor Author

Does anyone have any idea why filter_all_foreach_allows_GC is failing? I can't reproduce it locally, and it seems like the primary things blocking this PR. withFilter is backed by Stream#filter too, and its tests aren't failing.

@javax-swing
Copy link

javax-swing commented Aug 9, 2018

From looking at the previous implementation, it used to call iterableFactory.filteredTail on the head of the LazyList, this lazily took the value of the head and tail and stuck it into a new LazyList.Cons. This meant that once the function had exited, nothing was holding on to a reference to the head of the LazyList.

However in the new design the lazy State function for the filtered LazyList (for the head) holds a reference to the head of the Unfiltered LazyList. So in order for it to GC the head of the unfiltered LazyList it must first GC the lazyState function, so it could be purely that the GC path is more complicated for the JVM and it just doesn't feel like doing it 😁

Also the lazyState function wraps a byName parameter, which could also be in play here (in terms of GC). It may work locally for you due to differences in memory, cpu and jvm args.

Hope that this is useful or at the very least correct 😁

@lrytz
Copy link
Member

lrytz commented Aug 9, 2018

The filter_all_foreach_allows_GC test fails when the optimizer is enabled, you can reproduce it by running setupValidateTest then testOnly *.LazyListTest in sbt. I suspect this is due to scala/bug#9137, will take a look.

@NthPortal
Copy link
Contributor Author

@lrytz thanks! I think I have fixed it with @noinline, which I will push shortly

Extract Stream into its own file, and inline methods and
implementation into Stream or its companion object.
Remove LazyListOps and LazyListFactory.

Change LazyList to have a lazy state. The state can be either a
cons cell with a lazy head, or an empty state.
@NthPortal
Copy link
Contributor Author

I think everything brought up so far has been taken care of, and all the tests pass. Ready for final review so this can get in by tomorrow.

Copy link
Contributor

@Ichoran Ichoran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a couple small comments, but I think it overall looks good and what really needs to happen is that it gets into the library so it's easier to play with it and figure out if there are any bugs and if it has the right characteristics with regard to laziness etc..


lazy val head: A = {
hdEvaluated = true
val res = hd()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to keep retrying instead of switching from an exception to nulls.

}
}

private class LazyIterator[+A](private[this] var lazyList: LazyList[A]) extends AbstractIterator[A] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we certain this should be private? We seem to rely on it for implementing a fair bit of functionality. How are library users supposed to replicate the functionality if this is private? If they can, why don't we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's specific to LazyList, it's not clear to me why you'd need to replicate the functionality - you can just call .iterator

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I don't feel strongly about it either way

x = LazyList(_, ?)
y = LazyList(_, ?)
x = LazyList(?)
y = LazyList(?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of addString is very hard to review. I guess all the places where you have changed the expected output of the tests are places where we previously knew that the lazy list was not empty and now we don’t know?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it's hard to review (it was also very hard to understand well enough to change to not use tailDefined). The tests were changed either because (a) previously we knew it was not empty, but now we don't, or (b) previously the tail hadn't been evaluated to a cycle, but since the tail is no longer lazy (since the whole thing is lazy), it now knows it's cyclic slightly earlier

val stream = Iterator.iterate(Stream.from(0))(_.dropWhile(_ => false)).drop(10000).next
stream.head // superfluous, because `head` should be eager
stream.tail.head
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove that test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test was added in #6608, which made drop and dropWhile stack-safe but not lazy. Since I changed them back to being lazy, they are no longer stack-safe

@@ -101,12 +101,12 @@ class LazyListTest {

@Test // scala/bug#9134
def filter_map_properly_lazy_in_tail: Unit = {
assertLazyListOpLazyInTail(_.filter(_ % 2 == 0).map(identity), List(1, 2))
assertLazyListOpLazyInTail(_.filter(_ % 2 == 0).map(identity), List(1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this means that when we call filter and map we evaluate no elements of the lazy list, whereas we previously evaluated the first element?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct

@julienrf
Copy link
Contributor

Thanks a lot to all the people who contributed to that work!

@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Aug 10, 2018
@SethTisue SethTisue merged commit a2213ce into scala:2.13.x Aug 10, 2018
@SethTisue
Copy link
Member

🎉

@hrhino
Copy link
Member

hrhino commented Aug 10, 2018

🎉 💯

Thanks for making this actually happen, @NthPortal!

@NthPortal NthPortal deleted the topic/lazyList/PR branch August 10, 2018 21:03
@NthPortal
Copy link
Contributor Author

NthPortal commented Aug 10, 2018

🎉
You're welcome! And thanks to everyone who made this happen, through reviews, prior PRs and discussions!

@SethTisue
Copy link
Member

intermittent LazyListTest.foreach_allows_GC failure

java.lang.AssertionError: null, took 0.364 sec
[error]     at scala.collection.immutable.LazyListTest.assertLazyListOpAllowsGC(LazyListTest.scala:47)
[error]     at scala.collection.immutable.LazyListTest.foreach_allows_GC(LazyListTest.scala:52)
[error]     ...

https://scala-ci.typesafe.com/view/scala-2.13.x/job/scala-2.13.x-integrate-windows/663/consoleFull

@NthPortal
Copy link
Contributor Author

could it be another inlining problem? I can try @noinlineing foreach, but I'm not sure how to tell if it fixes the problem

@NthPortal
Copy link
Contributor Author

@SethTisue is it only a Windows thing? Also, how intermittent?

@SethTisue
Copy link
Member

SethTisue commented Aug 13, 2018

not sure yet on either question. so far I've only seen the one Windows failure, and then the next run was green

@SethTisue
Copy link
Member

followup discussion taking place at scala/bug#11089 on whether this is now too lazy...

@SethTisue SethTisue changed the title Make LazyList fully lazy Make LazyList's empty-or-not status lazy too Aug 22, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
prio:blocker release blocker (used only by core team, only near release time) release-notes worth highlighting in next release notes
Projects
None yet
10 participants