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

Treat Stream and LazyList as different types #2983

Merged
merged 20 commits into from
Aug 22, 2019

Conversation

travisbrown
Copy link
Contributor

@travisbrown travisbrown commented Aug 9, 2019

A quick implementation of the second proposal in #2982.

This became a much deeper fix for #2982 after conversation and a vote in #2991. In the initial version it only removed the aliasing in instances, but now it removes the aliasing altogether.

case Left(a) => Some((None, fn(a).iterator ++ it))
case Right(b) => Some((Some(b), it))
}
else
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is Scalafmt, which doesn't seem to be running for 2.13-specific code?

@codecov-io
Copy link

codecov-io commented Aug 9, 2019

Codecov Report

Merging #2983 into master will decrease coverage by 0.07%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2983      +/-   ##
==========================================
- Coverage   93.83%   93.75%   -0.08%     
==========================================
  Files         369      366       -3     
  Lines        7074     6922     -152     
  Branches      189      179      -10     
==========================================
- Hits         6638     6490     -148     
+ Misses        436      432       -4
Impacted Files Coverage Δ
core/src/main/scala/cats/data/package.scala 88.88% <ø> (-2.03%) ⬇️
core/src/main/scala/cats/data/OneAnd.scala 95.45% <ø> (-0.5%) ⬇️
core/src/main/scala/cats/instances/parallel.scala 100% <ø> (ø) ⬆️
...rc/main/scala/cats/laws/discipline/arbitrary.scala 99.14% <ø> (-0.01%) ⬇️
core/src/main/scala/cats/data/NonEmptyVector.scala 100% <ø> (ø) ⬆️
core/src/main/scala/cats/instances/sortedSet.scala 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 48c8b34...6a1c334. Read the comment docs.

@LukaJCB LukaJCB self-requested a review August 9, 2019 12:52
@@ -0,0 +1,62 @@
package cats

