From e5fca9ddfec7c2de81457c1543bfd1a4022699a0 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 13:02:31 +0000 Subject: [PATCH 01/15] Add StoreT --- core/src/main/scala/cats/data/StoreT.scala | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 core/src/main/scala/cats/data/StoreT.scala diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala new file mode 100644 index 0000000000..20ce89e827 --- /dev/null +++ b/core/src/main/scala/cats/data/StoreT.scala @@ -0,0 +1,78 @@ +package cats.data + +import cats.{Comonad, Functor} + +/* + * The dual of `StateT` + */ +final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { + + /** + * Get the current index. + */ + def pos: S = index + + /** + * Peek at what the focus would be for a different focus. + */ + def peek(s: S)(implicit F: Functor[F]): F[A] = F.map(runF)(_.apply(index)) + + /** + * Peek at what the focus would be if the current focus where transformed + * with the given function. + */ + def peeks(f: S => S)(implicit F: Functor[F]): F[A] = peek(f(index)) + + /* + * Set the current focus. + */ + def seek(s: S): StoreT[F, S, A] = StoreT(runF, s) + + /* + * Modify the current focus with the given function. + */ + def seeks(f: S => S): StoreT[F, S, A] = seek(f(index)) + + /** + * Extract the focus at the current index. + */ + def extract(implicit F: Comonad[F]): A = F.extract(peek(index)) + + def coflatMap[B](f: StoreT[F, S, A] => B)(implicit F: Comonad[F]): StoreT[F, S, B] = StoreT( + F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => f(StoreT(x, s))), + index + ) + + def coflatten(implicit F: Comonad[F]): StoreT[F, S, StoreT[F, S, A]] = + StoreT( + F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => StoreT(x, s)), + index + ) + + def map[B](g: A => B)(implicit F: Functor[F]): StoreT[F, S, B] = StoreT( + F.map(runF)((f: S => A) => AndThen(f).andThen(g(_))), + index + ) + +} + +object StoreT extends StoreTInstances1 { + implicit def comonadForStoreT[F[_]: Comonad, S]: Comonad[StoreT[F, S, *]] = new Comonad[StoreT[F, S, *]] { + + override def map[A, B](fa: StoreT[F, S, A])(f: A => B): StoreT[F, S, B] = fa.map(f) + + override def coflatMap[A, B](fa: StoreT[F, S, A])(f: StoreT[F, S, A] => B): StoreT[F, S, B] = + fa.coflatMap(f) + + override def extract[A](fa: StoreT[F, S, A]): A = fa.extract + + } +} + +trait StoreTInstances1 { + implicit def functorForStoreT[F[_]: Functor, S]: Functor[StoreT[F, S, *]] = new Functor[StoreT[F, S, *]] { + + override def map[A, B](fa: StoreT[F, S, A])(f: A => B): StoreT[F, S, B] = fa.map(f) + + } +} From 10ff5e021d9efdafe6d67a811d40293a3ac65ede Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 14:40:53 +0000 Subject: [PATCH 02/15] More features for StoreT --- core/src/main/scala/cats/data/StoreT.scala | 50 +++++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala index 20ce89e827..e1937de5a5 100644 --- a/core/src/main/scala/cats/data/StoreT.scala +++ b/core/src/main/scala/cats/data/StoreT.scala @@ -1,6 +1,6 @@ package cats.data -import cats.{Comonad, Functor} +import cats.{Applicative, Comonad, Functor, Monoid} /* * The dual of `StateT` @@ -15,20 +15,20 @@ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { /** * Peek at what the focus would be for a different focus. */ - def peek(s: S)(implicit F: Functor[F]): F[A] = F.map(runF)(_.apply(index)) + def peek(s: S)(implicit F: Comonad[F]): A = F.extract(F.map(runF)(_.apply(index))) /** * Peek at what the focus would be if the current focus where transformed * with the given function. */ - def peeks(f: S => S)(implicit F: Functor[F]): F[A] = peek(f(index)) + def peeks(f: S => S)(implicit F: Comonad[F]): A = peek(f(index)) - /* + /** * Set the current focus. */ def seek(s: S): StoreT[F, S, A] = StoreT(runF, s) - /* + /** * Modify the current focus with the given function. */ def seeks(f: S => S): StoreT[F, S, A] = seek(f(index)) @@ -36,27 +36,46 @@ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { /** * Extract the focus at the current index. */ - def extract(implicit F: Comonad[F]): A = F.extract(peek(index)) + def extract(implicit F: Comonad[F]): A = peek(index) def coflatMap[B](f: StoreT[F, S, A] => B)(implicit F: Comonad[F]): StoreT[F, S, B] = StoreT( F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => f(StoreT(x, s))), index ) + /** + * `coflatMap` is the dual of `flatMap` on `FlatMap`. It applies + * a value in a context to a function that takes a value + * in a context and returns a normal value. + */ def coflatten(implicit F: Comonad[F]): StoreT[F, S, StoreT[F, S, A]] = StoreT( F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => StoreT(x, s)), index ) + /** + * `coflatten` is the dual of `flatten` on `FlatMap`. Whereas flatten removes + * a layer of `F`, coflatten adds a layer of `F` + */ def map[B](g: A => B)(implicit F: Functor[F]): StoreT[F, S, B] = StoreT( F.map(runF)((f: S => A) => AndThen(f).andThen(g(_))), index ) + /** + * Given a functorial computation on the index `S` peek at the value in that functor. + */ + def experiment[G[_]](f: S => G[S])(implicit F: Comonad[F], G: Functor[G]): G[A] = + G.map(f(index))(peek(_)) + } object StoreT extends StoreTInstances1 { + + def pure[F[_], S, A](x: A)(implicit F: Applicative[F], S: Monoid[S]): StoreT[F, S, A] = + StoreT(F.pure((_: S) => x), S.empty) + implicit def comonadForStoreT[F[_]: Comonad, S]: Comonad[StoreT[F, S, *]] = new Comonad[StoreT[F, S, *]] { override def map[A, B](fa: StoreT[F, S, A])(f: A => B): StoreT[F, S, B] = fa.map(f) @@ -69,7 +88,24 @@ object StoreT extends StoreTInstances1 { } } -trait StoreTInstances1 { +trait StoreTInstances1 extends StoreTInstances2 { + implicit def applicativeForStoreT[F[_], S](implicit F: Applicative[F], S: Monoid[S]): Applicative[StoreT[F, S, *]] = + new Applicative[StoreT[F, S, *]] { + + def pure[A](x: A): StoreT[F, S, A] = StoreT.pure[F, S, A](x) + + def ap[A, B](ff: StoreT[F, S, A => B])(fa: StoreT[F, S, A]): StoreT[F, S, B] = StoreT( + F.map(F.tuple2(ff.runF, fa.runF)) { case (f, a) => + (s: S) => f(s).apply(a(s)) + }, + S.combine(ff.index, fa.index) + ) + + } +} + +trait StoreTInstances2 { + implicit def functorForStoreT[F[_]: Functor, S]: Functor[StoreT[F, S, *]] = new Functor[StoreT[F, S, *]] { override def map[A, B](fa: StoreT[F, S, A])(f: A => B): StoreT[F, S, B] = fa.map(f) From 8c4799d7271bc2c561024bd8521340a6cc27ce1b Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 14:52:32 +0000 Subject: [PATCH 03/15] Add run to StoreT --- core/src/main/scala/cats/data/RepresentableStore.scala | 2 +- core/src/main/scala/cats/data/StoreT.scala | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/data/RepresentableStore.scala b/core/src/main/scala/cats/data/RepresentableStore.scala index 6731db1dee..b0931c590f 100644 --- a/core/src/main/scala/cats/data/RepresentableStore.scala +++ b/core/src/main/scala/cats/data/RepresentableStore.scala @@ -3,7 +3,7 @@ package cats.data import cats.{Comonad, Functor, Representable} /** - * A generalisation of the Store comonad, for any `Representable` functor. + * A specialization 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]) { diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala index e1937de5a5..2588eb2e9c 100644 --- a/core/src/main/scala/cats/data/StoreT.scala +++ b/core/src/main/scala/cats/data/StoreT.scala @@ -10,7 +10,9 @@ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { /** * Get the current index. */ - def pos: S = index + val pos: S = index + + def run(implicit F: Functor[F]) = F.map(runF)(_.apply(index)) /** * Peek at what the focus would be for a different focus. From 59286b4456db505a3f04628633a035a0110497eb Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 14:56:05 +0000 Subject: [PATCH 04/15] Add ComonadicStore to package object --- core/src/main/scala/cats/data/package.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/scala/cats/data/package.scala b/core/src/main/scala/cats/data/package.scala index 96fbff90cf..25d4e10d12 100644 --- a/core/src/main/scala/cats/data/package.scala +++ b/core/src/main/scala/cats/data/package.scala @@ -112,4 +112,12 @@ package object data extends ScalaVersionSpecificPackage { def shift[A, B](f: (B => Eval[A]) => Cont[A, A]): Cont[A, B] = ContT.shiftT(f) } + + type ComonadicStore[S, A] = StoreT[Id, S, A] + + object ComonadicStore { + + def pure[S, A](x: A)(implicit S: Monoid[S]): ComonadicStore[S, A] = StoreT.pure[Id, S, A](x) + + } } From 07a96e71949a4d42adf16e62c7674af2b9e8340a Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 15:53:12 +0000 Subject: [PATCH 05/15] StoreT suite --- laws/src/main/scala/cats/laws/discipline/Eq.scala | 5 ++++- .../scala/cats/laws/discipline/arbitrary.scala | 11 +++++++++++ tests/src/test/scala/cats/tests/StoreTSuite.scala | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/src/test/scala/cats/tests/StoreTSuite.scala diff --git a/laws/src/main/scala/cats/laws/discipline/Eq.scala b/laws/src/main/scala/cats/laws/discipline/Eq.scala index ed0933890c..0ddf866bf9 100644 --- a/laws/src/main/scala/cats/laws/discipline/Eq.scala +++ b/laws/src/main/scala/cats/laws/discipline/Eq.scala @@ -2,7 +2,7 @@ package cats package laws package discipline -import cats.data.{AndThen, RepresentableStore} +import cats.data.{AndThen, RepresentableStore, StoreT} import cats.instances.boolean._ import cats.instances.int._ import cats.instances.string._ @@ -121,6 +121,9 @@ object eq { eqS: Eq[S] ): Eq[RepresentableStore[F, S, A]] = Eq.instance((s1, s2) => eqFA.eqv(s1.fa, s2.fa) && eqS.eqv(s1.index, s2.index)) + + implicit def catsLawsEqForStoreT[F[_], S, A](implicit eqF: Eq[F[S => A]], eqS: Eq[S]): Eq[StoreT[F, S, A]] = + Eq.instance((s1, s2) => eqF.eqv(s1.runF, s2.runF) && eqS.eqv(s1.index, s2.index)) } @deprecated( diff --git a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala index 2ed73146ed..1ab42ee777 100644 --- a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala @@ -154,6 +154,17 @@ object arbitrary extends ArbitraryInstances0 with ScalaVersionSpecific.Arbitrary implicit def catsLawsCogenForOptionT[F[_], A](implicit F: Cogen[F[Option[A]]]): Cogen[OptionT[F, A]] = F.contramap(_.value) + implicit def catsLawsArbitraryForStoreT[F[_], S, A](implicit F: Arbitrary[F[S => A]], S: Arbitrary[S]) = + Arbitrary( + for { + runF <- F.arbitrary + index <- S.arbitrary + } yield StoreT(runF, index) + ) + + implicit def catsLawsCogenForStoreT[F[_], S, A](implicit F: Cogen[F[S => A]], S: Cogen[S]): Cogen[StoreT[F, S, A]] = + Cogen((seed, st) => S.perturb(F.perturb(seed, st.runF), st.index)) + implicit def catsLawsArbitraryForIdT[F[_], A](implicit F: Arbitrary[F[A]]): Arbitrary[IdT[F, A]] = Arbitrary(F.arbitrary.map(IdT.apply)) diff --git a/tests/src/test/scala/cats/tests/StoreTSuite.scala b/tests/src/test/scala/cats/tests/StoreTSuite.scala new file mode 100644 index 0000000000..15626b272e --- /dev/null +++ b/tests/src/test/scala/cats/tests/StoreTSuite.scala @@ -0,0 +1,15 @@ +package cats.tests + +import cats._ +import cats.data.StoreT +import cats.laws.discipline._ +import cats.laws.discipline.arbitrary._ +import cats.laws.discipline.eq._ +import org.scalacheck.Gen +import org.scalacheck.Arbitrary + +class StoreTSuite extends CatsSuite { + + checkAll("StoreT[Id, String, *]", ComonadTests[StoreT[Id, MiniInt, *]].comonad[MiniInt, MiniInt, MiniInt]) + +} From 587b95c7fb3199d04ac960f2dbc6839af9e79285 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 16:07:46 +0000 Subject: [PATCH 06/15] Test applicative instance for StoreT --- tests/src/test/scala/cats/tests/StoreTSuite.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/src/test/scala/cats/tests/StoreTSuite.scala b/tests/src/test/scala/cats/tests/StoreTSuite.scala index 15626b272e..8032d9d8f8 100644 --- a/tests/src/test/scala/cats/tests/StoreTSuite.scala +++ b/tests/src/test/scala/cats/tests/StoreTSuite.scala @@ -1,15 +1,19 @@ package cats.tests import cats._ -import cats.data.StoreT +import cats.data.{StoreT, Validated} import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ -import org.scalacheck.Gen -import org.scalacheck.Arbitrary class StoreTSuite extends CatsSuite { - checkAll("StoreT[Id, String, *]", ComonadTests[StoreT[Id, MiniInt, *]].comonad[MiniInt, MiniInt, MiniInt]) + implicit val monoid: Monoid[MiniInt] = MiniInt.miniIntAddition + + checkAll("StoreT[Id, MiniInt, *]", ComonadTests[StoreT[Id, MiniInt, *]].comonad[MiniInt, MiniInt, MiniInt]) + + checkAll("StoreT[Validated[String, *], MiniInt, *]", + ApplicativeTests[StoreT[Validated[String, *], MiniInt, *]].applicative[MiniInt, MiniInt, MiniInt] + ) } From 84e2e17c9feeda08e332cc35174ff9d0858b7f8f Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 16:11:55 +0000 Subject: [PATCH 07/15] Serializable tests for StoreT --- tests/src/test/scala/cats/tests/StoreTSuite.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/src/test/scala/cats/tests/StoreTSuite.scala b/tests/src/test/scala/cats/tests/StoreTSuite.scala index 8032d9d8f8..1773bc6134 100644 --- a/tests/src/test/scala/cats/tests/StoreTSuite.scala +++ b/tests/src/test/scala/cats/tests/StoreTSuite.scala @@ -12,8 +12,14 @@ class StoreTSuite extends CatsSuite { checkAll("StoreT[Id, MiniInt, *]", ComonadTests[StoreT[Id, MiniInt, *]].comonad[MiniInt, MiniInt, MiniInt]) - checkAll("StoreT[Validated[String, *], MiniInt, *]", + checkAll("Comonad[StoreT[Id, MiniInt, *]]", SerializableTests.serializable(Comonad[StoreT[Id, MiniInt, *]])) + + checkAll("StoreT[Validated[String, *], MiniInt, *]]", ApplicativeTests[StoreT[Validated[String, *], MiniInt, *]].applicative[MiniInt, MiniInt, MiniInt] ) + checkAll("Comonad[StoreT[Validated[String, *], MiniInt, *]]", + SerializableTests.serializable(Applicative[StoreT[Validated[String, *], MiniInt, *]]) + ) + } From 96996f386a4a7cf6f5d8e8d333368cc8bf266e56 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 16:14:54 +0000 Subject: [PATCH 08/15] Fix scaladoc --- core/src/main/scala/cats/data/StoreT.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala index 2588eb2e9c..a7ada52282 100644 --- a/core/src/main/scala/cats/data/StoreT.scala +++ b/core/src/main/scala/cats/data/StoreT.scala @@ -40,15 +40,19 @@ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { */ def extract(implicit F: Comonad[F]): A = peek(index) + /** + * `coflatMap` is the dual of `flatMap` on `FlatMap`. It applies + * a value in a context to a function that takes a value + * in a context and returns a normal value. + */ def coflatMap[B](f: StoreT[F, S, A] => B)(implicit F: Comonad[F]): StoreT[F, S, B] = StoreT( F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => f(StoreT(x, s))), index ) /** - * `coflatMap` is the dual of `flatMap` on `FlatMap`. It applies - * a value in a context to a function that takes a value - * in a context and returns a normal value. + * `coflatten` is the dual of `flatten` on `FlatMap`. Whereas flatten removes + * a layer of `F`, coflatten adds a layer of `F` */ def coflatten(implicit F: Comonad[F]): StoreT[F, S, StoreT[F, S, A]] = StoreT( @@ -57,8 +61,7 @@ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { ) /** - * `coflatten` is the dual of `flatten` on `FlatMap`. Whereas flatten removes - * a layer of `F`, coflatten adds a layer of `F` + * Functor `map` for StoreT */ def map[B](g: A => B)(implicit F: Functor[F]): StoreT[F, S, B] = StoreT( F.map(runF)((f: S => A) => AndThen(f).andThen(g(_))), From 64194e7adafa662bb23ca2e5d88abb1a4ee06d86 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 16:32:41 +0000 Subject: [PATCH 09/15] Unify interfaces of RepresentableStore and ComonadicStore --- .../scala/cats/data/RepresentableStore.scala | 32 +++++++++++++++++-- core/src/main/scala/cats/data/StoreT.scala | 8 ++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/cats/data/RepresentableStore.scala b/core/src/main/scala/cats/data/RepresentableStore.scala index b0931c590f..37ddd63f4f 100644 --- a/core/src/main/scala/cats/data/RepresentableStore.scala +++ b/core/src/main/scala/cats/data/RepresentableStore.scala @@ -9,21 +9,49 @@ import cats.{Comonad, Functor, Representable} final case class RepresentableStore[F[_], S, A](fa: F[A], index: S)(implicit R: Representable.Aux[F, S]) { /** - * Inspect the value at "index" s + * Peek at what the focus would be for a given focus s. */ def peek(s: S): A = R.index(fa)(s) + /** + * Peek at what the focus would be if the current focus where transformed + * with the given function. + */ + def peeks(f: S => S): A = peek(f(index)) + + /** + * Set the current focus. + */ + def seek(s: S): RepresentableStore[F, S, A] = RepresentableStore(fa, s) + + /** + * Modify the current focus with the given function. + */ + def seeks(f: S => S): RepresentableStore[F, S, A] = seek(f(index)) + /** * Extract the value at the current index. */ lazy val extract: A = peek(index) /** - * Duplicate the store structure + * `coflatten` is the dual of `flatten` on `FlatMap`. Whereas flatten removes + * a layer of `F`, coflatten adds a layer of `F` */ lazy val coflatten: RepresentableStore[F, S, RepresentableStore[F, S, A]] = RepresentableStore(R.tabulate(idx => RepresentableStore(fa, idx)), index) + /** + * `coflatMap` is the dual of `flatMap` on `FlatMap`. It applies + * a value in a context to a function that takes a value + * in a context and returns a normal value. + */ + def coflatMap[B](f: RepresentableStore[F, S, A] => B): RepresentableStore[F, S, B] = + coflatten.map(f) + + /** + * Functor `map` for `RepresentableStore` + */ def map[B](f: A => B): RepresentableStore[F, S, B] = RepresentableStore(R.F.map(fa)(f), index) diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala index a7ada52282..4cadfca962 100644 --- a/core/src/main/scala/cats/data/StoreT.scala +++ b/core/src/main/scala/cats/data/StoreT.scala @@ -7,15 +7,10 @@ import cats.{Applicative, Comonad, Functor, Monoid} */ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { - /** - * Get the current index. - */ - val pos: S = index - def run(implicit F: Functor[F]) = F.map(runF)(_.apply(index)) /** - * Peek at what the focus would be for a different focus. + * Peek at what the focus would be for a given focus s. */ def peek(s: S)(implicit F: Comonad[F]): A = F.extract(F.map(runF)(_.apply(index))) @@ -94,6 +89,7 @@ object StoreT extends StoreTInstances1 { } trait StoreTInstances1 extends StoreTInstances2 { + implicit def applicativeForStoreT[F[_], S](implicit F: Applicative[F], S: Monoid[S]): Applicative[StoreT[F, S, *]] = new Applicative[StoreT[F, S, *]] { From aef632df5a08f0a4d15c07ceb24f69c7e6e8a2e3 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 16:46:14 +0000 Subject: [PATCH 10/15] Tests consistent with RepresentableSuite --- tests/src/test/scala/cats/tests/StoreTSuite.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/src/test/scala/cats/tests/StoreTSuite.scala b/tests/src/test/scala/cats/tests/StoreTSuite.scala index 1773bc6134..e337912f91 100644 --- a/tests/src/test/scala/cats/tests/StoreTSuite.scala +++ b/tests/src/test/scala/cats/tests/StoreTSuite.scala @@ -5,6 +5,8 @@ import cats.data.{StoreT, Validated} import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ +import cats.syntax.eq._ +import org.scalacheck.Prop._ class StoreTSuite extends CatsSuite { @@ -22,4 +24,10 @@ class StoreTSuite extends CatsSuite { SerializableTests.serializable(Applicative[StoreT[Validated[String, *], MiniInt, *]]) ) + test("extract and peek are consistent") { + forAll { (store: StoreT[Id, String, String]) => + assert(store.extract === (store.peek(store.index))) + } + } + } From 6bec35a8feb343317ae481cd9f74255cbe713b89 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Thu, 25 Mar 2021 16:55:41 +0000 Subject: [PATCH 11/15] Scaladocs for StoreT --- core/src/main/scala/cats/data/StoreT.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala index 4cadfca962..91f8656aff 100644 --- a/core/src/main/scala/cats/data/StoreT.scala +++ b/core/src/main/scala/cats/data/StoreT.scala @@ -3,7 +3,12 @@ package cats.data import cats.{Applicative, Comonad, Functor, Monoid} /* - * The dual of `StateT` + * The dual of `StateT`. Stores some state `A` indexed by + * a type `S` with the notion of a cursor tracking the + * current position in the index. + * + * This state can be extracted if the underlying `F` has + * a Comonad instance. */ final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { From c74ed1c8849deaee200774cdf1698245bf9408d8 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Sun, 28 Mar 2021 09:38:00 +0100 Subject: [PATCH 12/15] Explicit type annotation --- core/src/main/scala/cats/data/RepresentableStore.scala | 2 +- laws/src/main/scala/cats/laws/discipline/arbitrary.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/data/RepresentableStore.scala b/core/src/main/scala/cats/data/RepresentableStore.scala index 37ddd63f4f..595627a6fe 100644 --- a/core/src/main/scala/cats/data/RepresentableStore.scala +++ b/core/src/main/scala/cats/data/RepresentableStore.scala @@ -3,7 +3,7 @@ package cats.data import cats.{Comonad, Functor, Representable} /** - * A specialization of the `Store` comonad, for any `Representable` functor. + * A generalization of `StoreT`, where the underlying functor `F` hasfor 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]) { diff --git a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala index 1ab42ee777..ebffe7568c 100644 --- a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala @@ -154,7 +154,7 @@ object arbitrary extends ArbitraryInstances0 with ScalaVersionSpecific.Arbitrary implicit def catsLawsCogenForOptionT[F[_], A](implicit F: Cogen[F[Option[A]]]): Cogen[OptionT[F, A]] = F.contramap(_.value) - implicit def catsLawsArbitraryForStoreT[F[_], S, A](implicit F: Arbitrary[F[S => A]], S: Arbitrary[S]) = + implicit def catsLawsArbitraryForStoreT[F[_], S, A](implicit F: Arbitrary[F[S => A]], S: Arbitrary[S]): Arbitrary[StoreT[F, S, A]] = Arbitrary( for { runF <- F.arbitrary From 6f6a4528c47ed4067848648bf58998fd717d2a48 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Mon, 29 Mar 2021 12:35:37 +0100 Subject: [PATCH 13/15] Generalize to RepresentableStoreT --- .../scala/cats/data/RepresentableStoreT.scala | 135 ++++++++++++++++++ core/src/main/scala/cats/data/StoreT.scala | 120 ---------------- core/src/main/scala/cats/data/package.scala | 7 +- .../cats/laws/discipline/arbitrary.scala | 17 ++- ...e.scala => RepresentableStoreTSuite.scala} | 0 5 files changed, 151 insertions(+), 128 deletions(-) create mode 100644 core/src/main/scala/cats/data/RepresentableStoreT.scala delete mode 100644 core/src/main/scala/cats/data/StoreT.scala rename tests/src/test/scala/cats/tests/{StoreTSuite.scala => RepresentableStoreTSuite.scala} (100%) diff --git a/core/src/main/scala/cats/data/RepresentableStoreT.scala b/core/src/main/scala/cats/data/RepresentableStoreT.scala new file mode 100644 index 0000000000..e3f719220d --- /dev/null +++ b/core/src/main/scala/cats/data/RepresentableStoreT.scala @@ -0,0 +1,135 @@ +package cats.data + +import cats.{Applicative, Comonad, Functor, Monoid, Representable} + +/* + * The dual of `StateT`. Stores some state `A` indexed by + * a type `S` with the notion of a cursor tracking the + * current position in the index. + * + * This state can be extracted if the underlying `F` has + * a Comonad instance. + */ +final case class RepresentableStoreT[W[_], F[_], S, A](runF: W[F[A]], index: S)(implicit F: Representable.Aux[F, S]) { + + def run(implicit W: Functor[W]): W[A] = W.map(runF)(fa => F.index(fa)(index)) + + /** + * Peek at what the focus would be for a given focus s. + */ + def peek(s: S)(implicit W: Comonad[W]): A = W.extract(W.map(runF)(fa => F.index(fa)(s))) + + /** + * Peek at what the focus would be if the current focus where transformed + * with the given function. + */ + def peeks(f: S => S)(implicit W: Comonad[W]): A = peek(f(index)) + + /** + * Set the current focus. + */ + def seek(s: S): RepresentableStoreT[W, F, S, A] = RepresentableStoreT(runF, s) + + /** + * Modify the current focus with the given function. + */ + def seeks(f: S => S): RepresentableStoreT[W, F, S, A] = seek(f(index)) + + /** + * Extract the focus at the current index. + */ + def extract(implicit W: Comonad[W]): A = peek(index) + + /** + * `coflatMap` is the dual of `flatMap` on `FlatMap`. It applies + * a value in a context to a function that takes a value + * in a context and returns a normal value. + */ + def coflatMap[B](f: RepresentableStoreT[W, F, S, A] => B)(implicit W: Comonad[W]): RepresentableStoreT[W, F, S, B] = + RepresentableStoreT( + W.map(W.coflatten(runF))((x: W[F[A]]) => F.tabulate(s => f(RepresentableStoreT(x, s)))), + index + ) + + /** + * `coflatten` is the dual of `flatten` on `FlatMap`. Whereas flatten removes + * a layer of `F`, coflatten adds a layer of `F` + */ + def coflatten(implicit W: Comonad[W]): RepresentableStoreT[W, F, S, RepresentableStoreT[W, F, S, A]] = + RepresentableStoreT( + W.map(W.coflatten(runF))((x: W[F[A]]) => F.tabulate(s => RepresentableStoreT(x, s))), + index + ) + + /** + * Functor `map` for StoreT + */ + def map[B](f: A => B)(implicit W: Functor[W]): RepresentableStoreT[W, F, S, B] = RepresentableStoreT( + W.map(runF)((fa: F[A]) => F.F.map(fa)(f)), + index + ) + + /** + * Given a functorial computation on the index `S` peek at the value in that functor. + */ + def experiment[G[_]](f: S => G[S])(implicit W: Comonad[W], G: Functor[G]): G[A] = + G.map(f(index))(peek(_)) + +} + +object RepresentableStoreT extends RepresentableStoreTInstances1 { + + def pure[W[_], F[_], S, A]( + x: A + )(implicit W: Applicative[W], F: Representable.Aux[F, S], S: Monoid[S]): RepresentableStoreT[W, F, S, A] = + RepresentableStoreT(W.pure(F.tabulate((_: S) => x)), S.empty) + + implicit def comonadForStoreT[W[_]: Comonad, F[_], S]: Comonad[RepresentableStoreT[W, F, S, *]] = + new Comonad[RepresentableStoreT[W, F, S, *]] { + + override def map[A, B](fa: RepresentableStoreT[W, F, S, A])(f: A => B): RepresentableStoreT[W, F, S, B] = + fa.map(f) + + override def coflatMap[A, B](fa: RepresentableStoreT[W, F, S, A])( + f: RepresentableStoreT[W, F, S, A] => B + ): RepresentableStoreT[W, F, S, B] = + fa.coflatMap(f) + + override def extract[A](fa: RepresentableStoreT[W, F, S, A]): A = fa.extract + + } +} + +trait RepresentableStoreTInstances1 extends RepresentableStoreTInstances2 { + + implicit def applicativeForStoreT[W[_], F[_], S](implicit + W: Applicative[W], + F: Representable.Aux[F, S], + S: Monoid[S] + ): Applicative[RepresentableStoreT[W, F, S, *]] = + new Applicative[RepresentableStoreT[W, F, S, *]] { + + def pure[A](x: A): RepresentableStoreT[W, F, S, A] = RepresentableStoreT.pure[W, F, S, A](x) + + def ap[A, B]( + ff: RepresentableStoreT[W, F, S, A => B] + )(fa: RepresentableStoreT[W, F, S, A]): RepresentableStoreT[W, F, S, B] = RepresentableStoreT( + W.map(W.tuple2(ff.runF, fa.runF)) { case (f, a) => + F.tabulate((s: S) => F.index(f)(s).apply(F.index(a)(s))) + }, + S.combine(ff.index, fa.index) + ) + + } +} + +trait RepresentableStoreTInstances2 { + + implicit def functorForStoreT[W[_]: Functor, F[_], S]: Functor[RepresentableStoreT[W, F, S, *]] = + new Functor[RepresentableStoreT[W, F, S, *]] { + + override def map[A, B](fa: RepresentableStoreT[W, F, S, A])(f: A => B): RepresentableStoreT[W, F, S, B] = + fa.map(f) + + } +} diff --git a/core/src/main/scala/cats/data/StoreT.scala b/core/src/main/scala/cats/data/StoreT.scala deleted file mode 100644 index 91f8656aff..0000000000 --- a/core/src/main/scala/cats/data/StoreT.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cats.data - -import cats.{Applicative, Comonad, Functor, Monoid} - -/* - * The dual of `StateT`. Stores some state `A` indexed by - * a type `S` with the notion of a cursor tracking the - * current position in the index. - * - * This state can be extracted if the underlying `F` has - * a Comonad instance. - */ -final case class StoreT[F[_], S, A](runF: F[S => A], index: S) { - - def run(implicit F: Functor[F]) = F.map(runF)(_.apply(index)) - - /** - * Peek at what the focus would be for a given focus s. - */ - def peek(s: S)(implicit F: Comonad[F]): A = F.extract(F.map(runF)(_.apply(index))) - - /** - * Peek at what the focus would be if the current focus where transformed - * with the given function. - */ - def peeks(f: S => S)(implicit F: Comonad[F]): A = peek(f(index)) - - /** - * Set the current focus. - */ - def seek(s: S): StoreT[F, S, A] = StoreT(runF, s) - - /** - * Modify the current focus with the given function. - */ - def seeks(f: S => S): StoreT[F, S, A] = seek(f(index)) - - /** - * Extract the focus at the current index. - */ - def extract(implicit F: Comonad[F]): A = peek(index) - - /** - * `coflatMap` is the dual of `flatMap` on `FlatMap`. It applies - * a value in a context to a function that takes a value - * in a context and returns a normal value. - */ - def coflatMap[B](f: StoreT[F, S, A] => B)(implicit F: Comonad[F]): StoreT[F, S, B] = StoreT( - F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => f(StoreT(x, s))), - index - ) - - /** - * `coflatten` is the dual of `flatten` on `FlatMap`. Whereas flatten removes - * a layer of `F`, coflatten adds a layer of `F` - */ - def coflatten(implicit F: Comonad[F]): StoreT[F, S, StoreT[F, S, A]] = - StoreT( - F.map(F.coflatten(runF))((x: F[S => A]) => (s: S) => StoreT(x, s)), - index - ) - - /** - * Functor `map` for StoreT - */ - def map[B](g: A => B)(implicit F: Functor[F]): StoreT[F, S, B] = StoreT( - F.map(runF)((f: S => A) => AndThen(f).andThen(g(_))), - index - ) - - /** - * Given a functorial computation on the index `S` peek at the value in that functor. - */ - def experiment[G[_]](f: S => G[S])(implicit F: Comonad[F], G: Functor[G]): G[A] = - G.map(f(index))(peek(_)) - -} - -object StoreT extends StoreTInstances1 { - - def pure[F[_], S, A](x: A)(implicit F: Applicative[F], S: Monoid[S]): StoreT[F, S, A] = - StoreT(F.pure((_: S) => x), S.empty) - - implicit def comonadForStoreT[F[_]: Comonad, S]: Comonad[StoreT[F, S, *]] = new Comonad[StoreT[F, S, *]] { - - override def map[A, B](fa: StoreT[F, S, A])(f: A => B): StoreT[F, S, B] = fa.map(f) - - override def coflatMap[A, B](fa: StoreT[F, S, A])(f: StoreT[F, S, A] => B): StoreT[F, S, B] = - fa.coflatMap(f) - - override def extract[A](fa: StoreT[F, S, A]): A = fa.extract - - } -} - -trait StoreTInstances1 extends StoreTInstances2 { - - implicit def applicativeForStoreT[F[_], S](implicit F: Applicative[F], S: Monoid[S]): Applicative[StoreT[F, S, *]] = - new Applicative[StoreT[F, S, *]] { - - def pure[A](x: A): StoreT[F, S, A] = StoreT.pure[F, S, A](x) - - def ap[A, B](ff: StoreT[F, S, A => B])(fa: StoreT[F, S, A]): StoreT[F, S, B] = StoreT( - F.map(F.tuple2(ff.runF, fa.runF)) { case (f, a) => - (s: S) => f(s).apply(a(s)) - }, - S.combine(ff.index, fa.index) - ) - - } -} - -trait StoreTInstances2 { - - implicit def functorForStoreT[F[_]: Functor, S]: Functor[StoreT[F, S, *]] = new Functor[StoreT[F, S, *]] { - - override def map[A, B](fa: StoreT[F, S, A])(f: A => B): StoreT[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 25d4e10d12..e4b263674d 100644 --- a/core/src/main/scala/cats/data/package.scala +++ b/core/src/main/scala/cats/data/package.scala @@ -113,11 +113,12 @@ package object data extends ScalaVersionSpecificPackage { ContT.shiftT(f) } - type ComonadicStore[S, A] = StoreT[Id, S, A] + type StoreT[W[_], S, A] = RepresentableStoreT[W, Function1[S, *], S, A] - object ComonadicStore { + object StoreT { - def pure[S, A](x: A)(implicit S: Monoid[S]): ComonadicStore[S, A] = StoreT.pure[Id, S, A](x) + def pure[W[_], S, A](x: A)(implicit W: Applicative[W], S: Monoid[S]): StoreT[W, S, A] = + RepresentableStoreT.pure[W, Function1[S, *], S, A](x) } } diff --git a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala index ebffe7568c..43b9752137 100644 --- a/laws/src/main/scala/cats/laws/discipline/arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/arbitrary.scala @@ -154,16 +154,23 @@ object arbitrary extends ArbitraryInstances0 with ScalaVersionSpecific.Arbitrary implicit def catsLawsCogenForOptionT[F[_], A](implicit F: Cogen[F[Option[A]]]): Cogen[OptionT[F, A]] = F.contramap(_.value) - implicit def catsLawsArbitraryForStoreT[F[_], S, A](implicit F: Arbitrary[F[S => A]], S: Arbitrary[S]): Arbitrary[StoreT[F, S, A]] = + implicit def catsLawsArbitraryForRepresentableStoreT[W[_], F[_], S, A](implicit + W: Arbitrary[W[F[A]]], + S: Arbitrary[S], + F: Representable.Aux[F, S] + ): Arbitrary[RepresentableStoreT[W, F, S, A]] = Arbitrary( for { - runF <- F.arbitrary + runF <- W.arbitrary index <- S.arbitrary - } yield StoreT(runF, index) + } yield RepresentableStoreT(runF, index) ) - implicit def catsLawsCogenForStoreT[F[_], S, A](implicit F: Cogen[F[S => A]], S: Cogen[S]): Cogen[StoreT[F, S, A]] = - Cogen((seed, st) => S.perturb(F.perturb(seed, st.runF), st.index)) + implicit def catsLawsCogenForRepresentableStoreT[W[_], F[_], S, A](implicit + W: Cogen[W[F[A]]], + S: Cogen[S] + ): Cogen[RepresentableStoreT[W, F, S, A]] = + Cogen((seed, st) => S.perturb(W.perturb(seed, st.runF), st.index)) implicit def catsLawsArbitraryForIdT[F[_], A](implicit F: Arbitrary[F[A]]): Arbitrary[IdT[F, A]] = Arbitrary(F.arbitrary.map(IdT.apply)) diff --git a/tests/src/test/scala/cats/tests/StoreTSuite.scala b/tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala similarity index 100% rename from tests/src/test/scala/cats/tests/StoreTSuite.scala rename to tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala From f09393a625578c89cecd5ed335aebeb418e29b75 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Mon, 29 Mar 2021 12:39:52 +0100 Subject: [PATCH 14/15] Update scaladoc --- core/src/main/scala/cats/data/RepresentableStore.scala | 2 +- core/src/main/scala/cats/data/RepresentableStoreT.scala | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/data/RepresentableStore.scala b/core/src/main/scala/cats/data/RepresentableStore.scala index 595627a6fe..1eee35d96c 100644 --- a/core/src/main/scala/cats/data/RepresentableStore.scala +++ b/core/src/main/scala/cats/data/RepresentableStore.scala @@ -3,7 +3,7 @@ package cats.data import cats.{Comonad, Functor, Representable} /** - * A generalization of `StoreT`, where the underlying functor `F` hasfor any `Representable` functor. + * A generalization of `StoreT`, where the underlying functor `F` has a `Representable` instance. * `Store` is the dual of `State` */ final case class RepresentableStore[F[_], S, A](fa: F[A], index: S)(implicit R: Representable.Aux[F, S]) { diff --git a/core/src/main/scala/cats/data/RepresentableStoreT.scala b/core/src/main/scala/cats/data/RepresentableStoreT.scala index e3f719220d..1e1d9ac51f 100644 --- a/core/src/main/scala/cats/data/RepresentableStoreT.scala +++ b/core/src/main/scala/cats/data/RepresentableStoreT.scala @@ -9,6 +9,8 @@ import cats.{Applicative, Comonad, Functor, Monoid, Representable} * * This state can be extracted if the underlying `F` has * a Comonad instance. + * + * This is the (co)monad-transformer version of `RepresentableStore` */ final case class RepresentableStoreT[W[_], F[_], S, A](runF: W[F[A]], index: S)(implicit F: Representable.Aux[F, S]) { From e6fed6231e14d4c3abe1e9f3d8a3661b95812f49 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Mon, 29 Mar 2021 13:03:52 +0100 Subject: [PATCH 15/15] Manual implicit resolution for the sake of scala 2.12 --- .../cats/tests/RepresentableStoreTSuite.scala | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala b/tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala index e337912f91..1482124ebd 100644 --- a/tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala +++ b/tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala @@ -7,12 +7,47 @@ import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ import cats.syntax.eq._ import org.scalacheck.Prop._ +import cats.data.RepresentableStoreT +import org.scalacheck.{Arbitrary, Cogen} -class StoreTSuite extends CatsSuite { +class RepresentableStoreTSuite extends CatsSuite { implicit val monoid: Monoid[MiniInt] = MiniInt.miniIntAddition - checkAll("StoreT[Id, MiniInt, *]", ComonadTests[StoreT[Id, MiniInt, *]].comonad[MiniInt, MiniInt, MiniInt]) + implicit val scala2_12_makes_me_sad: Comonad[StoreT[Id, MiniInt, *]] = + RepresentableStoreT.comonadForStoreT[Id, Function1[MiniInt, *], MiniInt] + //Like, really, really, really sad + val a: Arbitrary[Int] = implicitly[Arbitrary[Int]] + val b: Eq[Int] = Eq[Int] + val c: Arbitrary[StoreT[Id, MiniInt, Int]] = implicitly[Arbitrary[StoreT[Id, MiniInt, Int]]] + val d: Cogen[Int] = implicitly[Cogen[Int]] + val e: Cogen[StoreT[Id, MiniInt, Int]] = implicitly[Cogen[StoreT[Id, MiniInt, Int]]] + val f: Eq[StoreT[Id, MiniInt, Int]] = Eq[StoreT[Id, MiniInt, Int]] + val g: Eq[StoreT[Id, MiniInt, StoreT[Id, MiniInt, Int]]] = Eq[StoreT[Id, MiniInt, StoreT[Id, MiniInt, Int]]] + val h: Eq[StoreT[Id, MiniInt, StoreT[Id, MiniInt, StoreT[Id, MiniInt, Int]]]] = + Eq[StoreT[Id, MiniInt, StoreT[Id, MiniInt, StoreT[Id, MiniInt, Int]]]] + + checkAll("StoreT[Id, MiniInt, *]", + ComonadTests[StoreT[Id, MiniInt, *]].comonad[Int, Int, Int]( + a, + b, + a, + b, + a, + b, + c, + d, + d, + d, + e, + e, + f, + g, + h, + f, + f + ) + ) checkAll("Comonad[StoreT[Id, MiniInt, *]]", SerializableTests.serializable(Comonad[StoreT[Id, MiniInt, *]]))