From d094c66585eb858f502b3775905a79a9272e3672 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 9 Apr 2017 09:24:07 -0700 Subject: [PATCH 1/2] (re)introduce Inject for Either --- core/src/main/scala/cats/Inject.scala | 54 ++++++++++++++++++++++++++ core/src/main/scala/cats/InjectK.scala | 23 ++++++++--- 2 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 core/src/main/scala/cats/Inject.scala diff --git a/core/src/main/scala/cats/Inject.scala b/core/src/main/scala/cats/Inject.scala new file mode 100644 index 0000000000..04c075eb46 --- /dev/null +++ b/core/src/main/scala/cats/Inject.scala @@ -0,0 +1,54 @@ +package cats + +/** + * Inject is a type class providing an injection from type `A` into + * type `B`. An injection is a function `inj` which does not destroy + * any information: for every `b: B` there is at most one `a: A` such + * that `inj(a) = b`. + * + * Because of this all injections admit partial inverses `prj` which + * pair a value `b: B` back with a single value `a: A`. + * + * @since 1.0 + * @note Prior to cats 1.0, Inject handled injection for type + * constructors. For injection of type constructors, use [[InjectK]]. + * + * @see [[InjectK]] for injection for [[cats.data.EitherK]] + */ +abstract class Inject[A, B] { + def inj: A => B + + def prj: B => Option[A] + + final def apply(a: A): B = inj(a) + + final def unapply(b: B): Option[A] = prj(b) +} + +private[cats] sealed abstract class InjectInstances { + implicit def catsReflexiveInjectInstance[A]: Inject[A, A] = + new Inject[A, A] { + val inj = identity(_: A) + + val prj = Some(_: A) + } + + implicit def catsLeftInjectInstance[A, B]: Inject[A, Either[A, B]] = + new Inject[A, Either[A, B]] { + val inj = Left(_: A) + + val prj = (_: Either[A, B]).left.toOption + } + + implicit def catsRightInjectInstance[A, B, C](implicit I: Inject[A, B]): Inject[A, Either[C, B]] = + new Inject[A, Either[C, B]] { + val inj = (a: A) => Right(I.inj(a)) + + val prj = (_: Either[C, B]).right.toOption.flatMap(I.prj) + } + +} + +object Inject extends InjectInstances { + def apply[A, B](implicit I: Inject[A, B]): Inject[A, B] = I +} diff --git a/core/src/main/scala/cats/InjectK.scala b/core/src/main/scala/cats/InjectK.scala index 8c28f51658..f2b8d80559 100644 --- a/core/src/main/scala/cats/InjectK.scala +++ b/core/src/main/scala/cats/InjectK.scala @@ -4,11 +4,24 @@ import cats.arrow.FunctionK import cats.data.EitherK /** - * The injection type class as described in "Data types a la carte" - * (Swierstra 2008). - * - * @see [[http://www.staff.science.uu.nl/~swier004/publications/2008-jfp.pdf]] - */ + * InjectK is a type class providing an injection from type + * constructor `F` into type constructor `G`. An injection is a + * functor transformation `inj` which does not destroy any + * information: for every `ga: G[A]` there is at most one `fa: F[A]` + * such that `inj(fa) = ga`. + * + * Because of this all injections admit partial inverses `prj` which + * pair a value `ga: G[A]` back with a single value `fa: F[A]`. + * + * The behavior of the default instances for the InjectK type class + * are described thoroughly in "Data types a la carte" (Swierstra + * 2008). + * + * @note Prior to cats 1.0, InjectK was known as [[Inject]]. + * + * @see [[http://www.staff.science.uu.nl/~swier004/publications/2008-jfp.pdf]] + * @see [[Inject]] for injection for `Either` + */ abstract class InjectK[F[_], G[_]] { def inj: FunctionK[F, G] From 521a0c4513e91cb7ba06ba17aa19381ed30c9294 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 9 Apr 2017 10:10:34 -0700 Subject: [PATCH 2/2] Port tests for InjectK to new tests for Inject --- .../src/main/scala/cats/laws/InjectLaws.scala | 20 ++++++ .../cats/laws/discipline/InjectTests.scala | 31 +++++++++ .../test/scala/cats/tests/InjectTests.scala | 68 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 laws/src/main/scala/cats/laws/InjectLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/InjectTests.scala create mode 100644 tests/src/test/scala/cats/tests/InjectTests.scala diff --git a/laws/src/main/scala/cats/laws/InjectLaws.scala b/laws/src/main/scala/cats/laws/InjectLaws.scala new file mode 100644 index 0000000000..694e16edc8 --- /dev/null +++ b/laws/src/main/scala/cats/laws/InjectLaws.scala @@ -0,0 +1,20 @@ +package cats +package laws + +trait InjectLaws[A, B] { + def inject: Inject[A, B] + + def injectRoundTripInj(a: A): IsEq[Option[A]] = + (inject.prj compose inject.inj).apply(a) <-> Some(a) + + def injectRoundTripPrj(b: B): IsEq[Option[B]] = + inject.prj(b) match { + case Some(a) => (Some(inject.inj(a)): Option[B]) <-> Some(b) + case None => (None: Option[B]) <-> None + } +} + +object InjectLaws { + def apply[A, B](implicit ev: Inject[A, B]): InjectLaws[A, B] = + new InjectLaws[A, B]{ val inject: Inject[A, B] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/InjectTests.scala b/laws/src/main/scala/cats/laws/discipline/InjectTests.scala new file mode 100644 index 0000000000..8619d84cb9 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/InjectTests.scala @@ -0,0 +1,31 @@ +package cats +package laws +package discipline + +import org.scalacheck.Arbitrary +import org.scalacheck.Prop +import Prop._ +import org.typelevel.discipline.Laws + +trait InjectTests[A, B] extends Laws { + def laws: InjectLaws[A, B] + + def inject(implicit + ArbA: Arbitrary[A], + EqOptionA: Eq[Option[A]], + ArbB: Arbitrary[B], + EqOptionB: Eq[Option[B]] + ): RuleSet = + new DefaultRuleSet( + "inject", + None, + "inject round trip inj" -> forAll((a: A) => laws.injectRoundTripInj(a)), + "inject round trip prj" -> forAll((b: B) => laws.injectRoundTripPrj(b)) + ) + +} + +object InjectTests { + def apply[A, B](implicit ev: Inject[A, B]): InjectTests[A, B] = + new InjectTests[A, B] { val laws: InjectLaws[A, B] = InjectLaws[A, B] } +} diff --git a/tests/src/test/scala/cats/tests/InjectTests.scala b/tests/src/test/scala/cats/tests/InjectTests.scala new file mode 100644 index 0000000000..2d667d6dee --- /dev/null +++ b/tests/src/test/scala/cats/tests/InjectTests.scala @@ -0,0 +1,68 @@ +package cats + +import cats.laws.discipline.{ InjectTests => InjectTypeclassTests } +import cats.tests.CatsSuite + +class InjectTests extends CatsSuite { + + type StringOrInt = Either[String, Int] + + test("inj & prj") { + def distr[F](f1: F, f2: F) + (implicit + I0: Inject[String, F], + I1: Inject[Int, F] + ): Option[String] = + for { + x <- I0.prj(f1) + y <- I1.prj(f2) + } yield s"$x $y" + + forAll { (x: String, y: Int) => + val expr1: StringOrInt = Inject[String, StringOrInt].inj(x) + val expr2: StringOrInt = Inject[Int, StringOrInt].inj(y) + val res = distr(expr1, expr2) + res should ===(Some(s"$x $y")) + } + } + + test("apply & unapply") { + def distr[F](f1: F, f2: F) + (implicit + I0: Inject[String, F], + I1: Inject[Int, F] + ): Option[String] = + for { + x <- I0.unapply(f1) + y <- I1.unapply(f2) + } yield s"$x $y" + + forAll { (x: String, y: Int) => + val expr1: StringOrInt = Inject[String, StringOrInt].apply(x) + val expr2: StringOrInt = Inject[Int, StringOrInt].apply(y) + val res = distr(expr1, expr2) + res should ===(Some(s"$x $y")) + } + } + + test("apply in left") { + forAll { (y: String) => + Inject[String, StringOrInt].inj(y) == Left(y) should ===(true) + } + } + + test("apply in right") { + forAll { (y: Int) => + Inject[Int, StringOrInt].inj(y) == Right(y) should ===(true) + } + } + + test("null identity") { + val stringNull = null.asInstanceOf[String] + Inject.catsReflexiveInjectInstance[String].inj(stringNull) should ===(stringNull) + Inject.catsReflexiveInjectInstance[String].prj(stringNull) should ===(Some(stringNull)) + } + + checkAll("Inject[String, StringOrInt]", InjectTypeclassTests[String, StringOrInt].inject) + checkAll("Inject[Int, StringOrInt]", InjectTypeclassTests[Int, StringOrInt].inject) +}