From 39ffa70bba18dbac42621f0da71c2470880d89dc Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 19 Feb 2020 18:23:22 +0000 Subject: [PATCH 1/8] Ported the apS implementation from the paper. --- core/src/main/scala/cats/Selective.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/src/main/scala/cats/Selective.scala b/core/src/main/scala/cats/Selective.scala index d5db0b5..bb4370a 100644 --- a/core/src/main/scala/cats/Selective.scala +++ b/core/src/main/scala/cats/Selective.scala @@ -19,6 +19,22 @@ trait Selective[F[_]] { select(lhs)(r) } + /** + * 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) + } + def ifS[A](x: F[Boolean])(t: F[A])(e: F[A]): F[A] = { val condition: F[Either[Unit, Unit]] = map(x)(p => if (p) Left(()) else Right(())) val left: F[Unit => A] = map(t)(Function.const) From 47a4ee259598bda4f4edbdac8084b8a534a2003b Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 19 Feb 2020 18:29:05 +0000 Subject: [PATCH 2/8] The selective apply law and interchange theorem for rigid selectives --- .../scala/cats/laws/RigidSelectiveLaws.scala | 42 +++++++++++++++++++ .../laws/discipline/RigidSelectiveTests.scala | 41 ++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala 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..d204c95 --- /dev/null +++ b/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala @@ -0,0 +1,42 @@ +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, so don't strictly + * need to test this law, but I'm writing it as a sanity check + * + * `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..c18b7e9 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala @@ -0,0 +1,41 @@ +package cats +package laws +package discipline + +import cats.laws.discipline.SemigroupalTests.Isomorphisms +import org.scalacheck.Prop._ +import org.scalacheck.{Arbitrary, Cogen} +import org.typelevel.discipline.Laws + +trait RigidSelectiveTests[F[_]] extends Laws { + def laws: RigidSelectiveLaws[F] + + // TODO - probably don't need all these implicit args. Tidy up + def selective[A: Arbitrary, B: Arbitrary, C: Arbitrary]( + implicit + ArbFA: Arbitrary[F[A]], + ArbFEitherA: Arbitrary[F[Either[A, A]]], + ArbFEitherAB: Arbitrary[F[Either[A, B]]], + ArbFEitherCAtoB: Arbitrary[F[Either[C, A => B]]], + ArbFAtoB: Arbitrary[F[A => B]], + ArbFCtoAtoB: Arbitrary[F[C => A => B]], + EqFA: Eq[F[A]], + 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) } + +} From ada80ee27c8b0c1f48b8f3500e54d55ffa468de3 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 19 Feb 2020 18:29:52 +0000 Subject: [PATCH 3/8] Check that the (non-rigid) Validation selective fails rigid laws TODO: revert this commit prior to merge request --- tests/src/test/scala/cats/tests/ValidatedSuite.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/src/test/scala/cats/tests/ValidatedSuite.scala b/tests/src/test/scala/cats/tests/ValidatedSuite.scala index 1515ae7..7602b51 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, Int]) } From bbbc75743cb125530262dab59e3d61edba36f684 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 19 Feb 2020 18:31:24 +0000 Subject: [PATCH 4/8] Test that Eval satisifes the rigid selective laws --- tests/src/test/scala/cats/tests/EvalSuite.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/src/test/scala/cats/tests/EvalSuite.scala 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..ba27acd --- /dev/null +++ b/tests/src/test/scala/cats/tests/EvalSuite.scala @@ -0,0 +1,17 @@ +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]", RigidSelectiveTests[Eval].selective[Int, Int, Int]) +} From b24965ef2ee56af2e8cc52c94a8e34e706517cdb Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 19 Feb 2020 18:32:31 +0000 Subject: [PATCH 5/8] Also check that Eval satisfies the standard selective laws --- tests/src/test/scala/cats/tests/EvalSuite.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/src/test/scala/cats/tests/EvalSuite.scala b/tests/src/test/scala/cats/tests/EvalSuite.scala index ba27acd..1c21cc1 100644 --- a/tests/src/test/scala/cats/tests/EvalSuite.scala +++ b/tests/src/test/scala/cats/tests/EvalSuite.scala @@ -13,5 +13,6 @@ 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, Int]) } From f67b5a5ff797d0dda5a2292b0649b713101be106 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Sat, 11 Apr 2020 21:36:49 +0100 Subject: [PATCH 6/8] Tidy up RigidSelectiveTests by removing unnecessary implicit params --- laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala | 5 ++--- .../cats/laws/discipline/RigidSelectiveTests.scala | 10 ++-------- tests/src/test/scala/cats/tests/EvalSuite.scala | 2 +- tests/src/test/scala/cats/tests/ValidatedSuite.scala | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala b/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala index d204c95..724105f 100644 --- a/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala +++ b/laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala @@ -13,7 +13,7 @@ trait RigidSelectiveLaws[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]] = { @@ -23,8 +23,7 @@ trait RigidSelectiveLaws[F[_]] { } /** - * A consequence of `applicative.ap === apS` and associativity, so don't strictly - * need to test this law, but I'm writing it as a sanity check + * A consequence of `applicative.ap === apS` and associativity * * `x *> (y <*? z) === (x *> y) <*? z */ diff --git a/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala b/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala index c18b7e9..9132a13 100644 --- a/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala @@ -2,24 +2,18 @@ package cats package laws package discipline -import cats.laws.discipline.SemigroupalTests.Isomorphisms import org.scalacheck.Prop._ -import org.scalacheck.{Arbitrary, Cogen} +import org.scalacheck.Arbitrary import org.typelevel.discipline.Laws trait RigidSelectiveTests[F[_]] extends Laws { def laws: RigidSelectiveLaws[F] - // TODO - probably don't need all these implicit args. Tidy up - def selective[A: Arbitrary, B: Arbitrary, C: Arbitrary]( + def selective[A: Arbitrary, B: Arbitrary]( implicit ArbFA: Arbitrary[F[A]], - ArbFEitherA: Arbitrary[F[Either[A, A]]], ArbFEitherAB: Arbitrary[F[Either[A, B]]], - ArbFEitherCAtoB: Arbitrary[F[Either[C, A => B]]], ArbFAtoB: Arbitrary[F[A => B]], - ArbFCtoAtoB: Arbitrary[F[C => A => B]], - EqFA: Eq[F[A]], EqFB: Eq[F[B]] ): RuleSet = new DefaultRuleSet( diff --git a/tests/src/test/scala/cats/tests/EvalSuite.scala b/tests/src/test/scala/cats/tests/EvalSuite.scala index 1c21cc1..fd0cc46 100644 --- a/tests/src/test/scala/cats/tests/EvalSuite.scala +++ b/tests/src/test/scala/cats/tests/EvalSuite.scala @@ -14,5 +14,5 @@ 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, Int]) + checkAll("Eval[Int]", RigidSelectiveTests[Eval].selective[Int, Int]) } diff --git a/tests/src/test/scala/cats/tests/ValidatedSuite.scala b/tests/src/test/scala/cats/tests/ValidatedSuite.scala index 7602b51..6970936 100644 --- a/tests/src/test/scala/cats/tests/ValidatedSuite.scala +++ b/tests/src/test/scala/cats/tests/ValidatedSuite.scala @@ -14,5 +14,5 @@ class ValidatedSuite extends CatsSuite { // 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, Int]) + // checkAll("Validated[String, Int]", RigidSelectiveTests[Validated[String, ?]].selective[Int, Int]) } From cbed7f11293ff4d055876b256e11705fb66e6438 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Sat, 11 Apr 2020 22:22:15 +0100 Subject: [PATCH 7/8] Move `apS` to the bottom of the combinators defintions It's probably not one of the first that a user might reach for. --- core/src/main/scala/cats/Selective.scala | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/core/src/main/scala/cats/Selective.scala b/core/src/main/scala/cats/Selective.scala index bb4370a..635a6f4 100644 --- a/core/src/main/scala/cats/Selective.scala +++ b/core/src/main/scala/cats/Selective.scala @@ -19,22 +19,6 @@ trait Selective[F[_]] { select(lhs)(r) } - /** - * 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) - } - def ifS[A](x: F[Boolean])(t: F[A])(e: F[A]): F[A] = { val condition: F[Either[Unit, Unit]] = map(x)(p => if (p) Left(()) else Right(())) val left: F[Unit => A] = map(t)(Function.const) @@ -71,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 } From f8e831014e6ecbca94b86638bfc2c35a35d4fc69 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Sat, 11 Apr 2020 22:23:01 +0100 Subject: [PATCH 8/8] Basic test for `apS` --- tests/src/test/scala/cats/tests/SelectiveSuite.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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)) + } + } + } + }