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 Arrow Choice #2096

Merged
merged 8 commits into from
Dec 13, 2017
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ def mimaSettings(moduleName: String) = Seq(
exclude[ReversedMissingMethodProblem]("cats.data.CommonIRWSTConstructors.liftK"),
exclude[ReversedMissingMethodProblem]("cats.data.KleisliFunctions.liftF"),
exclude[ReversedMissingMethodProblem]("cats.data.KleisliFunctions.liftK"),
exclude[IncompatibleResultTypeProblem]("cats.implicits.catsStdInstancesForFunction1"),
exclude[IncompatibleResultTypeProblem]("cats.instances.package#all.catsStdInstancesForFunction1"),
exclude[IncompatibleResultTypeProblem]("cats.instances.Function1Instances.catsStdInstancesForFunction1"),
exclude[ReversedMissingMethodProblem]("cats.instances.Function1Instances.catsStdInstancesForFunction1"),
exclude[ReversedMissingMethodProblem]("cats.instances.Function1Instances.cats$instances$Function1Instances$_setter_$catsStdInstancesForFunction1_="),
exclude[IncompatibleResultTypeProblem]("cats.instances.package#function.catsStdInstancesForFunction1"),
exclude[InheritedNewAbstractMethodProblem]("cats.arrow.ArrowChoice#ToArrowChoiceOps.toArrowChoiceOps"),
exclude[ReversedMissingMethodProblem]("cats.data.CommonStateTConstructors.liftF"),
exclude[ReversedMissingMethodProblem]("cats.data.CommonStateTConstructors.liftK"),
exclude[ReversedMissingMethodProblem]("cats.NonEmptyParallel.parForEffect"),
Expand Down
43 changes: 43 additions & 0 deletions core/src/main/scala/cats/arrow/ArrowChoice.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cats
package arrow

import simulacrum.typeclass

/**
* Must obey the laws defined in cats.laws.ArrowChoiceLaws.
*/
@typeclass trait ArrowChoice[F[_, _]] extends Arrow[F] with Choice[F] { self =>

/**
* ArrowChoice yields Arrows with choice, allowing distribution
* over coproducts.
*
* Given two `F`s (`f` and `g`), create a new `F` with
* domain the coproduct of the domains of `f` and `g`,
* and codomain the coproduct of the codomains of `f` and `g`.
* This is the sum notion to `split`'s product.
*
* Example:
* {{{
* scala> import cats.implicits._
* scala> val toLong: Int => Long = _.toLong
* scala> val toDouble: Float => Double = _.toDouble
* scala> val f: Either[Int, Float] => Either[Long, Double] = toLong +++ toDouble
* scala> f(Left(3))
* res0: Either[Long,Double] = Left(3)
* scala> f(Right(3))
* res1: Either[Long,Double] = Right(3.0)
* }}}
*/
@simulacrum.op("+++", alias = true)
def choose[A, B, C, D](f: F[A, C])(g: F[B, D]): F[Either[A, B], Either[C, D]]

def left[A, B, C](fab: F[A, B]): F[Either[A, C], Either[B, C]] =
choose(fab)(lift(identity[C]))

def right[A, B, C](fab: F[A, B]): F[Either[C, A], Either[C, B]] =
choose(lift(identity[C]))(fab)
Copy link
Contributor

@kailuowang kailuowang Dec 13, 2017

Choose a reason for hiding this comment

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

This is never tested by law as I noticed from the law set defined in Haskell. Thus it doesn't have any test coverage.
Shall we add a law for this something like

  def leftRightConsistent[A, B, C](f: A => B): IsEq[F[Either[C, A], Either[C, B]]] =
     F.right[A, B, C](F.lift[A, B](f)) <-> F.left[A, B, C](F.lift[A, B](f)).dimap(_.swap, _.swap)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually added symmetric versions for all the laws last night for that reason, but it takes the method well over 50 lines. Something like that sounds like a more reasonable way to get similar effect. I'll try it out today, it certainly should work.


override def choice[A, B, C](f: F[A, C], g: F[B, C]): F[Either[A, B], C] =
rmap(choose(f)(g))(_.fold(identity, identity))
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that choice doesn’t have ||| as a symbolic alias. This is not really related to this PR but that would be a good thing to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, I can add.

}
32 changes: 24 additions & 8 deletions core/src/main/scala/cats/data/Kleisli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,10 @@ private[data] sealed abstract class KleisliInstances extends KleisliInstances0 {
implicit def F: Monad[F] = F0
}

implicit def catsDataArrowForKleisli[F[_]](implicit M: Monad[F]): Arrow[Kleisli[F, ?, ?]] =
new KleisliArrow[F] {
implicit def catsDataArrowChoiceForKleisli[F[_]](implicit M: Monad[F]): ArrowChoice[Kleisli[F, ?, ?]] =
new KleisliArrowChoice[F] {
def F: Monad[F] = M
}

}

private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1 {
Expand All @@ -147,11 +146,10 @@ private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1
implicit def catsDataMonadForKleisliId[A]: Monad[Kleisli[Id, A, ?]] =
catsDataMonadForKleisli[Id, A]

implicit def catsDataCommutativeArrowForKleisli[F[_]](implicit M: CommutativeMonad[F]): CommutativeArrow[Kleisli[F, ?, ?]] =
new KleisliCommutativeArrow[F] {def F: CommutativeMonad[F] = M }

implicit val catsDataCommutativeArrowForKleisliId: CommutativeArrow[Kleisli[Id, ?, ?]] =
catsDataCommutativeArrowForKleisli[Id]
implicit def catsDataArrowForKleisli[F[_]](implicit M: Monad[F]): Arrow[Kleisli[F, ?, ?]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

If I am not mistaken, since both Arrow and ArrowChoice instances have the same requirement (Monad[F]), one ArrowChoice instance should be fine. You can basically leave catsDataCommutativeArrowForKleisliId and catsDataCommutativeArrowForKleisli as is.

new KleisliArrow[F] {
def F: Monad[F] = M
}

implicit def catsDataContravariantMonoidalForKleisli[F[_], A](implicit F0: ContravariantMonoidal[F]): ContravariantMonoidal[Kleisli[F, A, ?]] =
new KleisliContravariantMonoidal[F, A] { def F: ContravariantMonoidal[F] = F0 }
Expand All @@ -161,6 +159,12 @@ private[data] sealed abstract class KleisliInstances1 extends KleisliInstances2
implicit def catsDataMonadForKleisli[F[_], A](implicit M: Monad[F]): Monad[Kleisli[F, A, ?]] =
new KleisliMonad[F, A] { def F: Monad[F] = M }

implicit def catsDataCommutativeArrowForKleisli[F[_]](implicit M: CommutativeMonad[F]): CommutativeArrow[Kleisli[F, ?, ?]] =
Copy link
Contributor

@kailuowang kailuowang Dec 12, 2017

Choose a reason for hiding this comment

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

These can return CommutativeArrow with ArrowChoice, if we let KleisliCommutativeArrow extend KleisliArrowChoice as mentioned below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, will do.

new KleisliCommutativeArrow[F] {def F: CommutativeMonad[F] = M }

implicit val catsDataCommutativeArrowForKleisliId: CommutativeArrow[Kleisli[Id, ?, ?]] =
catsDataCommutativeArrowForKleisli[Id]

implicit def catsDataParallelForKleisli[F[_], M[_], A]
(implicit P: Parallel[M, F]): Parallel[Kleisli[M, A, ?], Kleisli[F, A, ?]] = new Parallel[Kleisli[M, A, ?], Kleisli[F, A, ?]]{
implicit val appF = P.applicative
Expand Down Expand Up @@ -237,6 +241,18 @@ private[data] trait KleisliCommutativeArrow[F[_]] extends CommutativeArrow[Kleis
implicit def F: CommutativeMonad[F]
}

private[data] trait KleisliArrowChoice[F[_]] extends ArrowChoice[Kleisli[F, ?, ?]] with KleisliArrow[F] {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can fold KleisliArrow into KleisliArrowChoice right? KleisliCommutativeArrow can extend KleisliArrowChoice .

implicit def F: Monad[F]

def choose[A, B, C, D](f: Kleisli[F, A, C])(g: Kleisli[F, B, D]): Kleisli[F, Either[A, B], Either[C, D]] =
Kleisli(
(fe: Either[A, B]) =>
fe match {
case Left(a) => F.map(f(a))(Left.apply _)
case Right(b) => F.map(g(b))(Right.apply _)
})
}

private[data] trait KleisliArrow[F[_]] extends Arrow[Kleisli[F, ?, ?]] with KleisliCategory[F] with KleisliStrong[F] {
implicit def F: Monad[F]

Expand Down
15 changes: 8 additions & 7 deletions core/src/main/scala/cats/instances/function.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package instances

import cats.Contravariant
import cats.arrow.{Category, Choice, CommutativeArrow}
import cats.arrow.{Category, ArrowChoice, CommutativeArrow}

import annotation.tailrec

Expand Down Expand Up @@ -68,12 +68,13 @@ private[instances] sealed trait Function1Instances extends Function1Instances0 {
}
}

implicit val catsStdInstancesForFunction1: Choice[Function1] with CommutativeArrow[Function1] =
new Choice[Function1] with CommutativeArrow[Function1] {
def choice[A, B, C](f: A => C, g: B => C): Either[A, B] => C = {
case Left(a) => f(a)
case Right(b) => g(b)
}
implicit val catsStdInstancesForFunction1: ArrowChoice[Function1] with CommutativeArrow[Function1] =
new ArrowChoice[Function1] with CommutativeArrow[Function1] {
def choose[A, B, C, D](f: A => C)(g: B => D): Either[A, B] => Either[C, D] =
_.fold(
Copy link
Contributor

@kailuowang kailuowang Dec 13, 2017

Choose a reason for hiding this comment

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

using pattern match rather than fold has performance gain. see #1951

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, can do. I tend to forget that end of things.

Left.apply[C, D] _ compose f,
Right.apply[C, D] _ compose g
)

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

Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ trait AllSyntax
with ApplicativeErrorSyntax
with ApplySyntax
with ArrowSyntax
with ArrowChoiceSyntax
with BifunctorSyntax
with BifoldableSyntax
with BitraverseSyntax
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/syntax/arrowChoice.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package cats
package syntax

import cats.arrow.ArrowChoice

trait ArrowChoiceSyntax extends ArrowChoice.ToArrowChoiceOps
1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package object syntax {
object applicativeError extends ApplicativeErrorSyntax
object apply extends ApplySyntax
object arrow extends ArrowSyntax
object arrowChoice extends ArrowChoiceSyntax
object bifunctor extends BifunctorSyntax
object bifoldable extends BifoldableSyntax
object bitraverse extends BitraverseSyntax
Expand Down
55 changes: 28 additions & 27 deletions docs/src/main/tut/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,33 +211,34 @@ The `~>`, `⊥`, `⊤`, `:<:` and `:≺:` symbols can be imported with `import c

All other symbols can be imported with `import cats.implicits._`

| Symbol | Name | Nickname | Type Class | Signature |
| -------------------------------- | -------------------------| ---------------- | ----------------------- | --------------------------------------------------------------------|
| `fa *> fb` | followed by | | `Apply[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` |
| `fa <* fb` | for effect | | `Apply[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` |
| `x === y` | equals | | `Eq[A]` | `eqv(x: A, y: A): Boolean` |
| `x =!= y` | not equals | | `Eq[A]` | `neqv(x: A, y: A): Boolean` |
| `fa >>= f` | flatMap | | `FlatMap[F[_]]` | `flatMap(fa: F[A])(f: A => F[B]): F[B]` |
| `fa >> fb` | followed by | | `FlatMap[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` |
| <code>x &#124;-&#124; y</code> | remove | | `Group[A]` | `remove(x: A, y: A): A` |
| `x > y` | greater than | | `PartialOrder[A]` | `gt(x: A, y: A): Boolean` |
| `x >= y` | greater than or equal | | `PartialOrder[A]` | `gteq(x: A, y: A): Boolean` |
| `x < y` | less than | | `PartialOrder[A]` | `lt(x: A, y: A): Boolean` |
| `x <= y` | less than or equal | | `PartialOrder[A]` | `lteq(x: A, y: A): Boolean` |
| <code>x &#124;+&#124; y</code> | Semigroup combine | | `Semigroup[A]` | `combine(x: A, y: A): A` |
| `x <+> y` | SemigroupK combine | | `SemigroupK[F[_]]` | `combineK(x: F[A], y: F[A]): F[A]` |
| `f <<< g` | Arrow compose | | `Compose[F[_, _]]` | `compose(f: F[B, C], g: F[A, B]): F[A, C]` |
| `f >>> g` | Arrow andThen | | `Compose[F[_, _]]` | `andThen(f: F[B, C], g: F[A, B]): F[A, C]` |
| `f &&& g` | Arrow merge | | `Arrow[F[_, _]]` | `merge[A, B, C](f: F[A, B], g: F[A, C]): F[A, (B, C)]` |
| `f -< g` | Arrow combine and bypass | | `Arrow[F[_, _]]` | `combineAndByPass[A, B, C](f: F[A, B], g: F[B, C]): F[A, (B, C)]` |
| `F ~> G` | natural transformation | | `FunctionK[F[_], G[_]]` | `FunctionK` alias |
| `F :<: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias |
| `F :≺: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias |
| `fa &> fb` | parallel followed by | | `Parallel[M[_], F[_]]` | `parFollowedBy[A, B](ma: M[A])(mb: M[B]): M[B]` |
| `fa <& fb` | parallel for effect | | `Parallel[M[_], F[_]]` | `parForEffect[A, B](ma: M[A])(mb: M[B]): M[A]` |
| `⊥` | bottom | | N/A | `Nothing` |
| `⊤` | top | | N/A | `Any` |
| `fa << fb` (Deprecated) | for effect | | `FlatMap[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` |
| Symbol | Name | Nickname | Type Class | Signature |
| -------------------------------- | -------------------------| ---------------- | ----------------------- | -----------------------------------------------------------------------------|
| `fa *> fb` | followed by | | `Apply[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` |
| `fa <* fb` | for effect | | `Apply[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` |
| `x === y` | equals | | `Eq[A]` | `eqv(x: A, y: A): Boolean` |
| `x =!= y` | not equals | | `Eq[A]` | `neqv(x: A, y: A): Boolean` |
| `fa >>= f` | flatMap | | `FlatMap[F[_]]` | `flatMap(fa: F[A])(f: A => F[B]): F[B]` |
| `fa >> fb` | followed by | | `FlatMap[F[_]]` | `followedBy(fa: F[A])(fb: F[B]): F[B]` |
| <code>x &#124;-&#124; y</code> | remove | | `Group[A]` | `remove(x: A, y: A): A` |
| `x > y` | greater than | | `PartialOrder[A]` | `gt(x: A, y: A): Boolean` |
| `x >= y` | greater than or equal | | `PartialOrder[A]` | `gteq(x: A, y: A): Boolean` |
| `x < y` | less than | | `PartialOrder[A]` | `lt(x: A, y: A): Boolean` |
| `x <= y` | less than or equal | | `PartialOrder[A]` | `lteq(x: A, y: A): Boolean` |
| <code>x &#124;+&#124; y</code> | Semigroup combine | | `Semigroup[A]` | `combine(x: A, y: A): A` |
| `x <+> y` | SemigroupK combine | | `SemigroupK[F[_]]` | `combineK(x: F[A], y: F[A]): F[A]` |
| `f <<< g` | Arrow compose | | `Compose[F[_, _]]` | `compose(f: F[B, C], g: F[A, B]): F[A, C]` |
| `f >>> g` | Arrow andThen | | `Compose[F[_, _]]` | `andThen(f: F[B, C], g: F[A, B]): F[A, C]` |
| `f &&& g` | Arrow merge | | `Arrow[F[_, _]]` | `merge[A, B, C](f: F[A, B], g: F[A, C]): F[A, (B, C)]` |
| `f -< g` | Arrow combine and bypass | | `Arrow[F[_, _]]` | `combineAndByPass[A, B, C](f: F[A, B], g: F[B, C]): F[A, (B, C)]` |
| `f +++ g` | ArrowChoice choose | | `ArrowChoice[F[_, _]]` | `choose[A, B, C, D](f: F[A, C])(g: F[B, D]): F[Either [A, B], Either[C, D]]` |
| `F ~> G` | natural transformation | | `FunctionK[F[_], G[_]]` | `FunctionK` alias |
| `F :<: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias |
| `F :≺: G` | injectK | | `InjectK[F[_], G[_]]` | `InjectK` alias |
| `fa &> fb` | parallel followed by | | `Parallel[M[_], F[_]]` | `parFollowedBy[A, B](ma: M[A])(mb: M[B]): M[B]` |
| `fa <& fb` | parallel for effect | | `Parallel[M[_], F[_]]` | `parForEffect[A, B](ma: M[A])(mb: M[B]): M[A]` |
| `⊥` | bottom | | N/A | `Nothing` |
| `⊤` | top | | N/A | `Any` |
| `fa << fb` (Deprecated) | for effect | | `FlatMap[F[_]]` | `forEffect(fa: F[A])(fb: F[B]): F[A]` |

## <a id="law-testing" href="#law-testing"></a>How can I test instances against their type classes' laws?

Expand Down
43 changes: 43 additions & 0 deletions laws/src/main/scala/cats/laws/ArrowChoiceLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cats
package laws

import cats.arrow.ArrowChoice
import cats.syntax.compose._

/**
* Laws that must be obeyed by any `cats.arrow.ArrowChoice`.
*/
trait ArrowChoiceLaws[F[_, _]] extends ArrowLaws[F] with ChoiceLaws[F] {
implicit override def F: ArrowChoice[F]
implicit def Function: ArrowChoice[Function1]
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these laws based on the ones defined in Control-ArrowChoice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, they are meant to but interestingly the ones in the source and the ones in the Haskell doc do not quite look the same, I'll give them another pass tonight to make sure I transcribed the correct set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I totally misread them, somehow. How embarrassing! Sorry about that. Fixing.


def leftLiftCommute[A, B, C](f: A => B): IsEq[F[Either[A, C], Either[B, C]]] =
F.left[A, B, C](F.lift[A, B](f)) <-> F.lift[Either[A, C], Either[B, C]](Function.left[A, B, C](f))

def rightLiftCommute[A, B, C](f: A => B): IsEq[F[Either[C, A], Either[C, B]]] =
F.right[A, B, C](F.lift[A, B](f)) <-> F.lift[Either[C, A], Either[C, B]](Function.right[A, B, C](f))

def chooseLiftCommute[A, B, C, D](f: A => C, g: B => D): IsEq[F[Either[A, B], Either[C, D]]] =
F.lift[Either[A, B], Either[C, D]](Function.choose(f)(g)) <-> F.choose[A, B, C, D](F.lift(f))(F.lift(g))

def choiceLiftCommute[A, B, C](f: A => C, g: B => C): IsEq[F[Either[A, B], C]] =
F.lift[Either[A, B], C](Function.choice[A, B, C](f, g)) <-> F.choice[A, B, C](F.lift[A, C](f), F.lift[B, C](g))

def leftComposeCommute[A, B, C, D](f: A => B, g: B => C): IsEq[F[Either[A, D], Either[C, D]]] =
F.left[A, C, D](F.lift(g compose f)) <->
(F.lift[Either[B, D], Either[C, D]](Function.left(g)) <<<
F.lift[Either[A, D], Either[B, D]](Function.left(f)))

def rightComposeCommute[A, B, C, D](f: A => B, g: B => C): IsEq[F[Either[D, A], Either[D, C]]] =
F.right(F.lift(g compose f)) <->
(F.lift[Either[D, B], Either[D, C]](Function.right(g)) <<<
F.lift[Either[D, A], Either[D, B]](Function.right(f)))
}

object ArrowChoiceLaws {
def apply[F[_, _]](implicit ev: ArrowChoice[F], f: ArrowChoice[Function1]): ArrowChoiceLaws[F] =
new ArrowChoiceLaws[F] {
def F: ArrowChoice[F] = ev
def Function: ArrowChoice[Function1] = f
}
}
Loading