Skip to content

Commit

Permalink
Merge pull request #3836 from TimWSpence/store-commonad
Browse files Browse the repository at this point in the history
  • Loading branch information
larsrh authored Apr 10, 2021
2 parents c0391c6 + e6fed62 commit 08d7acc
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 4 deletions.
34 changes: 31 additions & 3 deletions core/src/main/scala/cats/data/RepresentableStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,55 @@ package cats.data
import cats.{Comonad, Functor, Representable}

/**
* A generalisation of the Store comonad, for 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]) {

/**
* 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)

Expand Down
137 changes: 137 additions & 0 deletions core/src/main/scala/cats/data/RepresentableStoreT.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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.
*
* 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]) {

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)

}
}
9 changes: 9 additions & 0 deletions core/src/main/scala/cats/data/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,13 @@ package object data extends ScalaVersionSpecificPackage {
def shift[A, B](f: (B => Eval[A]) => Cont[A, A]): Cont[A, B] =
ContT.shiftT(f)
}

type StoreT[W[_], S, A] = RepresentableStoreT[W, Function1[S, *], S, A]

object StoreT {

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)

}
}
5 changes: 4 additions & 1 deletion laws/src/main/scala/cats/laws/discipline/Eq.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/arbitrary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ 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 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 <- W.arbitrary
index <- S.arbitrary
} yield RepresentableStoreT(runF, 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))

Expand Down
68 changes: 68 additions & 0 deletions tests/src/test/scala/cats/tests/RepresentableStoreTSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cats.tests

import cats._
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._
import cats.data.RepresentableStoreT
import org.scalacheck.{Arbitrary, Cogen}

class RepresentableStoreTSuite extends CatsSuite {

implicit val monoid: Monoid[MiniInt] = MiniInt.miniIntAddition

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, *]]))

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, *]])
)

test("extract and peek are consistent") {
forAll { (store: StoreT[Id, String, String]) =>
assert(store.extract === (store.peek(store.index)))
}
}

}

0 comments on commit 08d7acc

Please sign in to comment.