Skip to content
Open
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
16 changes: 16 additions & 0 deletions core/src/main/scala/cats/Selective.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
41 changes: 41 additions & 0 deletions laws/src/main/scala/cats/laws/RigidSelectiveLaws.scala
Original file line number Diff line number Diff line change
@@ -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 }
}
35 changes: 35 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/RigidSelectiveTests.scala
Original file line number Diff line number Diff line change
@@ -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) }

}
18 changes: 18 additions & 0 deletions tests/src/test/scala/cats/tests/EvalSuite.scala
Original file line number Diff line number Diff line change
@@ -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])
}
12 changes: 12 additions & 0 deletions tests/src/test/scala/cats/tests/SelectiveSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}

}
3 changes: 3 additions & 0 deletions tests/src/test/scala/cats/tests/ValidatedSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}