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 Cats Async, ApplicativeAsk and ArrowChoice instances #13

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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
127 changes: 127 additions & 0 deletions arrows-stdlib-cats/src/main/scala/arrows/stdlib/cats.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package arrows.stdlib

import cats.{ Applicative, StackSafeMonad }
import cats.effect._
import cats.arrow.ArrowChoice
import cats.kernel.{ Monoid, Semigroup }
import cats.mtl.ApplicativeAsk

import scala.concurrent.{ ExecutionContext, Promise }
import scala.util.control.NonFatal
import scala.util.{ Failure, Success }

object Cats extends ArrowInstances

sealed abstract class ArrowInstances extends ArrowInstances1 {

private final object ToLeft {
Copy link
Collaborator

@fwbrasil fwbrasil Aug 27, 2018

Choose a reason for hiding this comment

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

I took a look at the bytecode and it doesn't seem that final object will allow constant folding by the JIT compiler. Sorry for the back and forth, but could you change it to:

private final val toLeftInstance: Any => Left[Any, Nothing] = Left(_)
private final def toLeftI[T] = toLeftInstance.asInstanceOf[T => Left[T, Nothing]]

private final val instance: Any => Left[Any, Nothing] = Left(_)
def apply[T] = instance.asInstanceOf[T => Left[T, Nothing]]
}

private final object ToRight {
private final val instance: Any => Right[Nothing, Any] = Right(_)
def apply[T] = instance.asInstanceOf[T => Right[Nothing, T]]
}

implicit def catsEffectForTask(implicit ec: ExecutionContext): Effect[Task] = new ArrowAsync[Unit] with Effect[Task] {
def runAsync[A](fa: Task[A])(cb: Either[Throwable, A] => IO[Unit]): SyncIO[Unit] =
SyncIO(fa.run(())(ec).onComplete(t => cb(t.toEither).unsafeRunAsync(_ => ())))
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's cache _ => ():

private final val toUnit: Any => Unit = _ => ()

.unsafeRunAsync(toUnit)


override def flatMap[A, B](fa: Task[A])(f: A => Task[B]): Task[B] =
fa.flatMap(f)
}

implicit val catsArrowChoiceForArrow: ArrowChoice[Arrow] = new ArrowChoice[Arrow] {
def choose[A, B, C, D](f: Arrow[A, C])(g: Arrow[B, D]): Arrow[Either[A, B], Either[C, D]] =
Arrow[Either[A, B]].flatMap { eab =>
if (eab.isLeft) f(eab.left.get).map(ToLeft[C])
else g(eab.right.get).map(ToRight[D])
}

def lift[A, B](f: A => B): Arrow[A, B] = Arrow[A].map(f)

def first[A, B, C](fa: Arrow[A, B]): Arrow[(A, C), (B, C)] =
Arrow[(A, C)].flatMap { case (a, c) => fa(a).map(_ -> c) }

def compose[A, B, C](f: Arrow[B, C], g: Arrow[A, B]): Arrow[A, C] = g.andThen(f)
}

implicit def catsApplicativeAskForArrow[E]: ApplicativeAsk[Arrow[E, ?], E] = new ApplicativeAsk[Arrow[E, ?], E] {
Copy link
Collaborator

@fwbrasil fwbrasil Aug 27, 2018

Choose a reason for hiding this comment

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

could it be a val?

Copy link
Author

Choose a reason for hiding this comment

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

It can't be a val because then you can't define the E type parameter :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

we could cache it and cast:

private final val catsApplicativeAskForArrowInstance: ApplicativeAsk[Arrow[Any, ?], Any] ...
implicit def catsApplicativeAskForArrow[E] = catsApplicativeAskForArrowInstance.asInstanceOf[ApplicativeAsk[Arrow[E, ?], E]]

val applicative: Applicative[Arrow[E, ?]] = catsAsyncForArrow[E]

def ask: Arrow[E, E] = Arrow[E]

def reader[A](f: E => A): Arrow[E, A] = ask.map(f)

}

implicit def catsMonoidForArrow[A, B](implicit B0: Monoid[B]): Monoid[Arrow[A, B]] =
new Monoid[Arrow[A, B]] with ArrowSemigroup[A, B] {
implicit def B: Semigroup[B] = B0

def empty: Arrow[A, B] = Arrow.successful(Monoid[B].empty)
}
}

