-
Notifications
You must be signed in to change notification settings - Fork 17
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
Changes from 4 commits
1affa4d
883844f
676fe05
cacd020
f093ec3
dd74123
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
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(_ => ()))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could it be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can't be a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, together with something like 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @LukaJCB what's the type of |
||
|
||
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) }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there's no need to catch here There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
If I use my implementation instead:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you use 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)) | ||
} |
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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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` | ||
) | ||
|
||
|
@@ -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` = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the convention for these sub-projects? What is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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( | ||
|
There was a problem hiding this comment.
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: