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

Add MonadRec for Future and reinstate Future tests for JVM #1261

Merged
merged 2 commits into from
Aug 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")),
Expand Down Expand Up @@ -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")

Expand Down
14 changes: 12 additions & 2 deletions core/src/main/scala/cats/instances/future.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Copy link
Contributor

Choose a reason for hiding this comment

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

shall we add @tailrec ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kailuowang This implementation won't actually compile with @tailrec, since the recursive call is inside the flatMap. It is stack-safe, though, thanks to the way Future#flatMap works.

Copy link
Contributor

Choose a reason for hiding this comment

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

@travisbrown is this stack safe on all versions of scala? I thought scala futures could have issues with this (at least in 2.10 or something. This is just a vague recollection).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek I'm pretty sure that was only an issue with the original Akka Future implementation, and that by the time SIP-14 landed flatMap was stack-safe (thanks to pressure from Twitter and inspired by c.t.u.Future). I'll double-check that, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek I'm not finding the discussion I was thinking of at the moment, but I did try this on 2.9.3:

import scala.concurrent.{ Await, Future }, scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

def test(i: Int)(f: Int => Future[Int]): Future[Int] = f(i).flatMap {
  case x if x > 100000 => Future.successful(x)
  case x => test(x)(f)
}

val f1 = test(0)(x => Future(x + 1))
val f2 = test(0)(x => Future(x))

Neither blows the stack, and f1 succeeds as expected.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, looks legit. Thanks (I know Twitter's is stack safe, I didn't know scala was).

Copy link
Member

Choose a reason for hiding this comment

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

Maybe a code comment mentioning that this construct is actually stack-safe would be appropriate here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@fthomas Makes sense—done.

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)
Expand Down
1 change: 1 addition & 0 deletions js/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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])
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 test a reliable test of stack safety?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek I don't believe so—it looks like it just checks for consistency with flatMap.

}
39 changes: 39 additions & 0 deletions jvm/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
@@ -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]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add this but have it take a timeout to the object where we define the Monad?

def futureEq[A: Eq](timeout: Duration): Eq[Future[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.

@johnynek That's what I do for Twitter futures in catbird, but there's no chance for Scala.js there, anyway, and I think Scala.js support is the motivation for keeping all Await out of cats-core.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I didn't know that restriction.

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])
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this repeated above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek There's a bit of duplication here across js and jvm, since the Eq instance for Scala.js uses a custom Await.

}