Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
54 changes: 54 additions & 0 deletions core/src/main/scala/cats/Inject.scala
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 18 additions & 5 deletions core/src/main/scala/cats/InjectK.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
20 changes: 20 additions & 0 deletions laws/src/main/scala/cats/laws/InjectLaws.scala
Original file line number Diff line number Diff line change
@@ -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 }
}
31 changes: 31 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/InjectTests.scala
Original file line number Diff line number Diff line change
@@ -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] }
}
68 changes: 68 additions & 0 deletions tests/src/test/scala/cats/tests/InjectTests.scala
Original file line number Diff line number Diff line change
@@ -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)
}