diff --git a/core/src/main/scala/cats/Selective.scala b/core/src/main/scala/cats/Selective.scala index d5db0b5..635a6f4 100644 --- a/core/src/main/scala/cats/Selective.scala +++ b/core/src/main/scala/cats/Selective.scala @@ -55,6 +55,22 @@ trait Selective[F[_]] { lb.map(andS(_)(test(a))) }) + /** + * apS :: Selective f => f (a -> b) -> f a -> f b + * apS f x = select (Left <$> f) ((&) <$> x) + * + * (&) :: c -> (c -> d) -> d -- reverse function application + * + * Although the type signature of `apS` matches `applicative.ap`, in general, + * they are *not* equivalent. Selective functors that *do* satisfy the + * property `applicative.ap === apS` are called *rigid* selectives. + */ + def apS[A, B](fn: F[A => B])(fa: F[A]): F[B] = { + val leftFn: F[Either[A => B, B]] = map(fn)(Left(_)) + val application = map(fa)(a => (g: A => B) => g(a)) + select(leftFn)(application) + } + // TODO more combinators here } diff --git a/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala b/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala new file mode 100644 index 0000000..724105f --- /dev/null +++ b/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala @@ -0,0 +1,41 @@ +package cats.laws + +import cats.Selective +import cats.Selective.ops._ + +/** + * A selective functor is _rigid_ if it satisfies `<*> = apS`. + * + * These laws are a consequence of that assumption + */ +trait RigidSelectiveLaws[F[_]] { + implicit def F: Selective[F] + + /** + * Selectives that satisfy this criteria are known as rigid selectives. + * + * `applicative.ap === apS` + */ + def rigidSelectiveApply[A, B](x: F[A], fn: F[A => B]): IsEq[F[B]] = { + val lhs = F.apS(fn)(x) + val rhs = F.applicative.ap(fn)(x) + lhs <-> rhs + } + + /** + * A consequence of `applicative.ap === apS` and associativity + * + * `x *> (y <*? z) === (x *> y) <*? z + */ + def rigidSelectiveInterchange[A, B](x: F[A], y: F[Either[A, B]], z: F[A => B]): IsEq[F[B]] = { + val lhs = F.applicative.*>(x)(F.select(y)(z)) + val rhs = F.select(F.applicative.*>(x)(y))(z) + lhs <-> rhs + } + +} + +object RigidSelectiveLaws { + def apply[F[_]](implicit ev: Selective[F]): RigidSelectiveLaws[F] = + new RigidSelectiveLaws[F] { def F: Selective[F] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala b/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala new file mode 100644 index 0000000..9132a13 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala @@ -0,0 +1,35 @@ +package cats +package laws +package discipline + +import org.scalacheck.Prop._ +import org.scalacheck.Arbitrary +import org.typelevel.discipline.Laws + +trait RigidSelectiveTests[F[_]] extends Laws { + def laws: RigidSelectiveLaws[F] + + def selective[A: Arbitrary, B: Arbitrary]( + implicit + ArbFA: Arbitrary[F[A]], + ArbFEitherAB: Arbitrary[F[Either[A, B]]], + ArbFAtoB: Arbitrary[F[A => B]], + EqFB: Eq[F[B]] + ): RuleSet = + new DefaultRuleSet( + name = "rigid selective", + parent = None, + "rigid selective apply" -> forAll(laws.rigidSelectiveApply[A, B] _), + "rigid selective interchange" -> forAll(laws.rigidSelectiveInterchange[A, B] _) + ) +} + +object RigidSelectiveTests { + + def apply[F[_]: Selective]: RigidSelectiveTests[F] = + new RigidSelectiveTests[F] { def laws: RigidSelectiveLaws[F] = RigidSelectiveLaws[F] } + + def monad[F[_]: Monad]: RigidSelectiveTests[F] = + new RigidSelectiveTests[F] { def laws: RigidSelectiveLaws[F] = RigidSelectiveLaws[F](Selective.fromMonad) } + +} diff --git a/tests/src/test/scala/cats/tests/EvalSuite.scala b/tests/src/test/scala/cats/tests/EvalSuite.scala new file mode 100644 index 0000000..fd0cc46 --- /dev/null +++ b/tests/src/test/scala/cats/tests/EvalSuite.scala @@ -0,0 +1,18 @@ +package cats.tests + +import cats.data._ +import cats.laws.discipline._ +import org.scalacheck.Arbitrary._ +import cats.laws.discipline.arbitrary._ +import cats.Eval +import cats.Selective + +// TODO: abstract this to a general Monad because all monadic selective functors are rigid + +class EvalSuite extends CatsSuite { + + implicit val selective = Selective.fromMonad[Eval] + + checkAll("Eval[Int]", SelectiveTests[Eval].selective[Int, Int, Int]) + checkAll("Eval[Int]", RigidSelectiveTests[Eval].selective[Int, Int]) +} diff --git a/tests/src/test/scala/cats/tests/SelectiveSuite.scala b/tests/src/test/scala/cats/tests/SelectiveSuite.scala index ef75d61..9ae34cc 100644 --- a/tests/src/test/scala/cats/tests/SelectiveSuite.scala +++ b/tests/src/test/scala/cats/tests/SelectiveSuite.scala @@ -121,4 +121,16 @@ class SelectiveSuite extends CatsSuite { } } } + + test("apS") { + forAll { (fn: Option[Int => String], fa: Option[Int]) => + fn match { + case None => + testInstance.apS(fn)(fa) should ===(None) + case Some(f) => + testInstance.apS(fn)(fa) should ===(fa.map(f)) + } + } + } + } diff --git a/tests/src/test/scala/cats/tests/ValidatedSuite.scala b/tests/src/test/scala/cats/tests/ValidatedSuite.scala index 1515ae7..6970936 100644 --- a/tests/src/test/scala/cats/tests/ValidatedSuite.scala +++ b/tests/src/test/scala/cats/tests/ValidatedSuite.scala @@ -12,4 +12,7 @@ class ValidatedSuite extends CatsSuite { checkAll("Validated[String, Int]", SelectiveTests[Validated[String, ?]].selective[Int, Int, Int]) + // This fails, as expected, because Validated[E,?] is no a *rigid* selective functor + // Useful for checking that the rigid laws fail when they are expected to! + // checkAll("Validated[String, Int]", RigidSelectiveTests[Validated[String, ?]].selective[Int, Int]) }