From 186dbabfbdcc6448823d35131e483b9688da6a0b Mon Sep 17 00:00:00 2001 From: Eli Jordan Date: Tue, 10 Jul 2018 06:04:22 +1000 Subject: [PATCH] Add Representable Functor (#2284) * Add Representable Functor and RepresentableStore * code review updates * extend AnyVal for representable syntax * updates to RepresentableSyntax --- core/src/main/scala/cats/Representable.scala | 90 +++++++++++++++++++ core/src/main/scala/cats/data/Kleisli.scala | 23 +++++ .../scala/cats/data/RepresentableStore.scala | 61 +++++++++++++ core/src/main/scala/cats/data/package.scala | 7 ++ core/src/main/scala/cats/implicits.scala | 1 + core/src/main/scala/cats/instances/all.scala | 4 + .../main/scala/cats/instances/function.scala | 12 +++ .../main/scala/cats/instances/package.scala | 4 +- .../src/main/scala/cats/instances/tuple.scala | 23 +++++ core/src/main/scala/cats/package.scala | 12 +++ core/src/main/scala/cats/syntax/all.scala | 1 + core/src/main/scala/cats/syntax/package.scala | 1 + .../scala/cats/syntax/representable.scala | 18 ++++ .../scala/cats/laws/RepresentableLaws.scala | 24 +++++ .../cats/laws/discipline/Arbitrary.scala | 21 +++++ .../main/scala/cats/laws/discipline/Eq.scala | 6 +- .../laws/discipline/RepresentableTests.scala | 32 +++++++ .../src/main/scala/cats/tests/CatsSuite.scala | 8 +- .../cats/tests/RepresentableStoreSuite.scala | 39 ++++++++ .../scala/cats/tests/RepresentableSuite.scala | 80 +++++++++++++++++ 20 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 core/src/main/scala/cats/Representable.scala create mode 100644 core/src/main/scala/cats/data/RepresentableStore.scala create mode 100644 core/src/main/scala/cats/syntax/representable.scala create mode 100644 laws/src/main/scala/cats/laws/RepresentableLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/RepresentableTests.scala create mode 100644 tests/src/test/scala/cats/tests/RepresentableStoreSuite.scala create mode 100644 tests/src/test/scala/cats/tests/RepresentableSuite.scala diff --git a/core/src/main/scala/cats/Representable.scala b/core/src/main/scala/cats/Representable.scala new file mode 100644 index 0000000000..336dc0dd62 --- /dev/null +++ b/core/src/main/scala/cats/Representable.scala @@ -0,0 +1,90 @@ +package cats + +/** + * Representable. + * + * Is a witness to the isomorphism forall A. F[A] <-> Representation => A + * + * Must obey the laws defined in cats.laws.RepresentableLaws + * i.e. + * tabulate andThen index = identity + * index andThen tabulate = identity + * + * Inspired by the Haskell representable package + * http://hackage.haskell.org/package/representable-functors-3.2.0.2/docs/Data-Functor-Representable.html + */ +trait Representable[F[_]] extends Serializable { + + def F: Functor[F] + + type Representation + + /** + * Create a function that "indexes" into the `F` structure using `Representation` + */ + def index[A](f: F[A]): Representation => A + + /** + * Reconstructs the `F` structure using the index function + */ + def tabulate[A](f: Representation => A): F[A] +} + +private trait RepresentableMonad[F[_], R] extends Monad[F] { + + def R: Representable.Aux[F, R] + + override def pure[A](x: A): F[A] = R.tabulate(_ => x) + + override def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = + R.tabulate(a => R.index(f(R.index(fa)(a)))(a)) + + override def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B] = { + R.tabulate { r: R => + @annotation.tailrec + def loop(a: A): B = + R.index(f(a))(r) match { + case Right(b) => b + case Left(a) => loop(a) + } + + loop(a) + } + } +} + +private trait RepresentableBimonad[F[_], R] extends RepresentableMonad[F, R] with Bimonad[F] { + + def M: Monoid[R] + + override def coflatMap[A, B](w: F[A])(f: F[A] => B): F[B] = + R.tabulate(m => f(R.tabulate(x => R.index(w)(M.combine(m, x))))) + + override def extract[A](fa: F[A]): A = + R.index(fa)(M.empty) +} + +object Representable { + type Aux[F[_], R] = Representable[F] { type Representation = R } + + /** + * Summon the `Representable` instance for `F` + */ + def apply[F[_]](implicit ev: Representable[F]): Representable[F] = ev + + /** + * Derives a `Monad` instance for any `Representable` functor + */ + def monad[F[_]](implicit Rep: Representable[F]): Monad[F] = new RepresentableMonad[F, Rep.Representation] { + override def R: Representable.Aux[F, Rep.Representation] = Rep + } + + /** + * Derives a `Bimonad` instance for any `Representable` functor whos representation + * has a `Monoid` instance. + */ + def bimonad[F[_], R](implicit Rep: Representable.Aux[F, R], Mon: Monoid[R]): Bimonad[F] = new RepresentableBimonad[F, R] { + override def R: Representable.Aux[F, R] = Rep + override def M: Monoid[R] = Mon + } +} diff --git a/core/src/main/scala/cats/data/Kleisli.scala b/core/src/main/scala/cats/data/Kleisli.scala index dcff4135c6..327a5dd728 100644 --- a/core/src/main/scala/cats/data/Kleisli.scala +++ b/core/src/main/scala/cats/data/Kleisli.scala @@ -185,6 +185,29 @@ private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1 implicit def catsDataContravariantMonoidalForKleisli[F[_], A](implicit F0: ContravariantMonoidal[F]): ContravariantMonoidal[Kleisli[F, A, ?]] = new KleisliContravariantMonoidal[F, A] { def F: ContravariantMonoidal[F] = F0 } + + /** + * Witness for: Kleisli[M, E, A] <-> (E, R) => A + * if M is Representable + */ + implicit def catsDataRepresentableForKleisli[M[_], R, E](implicit + R: Representable.Aux[M, R], + FK: Functor[Kleisli[M, E, ?]]): Representable.Aux[Kleisli[M, E, ?], (E, R)] = new Representable[Kleisli[M, E, ?]] { + + override type Representation = (E, R) + + override val F: Functor[Kleisli[M, E, ?]] = FK + + def index[A](f: Kleisli[M, E, A]): Representation => A = { + case (e, r) => R.index(f.run(e))(r) + } + + def tabulate[A](f: Representation => A): Kleisli[M, E, A] = { + def curry[X, Y, Z](f: (X, Y) => Z): X => Y => Z = x => y => f(x, y) + + Kleisli[M, E, A](curry(Function.untupled(f)) andThen R.tabulate) + } + } } private[data] sealed abstract class KleisliInstances1 extends KleisliInstances2 { diff --git a/core/src/main/scala/cats/data/RepresentableStore.scala b/core/src/main/scala/cats/data/RepresentableStore.scala new file mode 100644 index 0000000000..4a0349e221 --- /dev/null +++ b/core/src/main/scala/cats/data/RepresentableStore.scala @@ -0,0 +1,61 @@ +package cats.data + +import cats.{Comonad, Functor, Representable} + +/** + * A generalisation of the Store comonad, for any `Representable` functor. + * `Store` is the dual of `State` + */ +final case class RepresentableStore[F[_], S, A](fa: F[A], index: S)(implicit R: Representable.Aux[F, S]) { + /** + * Inspect the value at "index" s + */ + def peek(s: S): A = R.index(fa)(s) + + /** + * Extract the value at the current index. + */ + lazy val extract: A = peek(index) + + /** + * Duplicate the store structure + */ + lazy val coflatten: RepresentableStore[F, S, RepresentableStore[F, S, A]] = + RepresentableStore(R.tabulate(idx => RepresentableStore(fa, idx)), index) + + def map[B](f: A => B): RepresentableStore[F, S, B] = { + RepresentableStore(R.F.map(fa)(f), index) + } + + /** + * Given a functorial computation on the index `S` peek at the value in that functor. + * + * {{{ + * import cats._, implicits._, data.Store + * + * val initial = List("a", "b", "c") + * val store = Store(idx => initial.get(idx).getOrElse(""), 0) + * val adjacent = store.experiment[List] { idx => List(idx - 1, idx, idx + 1) } + * + * require(adjacent == List("", "a", "b")) + * }}} + */ + def experiment[G[_]](fn: S => G[S])(implicit G: Functor[G]): G[A] = { + G.map(fn(index))(peek) + } +} + +object RepresentableStore { + + implicit def catsDataRepresentableStoreComonad[F[_], S](implicit R: Representable[F]): Comonad[RepresentableStore[F, S, ?]] = + new Comonad[RepresentableStore[F, S, ?]] { + override def extract[B](x: RepresentableStore[F, S, B]): B = + x.extract + + override def coflatMap[A, B](fa: RepresentableStore[F, S, A])(f: RepresentableStore[F, S, A] => B): RepresentableStore[F, S, B] = + fa.coflatten.map(f) + + override def map[A, B](fa: RepresentableStore[F, S, A])(f: A => B): RepresentableStore[F, S, B] = + fa.map(f) + } +} diff --git a/core/src/main/scala/cats/data/package.scala b/core/src/main/scala/cats/data/package.scala index 192f571c98..19d25bd21c 100644 --- a/core/src/main/scala/cats/data/package.scala +++ b/core/src/main/scala/cats/data/package.scala @@ -66,4 +66,11 @@ package object data { type RWS[E, L, S, A] = ReaderWriterState[E, L, S, A] val RWS = ReaderWriterState + + type Store[S, A] = RepresentableStore[S => ?, S, A] + object Store { + import cats.instances.function._ + def apply[S, A](f: S => A, s: S): Store[S, A] = + RepresentableStore[S => ?, S, A](f, s) + } } diff --git a/core/src/main/scala/cats/implicits.scala b/core/src/main/scala/cats/implicits.scala index 6db2f449b9..edc25bf7e8 100644 --- a/core/src/main/scala/cats/implicits.scala +++ b/core/src/main/scala/cats/implicits.scala @@ -5,3 +5,4 @@ object implicits with syntax.AllSyntaxBinCompat0 with syntax.AllSyntaxBinCompat1 with instances.AllInstances + with instances.AllInstancesBinCompat0 diff --git a/core/src/main/scala/cats/instances/all.scala b/core/src/main/scala/cats/instances/all.scala index 3e08de757b..d8c3361fa2 100644 --- a/core/src/main/scala/cats/instances/all.scala +++ b/core/src/main/scala/cats/instances/all.scala @@ -32,3 +32,7 @@ trait AllInstances with TupleInstances with UUIDInstances with VectorInstances + +trait AllInstancesBinCompat0 + extends FunctionInstancesBinCompat0 + with Tuple2InstancesBinCompat0 diff --git a/core/src/main/scala/cats/instances/function.scala b/core/src/main/scala/cats/instances/function.scala index aa3374e8c2..5be89d7ce1 100644 --- a/core/src/main/scala/cats/instances/function.scala +++ b/core/src/main/scala/cats/instances/function.scala @@ -10,6 +10,18 @@ import annotation.tailrec trait FunctionInstances extends cats.kernel.instances.FunctionInstances with Function0Instances with Function1Instances +trait FunctionInstancesBinCompat0 { + /** + * Witness for: E => A <-> E => A + */ + implicit def catsStdRepresentableForFunction1[E](implicit EF: Functor[E => ?]): Representable.Aux[E => ?, E] = new Representable[E => ?] { + override type Representation = E + override val F: Functor[E => ?] = EF + override def tabulate[A](f: E => A): E => A = f + override def index[A](f: E => A): E => A = f + } +} + private[instances] sealed trait Function0Instances extends Function0Instances0 { implicit val catsStdBimonadForFunction0: Bimonad[Function0] = new Bimonad[Function0] { diff --git a/core/src/main/scala/cats/instances/package.scala b/core/src/main/scala/cats/instances/package.scala index 1a942e3567..13f70fc8ee 100644 --- a/core/src/main/scala/cats/instances/package.scala +++ b/core/src/main/scala/cats/instances/package.scala @@ -1,7 +1,7 @@ package cats package object instances { - object all extends AllInstances + object all extends AllInstances with AllInstancesBinCompat0 object bigInt extends BigIntInstances object bigDecimal extends BigDecimalInstances object bitSet extends BitSetInstances @@ -15,6 +15,7 @@ package object instances { object equiv extends EquivInstances object float extends FloatInstances object function extends FunctionInstances + with FunctionInstancesBinCompat0 object future extends FutureInstances object int extends IntInstances object invariant extends InvariantMonoidalInstances @@ -36,6 +37,7 @@ package object instances { object string extends StringInstances object try_ extends TryInstances object tuple extends TupleInstances + with Tuple2InstancesBinCompat0 object unit extends UnitInstances object uuid extends UUIDInstances object vector extends VectorInstances diff --git a/core/src/main/scala/cats/instances/tuple.scala b/core/src/main/scala/cats/instances/tuple.scala index 2e8501a3f1..53cd07a3c4 100644 --- a/core/src/main/scala/cats/instances/tuple.scala +++ b/core/src/main/scala/cats/instances/tuple.scala @@ -2,10 +2,33 @@ package cats package instances import cats.kernel.{CommutativeMonoid, CommutativeSemigroup} + import scala.annotation.tailrec trait TupleInstances extends Tuple2Instances with cats.kernel.instances.TupleInstances +trait Tuple2InstancesBinCompat0 { + + /** + * Witness for: (A, A) <-> Boolean => A + */ + implicit def catsDataRepresentableForPair(implicit PF: Functor[λ[P => (P, P)]]): Representable.Aux[λ[P => (P, P)], Boolean] = new Representable[λ[P => (P, P)]] { + override type Representation = Boolean + override val F: Functor[λ[P => (P, P)]] = PF + + override def tabulate[A](f: Boolean => A): (A, A) = (f(true), f(false)) + + override def index[A](pair: (A, A)): Boolean => A = { + case true => pair._1 + case false => pair._2 + } + } + + implicit val catsDataFunctorForPair: Functor[λ[P => (P, P)]] = new Functor[λ[P => (P, P)]] { + override def map[A, B](fa: (A, A))(f: A => B): (B, B) = (f(fa._1), f(fa._2)) + } +} + sealed trait Tuple2Instances extends Tuple2Instances1 { implicit val catsStdBitraverseForTuple2: Bitraverse[Tuple2] = new Bitraverse[Tuple2] { diff --git a/core/src/main/scala/cats/package.scala b/core/src/main/scala/cats/package.scala index 33d47be4b4..308e5480b2 100644 --- a/core/src/main/scala/cats/package.scala +++ b/core/src/main/scala/cats/package.scala @@ -77,6 +77,18 @@ package object cats { override def isEmpty[A](fa: Id[A]): Boolean = false } + /** + * Witness for: Id[A] <-> Unit => A + */ + implicit val catsRepresentableForId: Representable.Aux[Id, Unit] = new Representable[Id] { + override type Representation = Unit + override val F: Functor[Id] = Functor[Id] + + override def tabulate[A](f: Unit => A): Id[A] = f(()) + + override def index[A](f: Id[A]): Unit => A = (_: Unit) => f + } + implicit val catsParallelForId: Parallel[Id, Id] = Parallel.identity type Eq[A] = cats.kernel.Eq[A] diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 72f7734d81..8c49c5d0f5 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -67,3 +67,4 @@ trait AllSyntaxBinCompat1 with ParallelFlatSyntax with SetSyntax with ValidatedExtensionSyntax + with RepresentableSyntax diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index 355433df4a..1149747784 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -42,6 +42,7 @@ package object syntax { object partialOrder extends PartialOrderSyntax object profunctor extends ProfunctorSyntax object reducible extends ReducibleSyntax + object representable extends RepresentableSyntax object semigroup extends SemigroupSyntax object semigroupal extends SemigroupalSyntax object semigroupk extends SemigroupKSyntax diff --git a/core/src/main/scala/cats/syntax/representable.scala b/core/src/main/scala/cats/syntax/representable.scala new file mode 100644 index 0000000000..e329c8c3a9 --- /dev/null +++ b/core/src/main/scala/cats/syntax/representable.scala @@ -0,0 +1,18 @@ +package cats +package syntax + +trait RepresentableSyntax { + implicit final def catsSyntaxTabulate[A, R](f: R => A): TabulateOps[A, R] = + new TabulateOps[A, R](f) + + implicit final def catsSyntaxIndex[F[_], A](fa: F[A]): IndexOps[F, A] = + new IndexOps[F, A](fa) +} + +final class IndexOps[F[_], A](val fa: F[A]) extends AnyVal { + def index[R](implicit R: Representable.Aux[F, R]): R => A = R.index(fa) +} + +final class TabulateOps[A, R](val f: R => A) extends AnyVal { + def tabulate[F[_]](implicit R: Representable.Aux[F, R]): F[A] = R.tabulate(f) +} diff --git a/laws/src/main/scala/cats/laws/RepresentableLaws.scala b/laws/src/main/scala/cats/laws/RepresentableLaws.scala new file mode 100644 index 0000000000..ed997a2ef4 --- /dev/null +++ b/laws/src/main/scala/cats/laws/RepresentableLaws.scala @@ -0,0 +1,24 @@ +package cats +package laws + + +/** + * Laws that must be obeyed by any `Representable` functor. + */ +trait RepresentableLaws[F[_], R] { + + implicit val R: Representable.Aux[F, R] + + def indexTabulateIsId[B](fb: F[B]): IsEq[F[B]] = { + R.tabulate(R.index(fb)) <-> fb + } + + def tabulateIndexIsId[B](f: R => B, x: R): IsEq[B] = { + R.index(R.tabulate(f))(x) <-> f(x) + } +} + +object RepresentableLaws { + def apply[F[_], R](implicit ev: Representable.Aux[F, R]): RepresentableLaws[F, R] = + new RepresentableLaws[F, R] { val R: Representable.Aux[F, R] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 4fa3633025..c7d077dfd6 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -250,11 +250,32 @@ object arbitrary extends ArbitraryInstances0 { F: Arbitrary[(E, SA) => F[(L, SB, A)]]): Arbitrary[IndexedReaderWriterStateT[F, E, L, SA, SB, A]] = Arbitrary(F.arbitrary.map(IndexedReaderWriterStateT(_))) + + implicit def catsLawsArbitraryForRepresentableStore[F[_], S, A](implicit + R: Representable.Aux[F, S], + ArbS: Arbitrary[S], + ArbFA: Arbitrary[F[A]] + ): Arbitrary[RepresentableStore[F, S, A]] = { + Arbitrary { + for { + fa <- ArbFA.arbitrary + s <- ArbS.arbitrary + } yield { + RepresentableStore[F, S, A](fa, s) + } + } + } + + implicit def catsLawsCogenForRepresentableStore[F[_]: Representable, S, A](implicit CA: Cogen[A]): Cogen[RepresentableStore[F, S, A]] = { + CA.contramap(_.extract) + } + implicit def catsLawsArbitraryForAndThen[A, B](implicit F: Arbitrary[A => B]): Arbitrary[AndThen[A, B]] = Arbitrary(F.arbitrary.map(AndThen(_))) implicit def catsLawsCogenForAndThen[A, B](implicit F: Cogen[A => B]): Cogen[AndThen[A, B]] = Cogen((seed, x) => F.perturb(seed, x)) + } private[discipline] sealed trait ArbitraryInstances0 { diff --git a/laws/src/main/scala/cats/laws/discipline/Eq.scala b/laws/src/main/scala/cats/laws/discipline/Eq.scala index 3993ccbac7..6c7b7c6098 100644 --- a/laws/src/main/scala/cats/laws/discipline/Eq.scala +++ b/laws/src/main/scala/cats/laws/discipline/Eq.scala @@ -3,6 +3,8 @@ package laws package discipline import catalysts.Platform + +import cats.data.RepresentableStore import cats.Eq import cats.data.AndThen import cats.instances.boolean._ @@ -163,5 +165,7 @@ object eq { implicit def catsLawsEqForCommutativeGroup[A](implicit eqMA: Eq[CommutativeMonoid[A]], eqGA: Eq[Group[A]], eqA: Eq[A]): Eq[CommutativeGroup[A]] = Eq.instance((f, g) => eqMA.eqv(f, g) && eqGA.eqv(f, g)) - + implicit def catsLawsEqForRepresentableStore[F[_]: Representable, S, A](implicit eqFA: Eq[F[A]], eqS: Eq[S]): Eq[RepresentableStore[F, S, A]] = { + Eq.instance((s1, s2) => eqFA.eqv(s1.fa, s2.fa) && eqS.eqv(s1.index, s2.index)) + } } diff --git a/laws/src/main/scala/cats/laws/discipline/RepresentableTests.scala b/laws/src/main/scala/cats/laws/discipline/RepresentableTests.scala new file mode 100644 index 0000000000..f62bd9e53f --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/RepresentableTests.scala @@ -0,0 +1,32 @@ +package cats.laws.discipline + +import cats.{Eq, Representable} +import cats.laws.RepresentableLaws +import org.scalacheck.Arbitrary +import org.typelevel.discipline.Laws +import org.scalacheck.Prop._ + +trait RepresentableTests[F[_], R] extends Laws { + + val laws: RepresentableLaws[F, R] + + def representable[A](implicit + ArbA: Arbitrary[A], + ArbFA: Arbitrary[F[A]], + ArbRep: Arbitrary[R], + ArbRepFn: Arbitrary[R => A], + EqFA: Eq[F[A]], + EqA: Eq[A] + ): RuleSet = new DefaultRuleSet( + name = "representable", + parent = None, + "index andThen tabulate = id" -> forAll(laws.indexTabulateIsId[A] _), + "tabulate andThen index = id" -> forAll(laws.tabulateIndexIsId[A] _) + ) +} + +object RepresentableTests { + def apply[F[_], R](implicit RF: Representable.Aux[F, R]): RepresentableTests[F, R] = new RepresentableTests[F, R] { + override implicit val laws: RepresentableLaws[F, R] = RepresentableLaws[F, R] + } +} diff --git a/testkit/src/main/scala/cats/tests/CatsSuite.scala b/testkit/src/main/scala/cats/tests/CatsSuite.scala index 63124f8923..a5195dcec8 100644 --- a/testkit/src/main/scala/cats/tests/CatsSuite.scala +++ b/testkit/src/main/scala/cats/tests/CatsSuite.scala @@ -2,11 +2,9 @@ package cats package tests import catalysts.Platform - -import cats.instances.AllInstances +import cats.instances.{AllInstances, AllInstancesBinCompat0} import cats.syntax.{AllSyntax, AllSyntaxBinCompat0, AllSyntaxBinCompat1, EqOps} - -import org.scalactic.anyvals.{PosZDouble, PosInt, PosZInt} +import org.scalactic.anyvals.{PosInt, PosZDouble, PosZInt} import org.scalatest.{FunSuite, FunSuiteLike, Matchers} import org.scalatest.prop.{Configuration, GeneratorDrivenPropertyChecks} import org.typelevel.discipline.scalatest.Discipline @@ -35,7 +33,7 @@ trait CatsSuite extends FunSuite with GeneratorDrivenPropertyChecks with Discipline with TestSettings - with AllInstances + with AllInstances with AllInstancesBinCompat0 with AllSyntax with AllSyntaxBinCompat0 with AllSyntaxBinCompat1 with StrictCatsEquality { self: FunSuiteLike => diff --git a/tests/src/test/scala/cats/tests/RepresentableStoreSuite.scala b/tests/src/test/scala/cats/tests/RepresentableStoreSuite.scala new file mode 100644 index 0000000000..2a9b08294c --- /dev/null +++ b/tests/src/test/scala/cats/tests/RepresentableStoreSuite.scala @@ -0,0 +1,39 @@ +package cats.tests + +import cats.Comonad +import cats.laws.discipline.{ComonadTests, SerializableTests} +import cats.laws.discipline.arbitrary._ +import cats.data.RepresentableStore +import cats.data.Store + +class RepresentableStoreSuite extends CatsSuite { + + // Note: The Eq instance for Function1 causes this to run excessively long, and timeout the travis build + // checkAll("Comonad[Store[String, ?]]", ComonadTests[Store[String, ?]].comonad[Int, Int, Int]) + + { + implicit val pairComonad = RepresentableStore.catsDataRepresentableStoreComonad[λ[P => (P, P)], Boolean] + implicit val arbStore = catsLawsArbitraryForRepresentableStore[λ[P => (P, P)], Boolean, Int] + implicit val cogenStore = catsLawsCogenForRepresentableStore[λ[P => (P, P)], Boolean, Int] + implicit val eqStore = cats.laws.discipline.eq.catsLawsEqForRepresentableStore[λ[P => (P, P)], Boolean, Int] + implicit val eqStoreStore = cats.laws.discipline.eq.catsLawsEqForRepresentableStore[λ[P => (P, P)], Boolean, RepresentableStore[λ[P => (P, P)], Boolean, Int]] + implicit val eqStoreStoreStore = cats.laws.discipline.eq.catsLawsEqForRepresentableStore[λ[P => (P, P)], Boolean, RepresentableStore[λ[P => (P, P)], Boolean, RepresentableStore[λ[P => (P, P)], Boolean, Int]]] + checkAll("Comonad[RepresentableStore[λ[P => (P, P)], Boolean, ?]]", ComonadTests[RepresentableStore[λ[P => (P, P)], Boolean, ?]].comonad[Int, Int, Int]) + + checkAll("Comonad[RepresentableStore[λ[P => (P, P)], Boolean, ?]]", SerializableTests.serializable(Comonad[RepresentableStore[λ[P => (P, P)], Boolean, ?]])) + } + + + test("extract and peek are consistent") { + forAll { (store: Store[String, String]) => + store.extract should === (store.peek(store.index)) + } + } + + test("use store alias constructor") { + forAll { (f: String => Int, s: String) => + val store = Store(f, s) + store.extract should === (f(s)) + } + } +} \ No newline at end of file diff --git a/tests/src/test/scala/cats/tests/RepresentableSuite.scala b/tests/src/test/scala/cats/tests/RepresentableSuite.scala new file mode 100644 index 0000000000..e4186bd827 --- /dev/null +++ b/tests/src/test/scala/cats/tests/RepresentableSuite.scala @@ -0,0 +1,80 @@ +package cats.tests + +import cats.laws.discipline.SemigroupalTests.Isomorphisms +import cats.laws.discipline.arbitrary._ +import cats.laws.discipline.eq._ +import cats.laws.discipline.{BimonadTests, MonadTests, RepresentableTests, SerializableTests} +import cats.{Bimonad, Eq, Id, Representable} +import org.scalacheck.Arbitrary +import cats.data.Kleisli + +class RepresentableSuite extends CatsSuite { + + type Pair[A] = (A, A) + + checkAll("Id[String] <-> Unit => String", RepresentableTests[Id, Unit].representable[String]) + checkAll("Representable[Id]", SerializableTests.serializable(Representable[Id])) + + checkAll("String => Int <-> String => Int", RepresentableTests[String => ?, String].representable[Int]) + checkAll("Representable[String => ?]", SerializableTests.serializable(Representable[String => ?])) + + checkAll("Pair[String, String] <-> Boolean => String", RepresentableTests[Pair, Boolean].representable[String]) + checkAll("Representable[Pair]", SerializableTests.serializable(Representable[Pair])) + + + { + implicit val representableKleisliPair = Kleisli.catsDataRepresentableForKleisli[Pair, Boolean, String] + + implicit def kleisliEq[F[_], A, B](implicit A: Arbitrary[A], FB: Eq[F[B]]): Eq[Kleisli[F, A, B]] = + Eq.by[Kleisli[F, A, B], A => F[B]](_.run) + + checkAll("Kleisli[Pair, String, Int] <-> (String, Boolean) => Int", + + // Have to summon all implicits using 'implicitly' otherwise we get a diverging implicits error + RepresentableTests[Kleisli[Pair, String, ?], (String, Boolean)].representable[Int]( + implicitly[Arbitrary[Int]], + implicitly[Arbitrary[Kleisli[Pair, String, Int]]], + implicitly[Arbitrary[(String, Boolean)]], + implicitly[Arbitrary[((String, Boolean)) => Int]], + implicitly[Eq[Kleisli[Pair, String, Int]]], + implicitly[Eq[Int]] + ) + ) + + checkAll("Representable[Kleisli[Pair, String, ?]]", SerializableTests.serializable(Representable[Kleisli[Pair, String, ?]])) + } + + { + implicit val andMonoid = new cats.Monoid[Boolean] { + def empty: Boolean = true + override def combine(x: Boolean, y: Boolean): Boolean = x && y + } + + implicit val isoPair = Isomorphisms.invariant[Pair] + implicit val bimonadInstance = Representable.bimonad[Pair, Boolean] + checkAll("Pair[Int]", BimonadTests[Pair].bimonad[Int, Int, Int]) + checkAll("Bimonad[Pair]", SerializableTests.serializable(Bimonad[Pair])) + } + + { + implicit val monadInstance = Representable.monad[String => ?] + checkAll("String => ?", MonadTests[String => ?].monad[String, String, String]) + } + + // Syntax tests. If it compiles is "passes" + { + // Pair + val pair: Pair[Int] = (3, 5) + val indexedPair: Boolean => Int = pair.index + val tabulatedPair = indexedPair.tabulate[Pair] + + // Function + val function: String => Int = _.length + val indexedFunction = function.index + val tabulatedFunction = indexedFunction.tabulate + } +} + + + +