sealed abstract class ArrowInstances1 {

implicit def catsAsyncForArrow[E]: Async[Arrow[E, ?]] = new ArrowAsync[E] {}

implicit def catsSemigroupForArrow[A, B](implicit B0: Semigroup[B]): Semigroup[Arrow[A, B]] = new ArrowSemigroup[A, B] {
implicit def B: Semigroup[B] = B0
}
}

trait ArrowAsync[E] extends StackSafeMonad[Arrow[E, ?]] with Async[Arrow[E, ?]] {
def flatMap[A, B](fa: Arrow[E, A])(f: A => Arrow[E, B]): Arrow[E, B] =
Arrow[E].flatMap(e => fa(e).flatMap(a => f(a)(e)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, I'm still not sure this is a good approach. There's the performance issue, and I don't see why passing the same input to both arrows is useful other than when the arrow is a Task so the input is fixed. Could you give an example of what you couldn't express if this class used Task instead of Arrow?

Copy link
Author

Choose a reason for hiding this comment

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

Sure, together with something like ApplicativeAsk, you can define cool ReaderT style code with Arrow:

def foo[F[_]: Monad: ApplicativeAsk[?[_], Config]] = for {
  config <- ApplicativeAsk[F, Config].ask
  response <- serviceCall(config)
} yield response

Just a small example, but this is a pretty neat feature IMO and one could easily use this with e.g. http4s which only needs Sync to define the routes :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

@LukaJCB what's the type of serviceCall(config)? @alexandru could you weight in on this?


def raiseError[A](e: Throwable): Arrow[E, A] = Arrow.failed[A](e)

def handleErrorWith[A](fa: Arrow[E, A])(f: Throwable => Arrow[E, A]): Arrow[E, A] =
Arrow[E].flatMap(e => fa.recoverWith { case t: Throwable => f(t)(e) }(e))

def pure[A](x: A): Arrow[E, A] = Arrow.successful(x)

def suspend[A](thunk: => Arrow[E, A]): Arrow[E, A] =
Arrow[E].flatMap(e => try { thunk(e) } catch { case NonFatal(t) => Arrow.failed[A](t)(e) })
Copy link
Collaborator

Choose a reason for hiding this comment

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

there's no need to catch here

Copy link
Author

Choose a reason for hiding this comment

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

Are you sure? If I change this line, it evaluates the line eagerly:

scala> def y(): Arrow[Int, String] = { throw new Exception(); }
y: ()arrows.stdlib.Arrow[Int,String]

scala> Arrow[Int].flatMap(y())
java.lang.Exception
  at .y(<console>:14)
  ... 36 elided

If I use my implementation instead:

scala> Arrow[Int].flatMap(i => try { y()(i) } catch { case t => Arrow.failed[String](t)(i) })
res6: arrows.stdlib.Arrow[Int,String] = <function1>

Sync#suspend requires to suspend any side-effects that occur when acquring the Arrow.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, you can't pass the arrow directly since it'll evaluate the by-name param. This should do it:

Arrow[E].flatMap(e => thunk(e))


override def delay[A](thunk: => A): Arrow[E, A] =
Arrow[E].flatMap(e => Arrow.successful(thunk))
Copy link
Collaborator

Choose a reason for hiding this comment

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

could you use map here instead? it's less expensive

Arrow[E].map(_ => thunk)


def bracketCase[A, B](acquire: Arrow[E, A])(use: A => Arrow[E, B])(release: (A, ExitCase[Throwable]) => Arrow[E, Unit]): Arrow[E, B] = Arrow[E].flatMap(e =>
acquire.flatMap(a => use(a).transformWith {
case Success(b) => release(a, ExitCase.complete)(e).map(_ => b)
case Failure(t) => release(a, ExitCase.error(t))(e).flatMap(_ => Task.failed[B](t))
}(e))(e))

def async[A](k: (Either[Throwable, A] => Unit) => Unit): Arrow[E, A] = Arrow[E].flatMap { _ =>
val promise = Promise[A]

k {
case Left(t) => promise.failure(t)
case Right(a) => promise.success(a)
}

Task.async(promise.future)
}

def asyncF[A](k: (Either[Throwable, A] => Unit) => Arrow[E, Unit]): Arrow[E, A] = Arrow[E].flatMap { e =>
val promise = Promise[A]

k {
case Left(t) => promise.failure(t)
case Right(a) => promise.success(a)
}.flatMap(_ => Task.async(promise.future))(e)
}

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

trait ArrowSemigroup[A, B] extends Semigroup[Arrow[A, B]] {
implicit def B: Semigroup[B]

def combine(x: Arrow[A, B], y: Arrow[A, B]): Arrow[A, B] =
Arrow[A].flatMap(a => x.flatMap(b => y(a).map(b2 => B.combine(b, b2)))(a))
}
104 changes: 104 additions & 0 deletions arrows-stdlib-cats/src/test/scala/arrows/stdlib/ArrowLawTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package arrows.stdlib

import java.util.concurrent.TimeUnit

import cats.Eq
import cats.data.EitherT
import cats.effect.Async
import cats.instances.all._
import cats.tests.CatsSuite
import org.scalacheck.{ Arbitrary, Cogen, Gen }
import org.scalacheck.Arbitrary.{ arbitrary => getArbitrary }

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ Await, Future }
import arrows.stdlib.Cats._
import cats.effect.laws.discipline.{ AsyncTests, EffectTests }
import cats.kernel.laws.discipline.MonoidTests
import cats.laws.discipline.SemigroupalTests.Isomorphisms
import cats.laws.discipline.{ ArrowChoiceTests, MonadTests }
import cats.effect.laws.discipline.arbitrary._
import cats.effect.laws.util.TestContext
import cats.effect.laws.util.TestInstances._
import cats.mtl.laws.discipline.ApplicativeAskTests

class ArrowLawTests extends CatsSuite {

implicit val context: TestContext = TestContext()

implicit def eqArrow[A, B](implicit A: Arbitrary[A], B: Eq[B]): Eq[Arrow[A, B]] = new Eq[Arrow[A, B]] {
val sampleCnt: Int = 10

def eqv(f: A Arrow B, g: A Arrow B): Boolean = {
val samples = List.fill(sampleCnt)(A.arbitrary.sample).collect {
case Some(a) => a
case None => sys.error("Could not generate arbitrary values to compare two Arrows")
}
samples.forall(s => eqFuture[B].eqv(f.run(s), g.run(s)))
}
}

implicit def arrowArbitrary[A: Arbitrary, B: Arbitrary: Cogen]: Arbitrary[Arrow[A, B]] =
Arbitrary(Gen.delay(genArrow[A, B]))

implicit val nonFatalArbitrary: Arbitrary[Throwable] =
Arbitrary(Gen.const(new Exception()))

def genArrow[A: Arbitrary, B: Arbitrary: Cogen]: Gen[Arrow[A, B]] = {
Gen.frequency(
5 -> genPure[A, B],
5 -> genApply[A, B],
1 -> genFail[A, B],
5 -> genAsync[A, B],
5 -> getMapOne[A, B],
5 -> getMapTwo[A, B],
10 -> genFlatMap[A, B]
)
}

def genPure[A: Arbitrary, B: Arbitrary]: Gen[Arrow[A, B]] =
getArbitrary[B].map(Arrow.successful)

def genApply[A: Arbitrary, B: Arbitrary]: Gen[Arrow[A, B]] =
getArbitrary[B].map(a => Arrow[A].map(_ => a))

def genFail[A: Arbitrary, B]: Gen[Arrow[A, B]] =
getArbitrary[Throwable].map(Arrow.failed)

def genAsync[A: Arbitrary, B: Arbitrary]: Gen[Arrow[A, B]] =
getArbitrary[(Either[Throwable, B] => Unit) => Unit].map(Async[Arrow[A, ?]].async)

def genFlatMap[A: Arbitrary, B: Arbitrary: Cogen]: Gen[Arrow[A, B]] =
for {
arr <- getArbitrary[Arrow[A, B]]
f <- getArbitrary[B => Task[B]]
} yield arr.flatMap(f)

def getMapOne[A: Arbitrary, B: Arbitrary: Cogen]: Gen[Arrow[A, B]] =
for {
arr <- getArbitrary[Arrow[A, B]]
f <- getArbitrary[B => B]
} yield arr.map(f)

def getMapTwo[A: Arbitrary, B: Arbitrary: Cogen]: Gen[Arrow[A, B]] =
for {
arr <- getArbitrary[Arrow[A, B]]
f1 <- getArbitrary[B => B]
f2 <- getArbitrary[B => B]
} yield arr.map(f1).map(f2)

implicit def arrowCogen[A, B]: Cogen[Arrow[A, B]] =
Cogen[Unit].contramap(_ => ())

implicit def arrowIso[A]: Isomorphisms[Arrow[A, ?]] = Isomorphisms.invariant[Arrow[A, ?]]

implicit def eitherTEq[R: Eq: Arbitrary, A: Eq]: Eq[EitherT[Arrow[R, ?], Throwable, A]] =
EitherT.catsDataEqForEitherT[Arrow[R, ?], Throwable, A]

checkAll("ApplicativeAsk[Arrow, String]", ApplicativeAskTests[Arrow[String, ?], String].applicativeAsk[Int])
checkAll("ArrowChoice[Arrow]", ArrowChoiceTests[Arrow].arrowChoice[Int, Int, Int, String, String, String])
checkAll("Async[Arrow]", AsyncTests[Arrow[String, ?]].async[Int, Int, Int])
checkAll("Effect[Task]", EffectTests[Task].effect[Int, Int, Int])
checkAll("Monoid[Arrow[Int, String]", MonoidTests[Arrow[Int, String]].monoid)
}
28 changes: 28 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@ lazy val `arrows` =
`arrows-stdlib-jvm`,
`arrows-stdlib-js`,
`arrows-twitter`,
`arrows-stdlib-cats-jvm`,
`arrows-stdlib-cats-js`,
`arrows-benchmark`
)
.dependsOn(
`arrows-stdlib-jvm`,
`arrows-stdlib-js`,
`arrows-twitter`,
`arrows-twitter`,
`arrows-stdlib-cats-jvm`,
`arrows-stdlib-cats-js`,
`arrows-benchmark`
)

Expand All @@ -50,6 +55,29 @@ lazy val `arrows-stdlib` =
lazy val `arrows-stdlib-jvm` = `arrows-stdlib`.jvm
lazy val `arrows-stdlib-js` = `arrows-stdlib`.js.settings(test := {})

lazy val `arrows-stdlib-cats` =

Choose a reason for hiding this comment

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

What is the convention for these sub-projects? What is stdlib referring to? Couldn't it be just arrows-cats?

Copy link
Author

Choose a reason for hiding this comment

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

stdlib refers to the use of stdlib Future wheras arrows-twitter uses twitter Future.

crossProject.crossType(superPure)
.settings(commonSettings)
.settings(
crossScalaVersions := Seq("2.12.6"),
name := "arrows-stdlib-cats",
libraryDependencies ++= List(
compilerPlugin("org.spire-math" %% "kind-projector" % "0.9.7"),
"org.typelevel" %%% "cats-effect" % "1.0.0-RC3",
"org.typelevel" %%% "cats-mtl-core" % "0.2.3",
"org.typelevel" %%% "cats-effect-laws" % "1.0.0-RC3" % "test",
"org.typelevel" %%% "cats-mtl-laws" % "0.2.3" % "test",
"org.typelevel" %%% "cats-testkit" % "1.2.0" % "test",
),
scoverage.ScoverageKeys.coverageMinimum := 60,
scoverage.ScoverageKeys.coverageFailOnMinimum := false)
.jsSettings(
coverageExcludedPackages := ".*"
).dependsOn(`arrows-stdlib`)

lazy val `arrows-stdlib-cats-jvm` = `arrows-stdlib-cats`.jvm
lazy val `arrows-stdlib-cats-js` = `arrows-stdlib-cats`.js.settings(test := {})

lazy val `arrows-twitter` = project
.settings(commonSettings)
.settings(
Expand Down