Skip to content

Commit

Permalink
Cross compile replace Stream with LazyList on Scala 2.13 (#2904)
Browse files Browse the repository at this point in the history
* wip

* wip

* fix LazyList tests

* all core tests passes except OneAnd tailRecM Stacksafety on 2.13

* temporarily disable NoneEmptyStream tailRecM stack safety test

* all tests passes, it seems

* reformat

* more stream => LazyList

* fix format
  • Loading branch information
kailuowang authored Jun 21, 2019
1 parent d822620 commit 69fd74e
Show file tree
Hide file tree
Showing 25 changed files with 475 additions and 199 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jdk:
- oraclejdk8

scala_version_211: &scala_version_211 2.11.12
scala_version_212: &scala_version_212 2.12.7
scala_version_212: &scala_version_212 2.12.8
scala_version_213: &scala_version_213 2.13.0

before_install:
Expand Down
34 changes: 20 additions & 14 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ val isTravisBuild = settingKey[Boolean]("Flag indicating whether the current bui
val crossScalaVersionsFromTravis = settingKey[Seq[String]]("Scala versions set in .travis.yml as scala_version_XXX")
isTravisBuild in Global := sys.env.get("TRAVIS").isDefined

val scalatestVersion = "3.1.0-SNAP13"

val scalatestplusScalaCheckVersion = "1.0.0-SNAP8"

val scalaCheckVersion = "1.14.0"

val disciplineVersion = "0.12.0-M3"

val kindProjectorVersion = "0.10.3"

crossScalaVersionsFromTravis in Global := {
val manifest = (baseDirectory in ThisBuild).value / ".travis.yml"
import collection.JavaConverters._
Expand All @@ -40,7 +50,8 @@ crossScalaVersionsFromTravis in Global := {

def scalaVersionSpecificFolders(srcName: String, srcBaseDir: java.io.File, scalaVersion: String) = {
def extraDirs(suffix: String) =
CrossType.Pure.sharedSrcDir(srcBaseDir, "main").toList.map(f => file(f.getPath + suffix))
List(CrossType.Pure, CrossType.Full)
.flatMap(_.sharedSrcDir(srcBaseDir, srcName).toList.map(f => file(f.getPath + suffix)))
CrossVersion.partialVersion(scalaVersion) match {
case Some((2, y)) if y <= 12 =>
extraDirs("-2.12-")
Expand All @@ -49,9 +60,12 @@ def scalaVersionSpecificFolders(srcName: String, srcBaseDir: java.io.File, scala
case _ => Nil
}
}

lazy val commonSettings = Seq(
lazy val commonScalaVersionSettings = Seq(
crossScalaVersions := (crossScalaVersionsFromTravis in Global).value,
scalaVersion := crossScalaVersions.value.find(_.contains("2.12")).get
)

lazy val commonSettings = commonScalaVersionSettings ++ Seq(
scalacOptions ++= commonScalacOptions(scalaVersion.value),
Compile / unmanagedSourceDirectories ++= scalaVersionSpecificFolders("main", baseDirectory.value, scalaVersion.value),
Test / unmanagedSourceDirectories ++= scalaVersionSpecificFolders("test", baseDirectory.value, scalaVersion.value),
Expand Down Expand Up @@ -83,7 +97,7 @@ lazy val catsSettings = Seq(
incOptions := incOptions.value.withLogRecompileOnMacro(false),
libraryDependencies ++= Seq(
"org.typelevel" %%% "machinist" % "0.6.8",
compilerPlugin("org.typelevel" %% "kind-projector" % "0.10.3")
compilerPlugin("org.typelevel" %% "kind-projector" % kindProjectorVersion)
) ++ macroDependencies(scalaVersion.value),
) ++ commonSettings ++ publishSettings ++ scoverageSettings ++ simulacrumSettings

Expand Down Expand Up @@ -150,14 +164,6 @@ lazy val includeGeneratedSrc: Setting[_] = {
}
}

val scalatestVersion = "3.1.0-SNAP13"

val scalatestplusScalaCheckVersion = "1.0.0-SNAP8"

val scalaCheckVersion = "1.14.0"

val disciplineVersion = "0.12.0-M3"

lazy val disciplineDependencies = Seq(
libraryDependencies ++= Seq("org.scalacheck" %%% "scalacheck" % scalaCheckVersion,
"org.typelevel" %%% "discipline-core" % disciplineVersion)
Expand Down Expand Up @@ -632,8 +638,8 @@ lazy val binCompatTest = project
.disablePlugins(CoursierPlugin)
.settings(noPublishSettings)
.settings(
crossScalaVersions := (crossScalaVersionsFromTravis in Global).value,
addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.9"),
commonScalaVersionSettings,
addCompilerPlugin("org.typelevel" %% "kind-projector" % kindProjectorVersion),
libraryDependencies ++= List(
{
if (priorTo2_13(scalaVersion.value))
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/scala-2.12-/cats/compat/lazyList.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cats.compat

object lazyList {

def toLazyList[A](traversableOnce: TraversableOnce[A]): Stream[A] = traversableOnce.toStream

def lazyListString: String = "Stream"

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import cats.syntax.show._

import scala.annotation.tailrec

/**
* For cross compile with backward compatibility
*/
trait LazyListInstances extends StreamInstances with StreamInstancesBinCompat0 {
val catsStdInstancesForLazyList = catsStdInstancesForStream
}

trait StreamInstances extends cats.kernel.instances.StreamInstances {

implicit val catsStdInstancesForStream
: Traverse[Stream] with Alternative[Stream] with Monad[Stream] with CoflatMap[Stream] =
new Traverse[Stream] with Alternative[Stream] with Monad[Stream] with CoflatMap[Stream] {
Expand Down Expand Up @@ -155,6 +163,7 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances {
new Show[Stream[A]] {
def show(fa: Stream[A]): String = if (fa.isEmpty) "Stream()" else s"Stream(${fa.head.show}, ?)"
}

}

trait StreamInstancesBinCompat0 {
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala-2.13+/cats/compat/lazyList.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cats.compat

object lazyList {
def toLazyList[A](io: IterableOnce[A]): LazyList[A] = LazyList.from(io)

def lazyListString: String = "LazyList"
}
206 changes: 206 additions & 0 deletions core/src/main/scala-2.13+/cats/instances/lazyList.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package cats
package instances
import cats.kernel
import cats.syntax.show._

import scala.annotation.tailrec

//For cross compile with backward compatibility
trait StreamInstancesBinCompat0

//For cross compile with backward compatibility
trait StreamInstances extends LazyListInstances {
val catsStdInstancesForStream = catsStdInstancesForLazyList
}

trait LazyListInstances extends cats.kernel.instances.StreamInstances {
implicit val catsStdInstancesForLazyList
: Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] =
new Traverse[LazyList] with Alternative[LazyList] with Monad[LazyList] with CoflatMap[LazyList] {

def empty[A]: LazyList[A] = LazyList.empty

def combineK[A](x: LazyList[A], y: LazyList[A]): LazyList[A] = x lazyAppendedAll y

def pure[A](x: A): LazyList[A] = LazyList(x)

override def map[A, B](fa: LazyList[A])(f: A => B): LazyList[B] =
fa.map(f)

def flatMap[A, B](fa: LazyList[A])(f: A => LazyList[B]): LazyList[B] =
fa.flatMap(f)

override def map2[A, B, Z](fa: LazyList[A], fb: LazyList[B])(f: (A, B) => Z): LazyList[Z] =
if (fb.isEmpty) LazyList.empty // do O(1) work if fb is empty
else fa.flatMap(a => fb.map(b => f(a, b))) // already O(1) if fa is empty

override def map2Eval[A, B, Z](fa: LazyList[A], fb: Eval[LazyList[B]])(f: (A, B) => Z): Eval[LazyList[Z]] =
if (fa.isEmpty) Eval.now(LazyList.empty) // no need to evaluate fb
else fb.map(fb => map2(fa, fb)(f))

def coflatMap[A, B](fa: LazyList[A])(f: LazyList[A] => B): LazyList[B] =
fa.tails.to(LazyList).init.map(f)

def foldLeft[A, B](fa: LazyList[A], b: B)(f: (B, A) => B): B =
fa.foldLeft(b)(f)

def foldRight[A, B](fa: LazyList[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
Now(fa).flatMap { s =>
// Note that we don't use pattern matching to deconstruct the
// stream, since that would needlessly force the tail.
if (s.isEmpty) lb else f(s.head, Eval.defer(foldRight(s.tail, lb)(f)))
}

override def foldMap[A, B](fa: LazyList[A])(f: A => B)(implicit B: Monoid[B]): B =
B.combineAll(fa.iterator.map(f))

def traverse[G[_], A, B](fa: LazyList[A])(f: A => G[B])(implicit G: Applicative[G]): G[LazyList[B]] =
// We use foldRight to avoid possible stack overflows. Since
// we don't want to return a Eval[_] instance, we call .value
// at the end.
foldRight(fa, Always(G.pure(LazyList.empty[B]))) { (a, lgsb) =>
G.map2Eval(f(a), lgsb)(_ #:: _)
}.value

override def mapWithIndex[A, B](fa: LazyList[A])(f: (A, Int) => B): LazyList[B] =
fa.zipWithIndex.map(ai => f(ai._1, ai._2))

override def zipWithIndex[A](fa: LazyList[A]): LazyList[(A, Int)] =
fa.zipWithIndex

def tailRecM[A, B](a: A)(fn: A => LazyList[Either[A, B]]): LazyList[B] = {
val it: Iterator[B] = new Iterator[B] {
var stack: List[Iterator[Either[A, B]]] = Nil
var state: Either[A, Option[B]] = Left(a)

@tailrec
def advance(): Unit = stack match {
case head :: tail =>
if (head.hasNext) {
head.next match {
case Right(b) =>
state = Right(Some(b))
case Left(a) =>
val nextFront = fn(a).iterator
stack = nextFront :: stack
advance()
}
}
else {
stack = tail
advance()
}
case Nil =>
state = Right(None)
}

@tailrec
def hasNext: Boolean = state match {
case Left(a) =>
// this is the first run
stack = fn(a).iterator :: Nil
advance()
hasNext
case Right(o) =>
o.isDefined
}

@tailrec
def next(): B = state match {
case Left(a) =>
// this is the first run
stack = fn(a).iterator :: Nil
advance()
next()
case Right(o) =>
val b = o.get
advance()
b
}
}

LazyList.from(it)
}

override def exists[A](fa: LazyList[A])(p: A => Boolean): Boolean =
fa.exists(p)

override def forall[A](fa: LazyList[A])(p: A => Boolean): Boolean =
fa.forall(p)

override def get[A](fa: LazyList[A])(idx: Long): Option[A] = {
@tailrec
def go(idx: Long, s: LazyList[A]): Option[A] =
s match {
case h #:: tail =>
if (idx == 0L) Some(h) else go(idx - 1L, tail)
case _ => None
}
if (idx < 0L) None else go(idx, fa)
}

override def isEmpty[A](fa: LazyList[A]): Boolean = fa.isEmpty

override def foldM[G[_], A, B](fa: LazyList[A], z: B)(f: (B, A) => G[B])(implicit G: Monad[G]): G[B] = {
def step(in: (LazyList[A], B)): G[Either[(LazyList[A], B), B]] = {
val (s, b) = in
if (s.isEmpty)
G.pure(Right(b))
else {
G.map(f(b, s.head)) { bnext =>
Left((s.tail, bnext))
}
}
}

G.tailRecM((fa, z))(step)
}

override def fold[A](fa: LazyList[A])(implicit A: Monoid[A]): A = A.combineAll(fa)

override def toList[A](fa: LazyList[A]): List[A] = fa.toList

override def reduceLeftOption[A](fa: LazyList[A])(f: (A, A) => A): Option[A] =
fa.reduceLeftOption(f)

override def find[A](fa: LazyList[A])(f: A => Boolean): Option[A] = fa.find(f)

override def algebra[A]: Monoid[LazyList[A]] = new kernel.instances.StreamMonoid[A]

override def collectFirst[A, B](fa: LazyList[A])(pf: PartialFunction[A, B]): Option[B] = fa.collectFirst(pf)

override def collectFirstSome[A, B](fa: LazyList[A])(f: A => Option[B]): Option[B] =
fa.collectFirst(Function.unlift(f))
}

implicit def catsStdShowForLazyList[A: Show]: Show[LazyList[A]] =
new Show[LazyList[A]] {
def show(fa: LazyList[A]): String = if (fa.isEmpty) "LazyList()" else s"LazyList(${fa.head.show}, ?)"
}

implicit val catsStdTraverseFilterForLazyList: TraverseFilter[LazyList] = new TraverseFilter[LazyList] {
val traverse: Traverse[LazyList] = catsStdInstancesForLazyList

override def mapFilter[A, B](fa: LazyList[A])(f: (A) => Option[B]): LazyList[B] =
fa.collect(Function.unlift(f))

override def filter[A](fa: LazyList[A])(f: (A) => Boolean): LazyList[A] = fa.filter(f)

override def collect[A, B](fa: LazyList[A])(f: PartialFunction[A, B]): LazyList[B] = fa.collect(f)

override def flattenOption[A](fa: LazyList[Option[A]]): LazyList[A] = fa.flatten

def traverseFilter[G[_], A, B](fa: LazyList[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[LazyList[B]] =
fa.foldRight(Eval.now(G.pure(LazyList.empty[B])))(
(x, xse) => G.map2Eval(f(x), xse)((i, o) => i.fold(o)(_ +: o))
)
.value

override def filterA[G[_], A](fa: LazyList[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[LazyList[A]] =
fa.foldRight(Eval.now(G.pure(LazyList.empty[A])))(
(x, xse) => G.map2Eval(f(x), xse)((b, as) => if (b) x +: as else as)
)
.value

}
}
15 changes: 8 additions & 7 deletions core/src/main/scala/cats/data/OneAnd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package data
import scala.annotation.tailrec
import scala.collection.mutable.Builder
import cats.instances.stream._
import kernel.compat.lazyList._

/**
* A data type which represents a single element (head) and some other
Expand Down Expand Up @@ -191,22 +192,22 @@ sealed abstract private[data] class OneAndInstances extends OneAndLowPriority0 {
}

sealed abstract private[data] class OneAndLowPriority4 {
implicit val catsDataComonadForNonEmptyStream: Comonad[OneAnd[Stream, ?]] =
new Comonad[OneAnd[Stream, ?]] {
def coflatMap[A, B](fa: OneAnd[Stream, A])(f: OneAnd[Stream, A] => B): OneAnd[Stream, B] = {
@tailrec def consume(as: Stream[A], buf: Builder[B, Stream[B]]): Stream[B] =
implicit val catsDataComonadForNonEmptyStream: Comonad[OneAnd[LazyList, ?]] =
new Comonad[OneAnd[LazyList, ?]] {
def coflatMap[A, B](fa: OneAnd[LazyList, A])(f: OneAnd[LazyList, A] => B): OneAnd[LazyList, B] = {
@tailrec def consume(as: LazyList[A], buf: Builder[B, LazyList[B]]): LazyList[B] =
if (as.isEmpty) buf.result
else {
val tail = as.tail
consume(tail, buf += f(OneAnd(as.head, tail)))
}
OneAnd(f(fa), consume(fa.tail, Stream.newBuilder))
OneAnd(f(fa), consume(fa.tail, LazyList.newBuilder))
}

def extract[A](fa: OneAnd[Stream, A]): A =
def extract[A](fa: OneAnd[LazyList, A]): A =
fa.head

def map[A, B](fa: OneAnd[Stream, A])(f: A => B): OneAnd[Stream, B] =
def map[A, B](fa: OneAnd[LazyList, A])(f: A => B): OneAnd[LazyList, B] =
fa.map(f)
}
}
Expand Down
Loading

0 comments on commit 69fd74e

Please sign in to comment.