package object instances {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use an abstract class for objects from both scala specific versions to inherit from and remove the duplication?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not without dropping in some lazy vals. Here's a minimization:

package foo

package object bar extends BarPackage
package bar {
  abstract class BarPackage {
    object baz extends Baz
  }
  trait Baz {
    val x = foo.bar.baz
  }
}

Referring to baz will stack overflow.

@@ -209,7 +209,7 @@ sealed abstract private[data] class OneAndLowPriority4 {
fa.head

def map[A, B](fa: OneAnd[LazyList, A])(f: A => B): OneAnd[LazyList, B] =
fa.map(f)
fa.map(f)(crossVersionInstancesForLazyList)
Copy link
Contributor

Choose a reason for hiding this comment

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

so for a user who needs to cross compile, they either have to create this crossVersionInstancesForLazyList themselves or have to write version specific code for every import cats.instances.stream._ they have in the past?

Copy link
Contributor

@kailuowang kailuowang Aug 9, 2019

Choose a reason for hiding this comment

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

I have a suggestion in my comment below.

@kailuowang
Copy link
Contributor

I am bit concerned with the ergonomics for cross compiling users who migrated away from Stream on 2.13.
Now that we have version specific cats.instances and cats.kernel.instances why not provide a
lazyListXStream (or a better name) that simply points to StreamInstances on 2.12- and LazyInstances on 2.13? That way the users mentioned above can simply replace import cats.intances.stream with import cats.instance.lazyListXStream._ in their codebase?

@travisbrown
Copy link
Contributor Author

@kailuowang Personally I think the alias is a bad idea in general. Stream and LazyList are different types with different semantics, and pretending they're the same is likely to cause subtle and annoying problems. I'm not going to use the alias in any code I maintain, and I think it's a mistake to use it here.

I've only included this crossVersionInstancesForLazyList stuff because it doesn't seem like there's any will to make a more complete separation of the types in the implementation of Cats itself. That's fine, I guess—I don't really care that much. I don't think something like this should be included in the public API, but if other people really want a cats.instance.lazyListXStream I can add it.

@kailuowang
Copy link
Contributor

kailuowang commented Aug 9, 2019

If you can make all Stream dependent code version specific, then we don't need the type alias. The approach before this PR was that alias was used as much as possible to support cross compiling except where it can't, such as LazyList instances in cats.core or NonEmptyStream, in which case version specific code was used.
This PR made more Stream dependent code (namely stream instance in kernel) version specific, but still not all of them, hence we still need the alias and the inconsistency.

I'd argue that we either leave as is, or we just go ahead and really make ALL Stream dependent code version specific and remove the need for type alias all together.

Either way, I think the crossVersionInstancesForLazyList and crossVersionEqForLazyList is awkward even as an internal API in Cats.
cats.instance.lazyListXStream would be better. I don't strongly believe it should be public either, but at the very least we should document to let users how to create their own cats.instance.lazyListXStream.

@kailuowang
Copy link
Contributor

One more thing, if we make all Stream dependent code version specific we then add back the stream instances on 2.13 and have them deprecated.

@travisbrown
Copy link
Contributor Author

@kailuowang

This PR made more Stream dependent code (namely stream instance in kernel) version specific, but still not all of them, hence we still need the alias and the inconsistency.

My goal was to remove any public conflation of the two types. My position is that I think conflating the two is a mistake, but as long as Cats doesn't force that decision on me I don't really care about the implementation.

I'd argue that we either leave as is, or we just go ahead and really make ALL Stream dependent code version specific and remove the need for type alias all together.

How should we decide on this?

@kailuowang
Copy link
Contributor

kailuowang commented Aug 15, 2019

But there is still public conflation (e.g. ZipStream and parallel instance for stream. ), There doesn't seem to be a lot left with the changes from this PR, completely removing the conflation with version specific code seems a reasonable effort.
Adding stream depending API deprecated back to 2.13 also becomes possible.
The trade off here is code duplication, hard to discover code duplication.

We can hold a vote among the maintainers, because if we chose duplication it will be maintainers burden.

@travisbrown
Copy link
Contributor Author

Okay! How do we call for a vote? I'm happy to make the remaining changes if we decide to go that route.

@kailuowang
Copy link
Contributor

Issue created for vote
#2991

@@ -523,6 +523,7 @@ lazy val kernelLaws = crossProject(JSPlatform, JVMPlatform)
.settings(scoverageSettings)
.settings(disciplineDependencies)
.settings(testingDependencies)
.settings(scalacOptions in Test := (scalacOptions in Test).value.filter(_ != "-Xfatal-warnings"))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is necessary because we now have deprecation warnings for Stream on 2.13.

kubukoz
kubukoz previously approved these changes Aug 20, 2019
type NonEmptyStream[A] = OneAnd[Stream, A]

@deprecated("2.0.0-RC2", "Use NonEmptyLazyList")
def NonEmptyStream[A](head: A, tail: Stream[A]): NonEmptyStream[A] =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that we can't have the default argument here that we have on 2.11 and 2.12, since it results in a deprecation warning (which is clearly a compiler bug).

@travisbrown
Copy link
Contributor Author

travisbrown commented Aug 20, 2019

This PR is becoming a nightmare, but I think it's ready for review.

Some notes:

  • All conflation of Stream and LazyList is removed (no more type alias, toLazyList helpers, etc.).
  • All Stream instances are still available on 2.13, but are deprecated.
  • The cats.instances.stream object itself is also deprecated, but cats.kernel.instances.stream is not, because it's a package object, and package objects can't be deprecated (this is a particularly fun interaction of our pervasive inconsistency and general Scala brokenness).
  • All Stream-related tests are still around on 2.13.
  • Everything that exists for Stream (instances, tests, NonEmptyStream, etc.) has a LazyList equivalent on 2.13.
  • In some cases 2.11 bincompat requires a horrifying amount of code duplication (e.g. if we want to deprecate catsStdParallelForZipStream I believe we have to duplicate most of parallel.scala).
  • Comparison and equality for SortedSet was previously implemented via conversions to Stream / LazyList. I've reimplemented them in terms of StaticMethods.iteratorCompare / iteratorEq, which is equivalent and in my view a little clearer, anyway (and definitely more efficient).

There is one difference between Stream-related APIs on 2.13 and earlier versions: the NonEmptyStream constructor no longer includes a default argument for tail:

@deprecated("2.0.0-RC2", "Use NonEmptyLazyList")
   def NonEmptyStream[A](head: A, tail: Stream[A]): NonEmptyStream[A] = ...

While on 2.11-12 we have:

   def NonEmptyStream[A](head: A, tail: Stream[A] = Stream.empty): NonEmptyStream[A] = ...

I removed the default argument on 2.13 because it results in a deprecation warning, which is a compiler bug—it clearly shouldn't warn. We could alternatively leave the default argument and use the silencer scalac plugin to ignore this one (spurious) warning, but that seemed like overkill for a deprecated method where we have no bincompat / source compat obligations.

LukaJCB
LukaJCB previously approved these changes Aug 20, 2019
@travisbrown travisbrown changed the title Remove cats.instances.stream, introduce cats.kernel.instances.lazyList on 2.13 Treat Stream and LazyList as different types Aug 20, 2019
trait LazyListInstances extends StreamInstances with StreamInstancesBinCompat0 {
val catsStdInstancesForLazyList = catsStdInstancesForStream
}
private[instances] trait LazyListInstances
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we don't want two copies of cats/instances/all.scala we do need it.

(I guess you could argue that we have two copies of everything else at this point so we might as well do it here.)

Copy link
Contributor

@kailuowang kailuowang Aug 20, 2019

Choose a reason for hiding this comment

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

didn't you already add version specific copies for cats/instances/package.scala?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but the definition of AllInstances in all.scala also needs to bring in the LazyList instances on 2.13.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah i see. I didn't see you were able to deprecate all the methods in StreamInstance to avoid deprecating the trait.
Since it confused me a bit, I think we might just as well create two copies for it so that hopefully we can get rid off all Stream/LazyList related ScalaVersionSpecific stuff in non test code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I can go ahead and do this.

import cats.kernel.Semigroup
import cats.syntax.either._
import cats.{~>, Applicative, Apply, FlatMap, Functor, Monad, NonEmptyParallel, Parallel}
import kernel.compat.scalaVersionSpecific._
Copy link
Contributor

Choose a reason for hiding this comment

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

In all version specific code (i.e. not being cross compiled), you shouldn't need to import a compat.scalaVersionSpecific._ right? which also means that you shouldn't need the @suppressUnusedImportWarningForScalaVersionSpecific below as well.

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 think you're right. Let me try removing all of these in the version-specific source trees.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In some cases we might still want to use some of this stuff to minimize the diff between version-specific files? Like IterableOnce? I'm not sure—let me try it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching that, @kailuowang—there were only a couple of place we were using IterableOnce or lazyZip in version-specific code. Just pushed a commit removing scalaVersionSpecific in all version-specific code (except where it's defined).

@travisbrown
Copy link
Contributor Author

Okay, after a bunch of slow experiments today, here's what I've figured out about the CI failure:

  • Changing the Travis CI JDK to OpenJDK fixes the issue (see Separate Stream and LazyList #2998).
  • Restarting, clearing the cache, rebasing commits, etc. doesn't fix the issue.
  • I can't reproduce the issue locally (on any JDK or on either Linux or OS X).
  • I can't make any sense of how the changes since 67dc634 (which was green) could cause this.

@kailuowang
Copy link
Contributor

Thanks for the trouble shooting and update.
I vote we just change the jdk in this PR and call it a day.

@travisbrown
Copy link
Contributor Author

@kailuowang Sounds reasonable. I've just pushed the change. I've also dropped the explicit dist config—I'm assuming it was only there because oraclejdk8 isn't available on the default and more recent options.

@LukaJCB
Copy link
Member

LukaJCB commented Aug 21, 2019

Thanks for this monster of an effort 👍

LukaJCB
LukaJCB previously approved these changes Aug 21, 2019
@@ -303,22 +303,6 @@ class ParallelSuite extends CatsSuite with ApplicativeErrorForEitherTest {
}
}

test("ParMap over Stream should be consistent with zip") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I couldn't find the Stream version of this test any more?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, ouch, that's my fault. Fixed now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, not quite, one second…

@@ -343,12 +327,6 @@ class ParallelSuite extends CatsSuite with ApplicativeErrorForEitherTest {
}
}

test("ParTupled of Stream should be consistent with ParMap of Tuple.apply") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I couldn't find the Stream version of this test any more?

@@ -361,12 +339,6 @@ class ParallelSuite extends CatsSuite with ApplicativeErrorForEitherTest {
}
}

test("ParTupled of Stream should be consistent with zip") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I couldn't find the Stream version of this test any more?

@@ -445,8 +417,6 @@ class ParallelSuite extends CatsSuite with ApplicativeErrorForEitherTest {
checkAll("NonEmptyParallel[Vector, ZipVector]",
NonEmptyParallelTests[Vector, ZipVector].nonEmptyParallel[Int, String])
checkAll("NonEmptyParallel[List, ZipList]", NonEmptyParallelTests[List, ZipList].nonEmptyParallel[Int, String])
// Can't test Parallel here, as Applicative[ZipStream].pure doesn't terminate
checkAll("Parallel[Stream, ZipStream]", NonEmptyParallelTests[LazyList, ZipStream].nonEmptyParallel[Int, String])
Copy link
Contributor

Choose a reason for hiding this comment

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

I couldn't find the Stream version of this test any more?

@@ -115,7 +115,7 @@ class RegressionSuite extends CatsSuite {
// shouldn't have ever evaluted validate(8)
checkAndResetCount(3)

LazyList(1, 2, 6, 8).traverse(validate) should ===(Either.left("6 is greater than 5"))
Stream(1, 2, 6, 8).traverse(validate) should ===(Either.left("6 is greater than 5"))
Copy link
Contributor

@kailuowang kailuowang Aug 22, 2019

Choose a reason for hiding this comment

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

I don't know much of these regression tests, but are we sure that LazyList doesn't need them (couldn't find the LazyList version)?

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 omitted these intentionally, but looking more closely I'm surprised the property being tested isn't covered by the Traverse laws, and we probably do want it for LazyList. Will add now.

Copy link
Contributor

@kailuowang kailuowang left a comment

Choose a reason for hiding this comment

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

LGTM thanks again!

@travisbrown
Copy link
Contributor Author

Thanks for the thorough review!

@kubukoz
Copy link
Member

kubukoz commented Aug 22, 2019

it's happening

@travisbrown travisbrown merged commit 8c031e5 into typelevel:master Aug 22, 2019
@travisbrown travisbrown added this to the 2.0-RC2 milestone Aug 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants