diff --git a/build.sbt b/build.sbt index f2b07c927e..bd2d483698 100644 --- a/build.sbt +++ b/build.sbt @@ -175,8 +175,8 @@ lazy val catsJVM = project.in(file(".catsJVM")) .settings(moduleName := "cats") .settings(catsSettings) .settings(commonJvmSettings) - .aggregate(macrosJVM, kernelJVM, kernelLawsJVM, coreJVM, lawsJVM, freeJVM, testsJVM, docs, bench) - .dependsOn(macrosJVM, kernelJVM, kernelLawsJVM, coreJVM, lawsJVM, freeJVM, testsJVM % "test-internal -> test", bench % "compile-internal;test-internal -> test") + .aggregate(macrosJVM, kernelJVM, kernelLawsJVM, coreJVM, lawsJVM, freeJVM, testsJVM, jvm, docs, bench) + .dependsOn(macrosJVM, kernelJVM, kernelLawsJVM, coreJVM, lawsJVM, freeJVM, testsJVM % "test-internal -> test", jvm, bench % "compile-internal;test-internal -> test") lazy val catsJS = project.in(file(".catsJS")) .settings(moduleName := "cats") @@ -290,6 +290,13 @@ lazy val js = project .settings(commonJsSettings:_*) .enablePlugins(ScalaJSPlugin) +// cats-jvm is JVM-only +lazy val jvm = project + .dependsOn(macrosJVM, coreJVM, testsJVM % "test-internal -> test") + .settings(moduleName := "cats-jvm") + .settings(catsSettings:_*) + .settings(commonJvmSettings:_*) + lazy val publishSettings = Seq( homepage := Some(url("https://github.com/typelevel/cats")), licenses := Seq("MIT" -> url("http://opensource.org/licenses/MIT")), @@ -358,7 +365,7 @@ lazy val publishSettings = Seq( ) ++ credentialSettings ++ sharedPublishSettings ++ sharedReleaseProcess // These aliases serialise the build for the benefit of Travis-CI. -addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;kernelLawsJVM/compile;lawsJVM/compile;freeJVM/compile;kernelLawsJVM/test;coreJVM/test;testsJVM/test;freeJVM/test;bench/test") +addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;kernelLawsJVM/compile;lawsJVM/compile;freeJVM/compile;kernelLawsJVM/test;coreJVM/test;testsJVM/test;freeJVM/test;jvm/test;bench/test") addCommandAlias("validateJVM", ";scalastyle;buildJVM;makeSite") diff --git a/core/src/main/scala/cats/instances/future.scala b/core/src/main/scala/cats/instances/future.scala index ba2490ee01..a7d444acf1 100644 --- a/core/src/main/scala/cats/instances/future.scala +++ b/core/src/main/scala/cats/instances/future.scala @@ -8,12 +8,22 @@ import scala.concurrent.{ExecutionContext, Future} trait FutureInstances extends FutureInstances1 { - implicit def catsStdInstancesForFuture(implicit ec: ExecutionContext): MonadError[Future, Throwable] with CoflatMap[Future] = - new FutureCoflatMap with MonadError[Future, Throwable]{ + implicit def catsStdInstancesForFuture(implicit ec: ExecutionContext): MonadError[Future, Throwable] with CoflatMap[Future] with MonadRec[Future] = + new FutureCoflatMap with MonadError[Future, Throwable] with MonadRec[Future] { def pure[A](x: A): Future[A] = Future.successful(x) def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f) + /** + * Note that while this implementation will not compile with `@tailrec`, + * it is in fact stack-safe. + */ + final def tailRecM[B, C](b: B)(f: B => Future[(B Xor C)]): Future[C] = + f(b).flatMap { + case Xor.Left(b1) => tailRecM(b1)(f) + case Xor.Right(c) => Future.successful(c) + } + def handleErrorWith[A](fea: Future[A])(f: Throwable => Future[A]): Future[A] = fea.recoverWith { case t => f(t) } def raiseError[A](e: Throwable): Future[A] = Future.failed(e) diff --git a/js/src/test/scala/cats/tests/FutureTests.scala b/js/src/test/scala/cats/tests/FutureTests.scala index f007a0c353..c257a67dc8 100644 --- a/js/src/test/scala/cats/tests/FutureTests.scala +++ b/js/src/test/scala/cats/tests/FutureTests.scala @@ -47,4 +47,5 @@ class FutureTests extends CatsSuite { checkAll("Future[Int]", MonadErrorTests[Future, Throwable].monadError[Int, Int, Int]) checkAll("Future[Int]", ComonadTests[Future].comonad[Int, Int, Int]) + checkAll("Future", MonadRecTests[Future].monadRec[Int, Int, Int]) } diff --git a/jvm/src/test/scala/cats/tests/FutureTests.scala b/jvm/src/test/scala/cats/tests/FutureTests.scala new file mode 100644 index 0000000000..fd478ad4c0 --- /dev/null +++ b/jvm/src/test/scala/cats/tests/FutureTests.scala @@ -0,0 +1,39 @@ +package cats +package jvm +package tests + +import cats.data.Xor +import cats.laws.discipline._ +import cats.tests.CatsSuite + +import scala.concurrent.{Await, Future} +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary + +class FutureTests extends CatsSuite { + val timeout = 3.seconds + + def futureXor[A](f: Future[A]): Future[Xor[Throwable, A]] = + f.map(Xor.right[Throwable, A]).recover { case t => Xor.left(t) } + + implicit def eqfa[A: Eq]: Eq[Future[A]] = + new Eq[Future[A]] { + def eqv(fx: Future[A], fy: Future[A]): Boolean = { + val fz = futureXor(fx) zip futureXor(fy) + Await.result(fz.map { case (tx, ty) => tx === ty }, timeout) + } + } + + implicit val throwableEq: Eq[Throwable] = + Eq.fromUniversalEquals + + // Need non-fatal Throwables for Future recoverWith/handleError + implicit val nonFatalArbitrary: Arbitrary[Throwable] = + Arbitrary(arbitrary[Exception].map(identity)) + + checkAll("Future with Throwable", MonadErrorTests[Future, Throwable].monadError[Int, Int, Int]) + checkAll("Future", MonadRecTests[Future].monadRec[Int, Int, Int]) +}