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 1 commit
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
107 changes: 107 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,107 @@
package arrows.stdlib

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

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

object Cats extends ArrowInstances

sealed abstract class ArrowInstances extends ArrowInstances1 {

implicit def catsMonadForArrow[E]: Async[Arrow[E, ?]] = new Async[Arrow[E, ?]] with StackSafeMonad[Arrow[E, ?]] {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this use 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.

No, that would constraint it unnecessary, An Arrow[E, A] is just as much valid as a Task[A]. Though for Effect it does need to be Task, as you can't implement runAsync otherwise. For Async we can do it for any Arrow.

Copy link
Collaborator

@fwbrasil fwbrasil Aug 24, 2018

Choose a reason for hiding this comment

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

Arrow is more general than monad, so I don't think it's a good fit to a monad typeclass. Also, the fixed Unit input is important for performance. This has much better performance:

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

than

    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
Author

Choose a reason for hiding this comment

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

Right yeah, that's why I need to write the Effect type for Task, but we should still provide all the instances that make sense :)

Copy link
Author

Choose a reason for hiding this comment

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

For example, we also have Async instances for Kleisli[F, R, A] where F has an Async instance itself. Since Arrow is roughly equivalent to that, it stands to reason that we should provide that too :)


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)))

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] =
Copy link
Collaborator

@fwbrasil fwbrasil Aug 24, 2018

Choose a reason for hiding this comment

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

Arrows aren't strict so I'm not sure I understand why suspend would be necessary

Copy link
Author

Choose a reason for hiding this comment

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

It's the contract for Sync :)

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.

you don't need to catch here, Arrow will do it

Copy link
Author

Choose a reason for hiding this comment

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

Ah, cool thanks! :)


override def delay[A](thunk: => A): Arrow[E, A] =
Arrow[E].flatMap(e => try { Arrow.successful(thunk) } catch { case NonFatal(t) => Arrow.failed[A](t)(e) })

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)
}

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 {
case Left(a) => f(a).map(Left(_))
Copy link
Collaborator

Choose a reason for hiding this comment

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

The bytecode for this transformation will be large. What do you think about optimizing it? Something like

// In a stable scope
private final object ToLeft {
   private final val instance: Any => Left[Any] = Left(_)
   def apply[T]() = instance.asInstanceOf[T => Left[T]]
}

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

and then

Arrow[Either[A, B]].flatMap { a =>
  if(a.isLeft) f(a).map(toLeft)
  else f(a).map(toRight)
}

case Right(b) => g(b).map(Right(_))
}

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, ?]] = catsMonadForArrow[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 catsSemigroupForArrow[A, B](implicit B0: Semigroup[B]): Semigroup[Arrow[A, B]] = new ArrowSemigroup[A, B] {
implicit def B: Semigroup[B] = B0
}
}

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))
}
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.5"),

Choose a reason for hiding this comment

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

I think we are at 2.12.6.

name := "arrows-stdlib-cats",
libraryDependencies ++= List(
"org.typelevel" %%% "cats-effect" % "1.0.0-RC3",
"org.typelevel" %%% "cats-effect-laws" % "1.0.0-RC3",

Choose a reason for hiding this comment

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

We only need cats-effect-laws for testing purposes.

"org.typelevel" %%% "cats-mtl-core" % "0.2.3",
"org.typelevel" %%% "cats-mtl-laws" % "0.2.3",

Choose a reason for hiding this comment

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

Here too, I think we only need cats-mtl-laws for testing purposes, no reason to depend on it in any other context.

compilerPlugin("org.spire-math" %%% "kind-projector" % "0.9.7"),
"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