diff --git a/core/src/main/scala/cats/Cartesian.scala b/core/src/main/scala/cats/Cartesian.scala index a3543d009a..6d33adf143 100644 --- a/core/src/main/scala/cats/Cartesian.scala +++ b/core/src/main/scala/cats/Cartesian.scala @@ -16,4 +16,13 @@ import simulacrum.typeclass def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] } -object Cartesian extends CartesianArityFunctions +object Cartesian extends CartesianArityFunctions with AlgebrCartesianInstances + +/** + * Cartesian instances for types that are housed in Algebra and therefore + * can't have instances for Cats type classes in their companion objects. + */ +private[cats] sealed trait AlgebrCartesianInstances { + implicit val invariantSemigroup: Cartesian[Semigroup] = InvariantMonoidal.invariantMonoidalSemigroup + implicit val invariantMonoid: Cartesian[Monoid] = InvariantMonoidal.invariantMonoidalMonoid +} diff --git a/core/src/main/scala/cats/InvariantMonoidal.scala b/core/src/main/scala/cats/InvariantMonoidal.scala new file mode 100644 index 0000000000..d42710bb48 --- /dev/null +++ b/core/src/main/scala/cats/InvariantMonoidal.scala @@ -0,0 +1,52 @@ +package cats + +import cats.functor.Invariant +import simulacrum.typeclass + +/** + * Invariant version of a Monoidal. + * + * Must obey the laws defined in cats.laws.InvariantMonoidalLaws. + */ +@typeclass trait InvariantMonoidal[F[_]] extends Invariant[F] with Cartesian[F] { + def pure[A](a: A): F[A] +} + +object InvariantMonoidal extends AlgebraInvariantMonoidalInstances + +/** + * InvariantMonoidal instances for types that are housed in Algebra and therefore + * can't have instances for Cats type classes in their companion objects. + */ +private[cats] trait AlgebraInvariantMonoidalInstances { + implicit val invariantMonoidalSemigroup: InvariantMonoidal[Semigroup] = new InvariantMonoidal[Semigroup] { + def product[A, B](fa: Semigroup[A], fb: Semigroup[B]): Semigroup[(A, B)] = new Semigroup[(A, B)] { + def combine(x: (A, B), y: (A, B)): (A, B) = fa.combine(x._1, y._1) -> fb.combine(x._2, y._2) + } + + def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = new Semigroup[B] { + def combine(x: B, y: B): B = f(fa.combine(g(x), g(y))) + } + + def pure[A](a: A): Semigroup[A] = new Semigroup[A] { + def combine(x: A, y: A): A = a + } + } + + implicit val invariantMonoidalMonoid: InvariantMonoidal[Monoid] = new InvariantMonoidal[Monoid] { + def product[A, B](fa: Monoid[A], fb: Monoid[B]): Monoid[(A, B)] = new Monoid[(A, B)] { + val empty = fa.empty -> fb.empty + def combine(x: (A, B), y: (A, B)): (A, B) = fa.combine(x._1, y._1) -> fb.combine(x._2, y._2) + } + + def imap[A, B](fa: Monoid[A])(f: A => B)(g: B => A): Monoid[B] = new Monoid[B] { + val empty = f(fa.empty) + def combine(x: B, y: B): B = f(fa.combine(g(x), g(y))) + } + + def pure[A](a: A): Monoid[A] = new Monoid[A] { + val empty = a + def combine(x: A, y: A): A = a + } + } +} diff --git a/core/src/main/scala/cats/data/Const.scala b/core/src/main/scala/cats/data/Const.scala index c7cbd9b78a..0af3ab85d7 100644 --- a/core/src/main/scala/cats/data/Const.scala +++ b/core/src/main/scala/cats/data/Const.scala @@ -97,6 +97,17 @@ private[data] sealed abstract class ConstInstances0 extends ConstInstances1 { } private[data] sealed abstract class ConstInstances1 { + implicit def contInvariantMonoidal[C : Monoid]: InvariantMonoidal[Const[C, ?]] = new InvariantMonoidal[Const[C, ?]] { + def pure[A](a: A): Const[C, A] = + Const.empty + + def imap[A, B](fa: Const[C, A])(f: A => B)(g: B => A): Const[C, B] = + fa.retag[B] + + def product[A, B](fa: Const[C, A],fb: Const[C, B]): Const[C, (A, B)] = + fa.retag[(A, B)] combine fb.retag[(A, B)] + } + implicit def constEq[A: Eq, B]: Eq[Const[A, B]] = new Eq[Const[A, B]] { def eqv(x: Const[A, B], y: Const[A, B]): Boolean = x === y diff --git a/core/src/main/scala/cats/free/FreeInvariantMonoidal.scala b/core/src/main/scala/cats/free/FreeInvariantMonoidal.scala new file mode 100644 index 0000000000..d8dfd05733 --- /dev/null +++ b/core/src/main/scala/cats/free/FreeInvariantMonoidal.scala @@ -0,0 +1,79 @@ +package cats +package free + +import cats.arrow.NaturalTransformation +import cats.data.Const + +/** + * Invariant Monoidal for Free + */ +sealed abstract class FreeInvariantMonoidal[F[_], A] extends Product with Serializable { self => + import FreeInvariantMonoidal.{FA, Zip, Imap, Pure, lift} + + def imap[B](f: A => B)(g: B => A): FA[F, B] = + Imap(this, f, g) + + def product[B](fb: FA[F, B]): FA[F, (A, B)] = + Zip(this, fb) + + /** Interprets/Runs the sequence of operations using the semantics of `InvariantMonoidal[G]` */ + def foldMap[G[_]](nt: NaturalTransformation[F, G])(implicit im: InvariantMonoidal[G]): G[A] + // Note that implementing a concrete `foldMap` here does not work because + // `Zip extends G[(A, B)]` confuses the type inferance when pattern matching on `this`. + + /** Interpret/run the operations using the semantics of `InvariantMonoidal[F]`. */ + final def fold(implicit F: InvariantMonoidal[F]): F[A] = + foldMap(NaturalTransformation.id[F]) + + /** Interpret this algebra into another InvariantMonoidal */ + final def compile[G[_]](f: F ~> G): FA[G, A] = + foldMap[FA[G, ?]] { + new NaturalTransformation[F, FA[G, ?]] { + def apply[B](fa: F[B]): FA[G, B] = lift(f(fa)) + } + } + + /** Interpret this algebra into a Monoid */ + final def analyze[M : Monoid](f: F ~> λ[α => M]): M = + foldMap[Const[M, ?]](new (F ~> Const[M, ?]) { + def apply[X](x: F[X]): Const[M, X] = Const(f(x)) + }).getConst +} + +object FreeInvariantMonoidal { + type FA[F[_], A] = FreeInvariantMonoidal[F, A] + + private final case class Pure[F[_], A](a: A) extends FA[F, A] { + def foldMap[G[_]](nt: NaturalTransformation[F, G])(implicit im: InvariantMonoidal[G]): G[A] = + im.pure(a) + } + + private final case class Suspend[F[_], A](fa: F[A]) extends FA[F, A] { + def foldMap[G[_]](nt: NaturalTransformation[F, G])(implicit im: InvariantMonoidal[G]): G[A] = + nt(fa) + } + + private final case class Zip[F[_], A, B](fa: FA[F, A], fb: FA[F, B]) extends FA[F, (A, B)] { + def foldMap[G[_]](nt: NaturalTransformation[F, G])(implicit im: InvariantMonoidal[G]): G[(A, B)] = + im.product(fa.foldMap(nt), fb.foldMap(nt)) + } + + private final case class Imap[F[_], A, B](fa: FA[F, A], f: A => B, g: B => A) extends FA[F, B] { + def foldMap[G[_]](nt: NaturalTransformation[F, G])(implicit im: InvariantMonoidal[G]): G[B] = + im.imap(fa.foldMap(nt))(f)(g) + } + + def pure[F[_], A](a: A): FA[F, A] = + Pure(a) + + def lift[F[_], A](fa: F[A]): FA[F, A] = + Suspend(fa) + + /** `FreeInvariantMonoidal[S, ?]` has a FreeInvariantMonoidal for any type constructor `S[_]`. */ + implicit def freeInvariant[S[_]]: InvariantMonoidal[FA[S, ?]] = + new InvariantMonoidal[FA[S, ?]] { + def pure[A](a: A): FA[S, A] = FreeInvariantMonoidal.pure(a) + def imap[A, B](fa: FA[S, A])(f: A => B)(g: B => A): FA[S, B] = fa.imap(f)(g) + def product[A, B](fa: FA[S, A], fb: FA[S, B]): FA[S, (A, B)] = fa.product(fb) + } +} diff --git a/core/src/main/scala/cats/functor/Invariant.scala b/core/src/main/scala/cats/functor/Invariant.scala index ac2653f7b5..18abb275a2 100644 --- a/core/src/main/scala/cats/functor/Invariant.scala +++ b/core/src/main/scala/cats/functor/Invariant.scala @@ -65,19 +65,6 @@ object Invariant extends AlgebraInvariantInstances { * can't have instances for Cats type classes in their companion objects. */ private[functor] sealed trait AlgebraInvariantInstances { - - implicit val invariantSemigroup: Invariant[Semigroup] = new Invariant[Semigroup] { - def imap[A, B](fa: Semigroup[A])(f: A => B)(g: B => A): Semigroup[B] = new Semigroup[B] { - - def combine(x: B, y: B): B = f(fa.combine(g(x), g(y))) - } - } - - implicit val invariantMonoid: Invariant[Monoid] = new Invariant[Monoid] { - def imap[A, B](fa: Monoid[A])(f: A => B)(g: B => A): Monoid[B] = new Monoid[B] { - val empty = f(fa.empty) - - def combine(x: B, y: B): B = f(fa.combine(g(x), g(y))) - } - } + implicit val invariantSemigroup: Invariant[Semigroup] = InvariantMonoidal.invariantMonoidalSemigroup + implicit val invariantMonoid: Invariant[Monoid] = InvariantMonoidal.invariantMonoidalMonoid } diff --git a/docs/src/main/tut/invariantmonoidal.md b/docs/src/main/tut/invariantmonoidal.md new file mode 100644 index 0000000000..08c4898c38 --- /dev/null +++ b/docs/src/main/tut/invariantmonoidal.md @@ -0,0 +1,235 @@ + --- +layout: default +title: "InvariantMonoidal" +section: "typeclasses" +source: "https://github.com/non/cats/blob/master/core/src/main/scala/cats/InvariantMonoidal.scala" +scaladoc: "#cats.InvariantMonoidal" +--- +# Invariant Monoidal + +`InvariantMonoidal` combines [`Invariant`](invariant.html) and [`Monoidal`](monoidal.html) with the addition of a `pure` methods, defined in isolation the `InvariantMonoidal` type class could be defined as follows: + +```tut:silent +trait InvariantMonoidal[F[_]] { + def pure[A](x: A): F[A] + def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] + def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] +} +``` + +Practical uses of `InvariantMonoidal` appear in the context of codecs, that is interfaces to capture both serialization and deserialization for a given format. Other notable examples are [`Semigroup`](semigroup.md) and [`Monoid`](monoid.md). + +This tutorial first shows how `Semigroup` is `InvariantMonoidal`, and how this can be used create `Semigroup` instances by combining other `Semigroup` instances. Secondly, we present a complete example of `Codec` for the CSV format, and show how it is `InvariantMonoidal`. Lastly, we present an alternative definition of `InvariantMonoidal` as a generalization of `Invariant`, and show that both definitions are equivalent. + +# `Semigroup` is `InvariantMonoidal` + +As explained in the [`Invariant` tutorial](invariant.html), `Semigroup` forms an invariant functor. Indeed, given a `Semigroup[A]` and two functions `A => B` and `B => A`, one can construct a `Semigroup[B]` by transforming two values from type `B` to type `A`, combining these using the `Semigroup[A]`, and transforming the result back to type `B`. Thus to define an `InvariantMonoidal[Semigroup]` we need implementations for `pure` and `product`. + +To construct a `Semigroup` from a single value, we can define a trivial `Semigroup` with a combine that always outputs the given value. A `Semigroup[(A, B)]` can be obtained from two `Semigroup`s for type `A` and `B` by deconstructing two pairs into elements of type `A` and `B`, combining these element using their respective `Semigroup`s, and reconstructing a pair from the results: + +```tut:silent +import cats.Semigroup + +def pure[A](a: A): Semigroup[A] = + new Semigroup[A] { + def combine(x: A, y: A): A = a + } + +def product[A, B](fa: Semigroup[A], fb: Semigroup[B]): Semigroup[(A, B)] = + new Semigroup[(A, B)] { + def combine(x: (A, B), y: (A, B)): (A, B) = (x, y) match { + case ((xa, xb), (ya, yb)) => fa.combine(xa, ya) -> fb.combine(xb, yb) + } + } +``` + +Given an instance of `InvariantMonoidal` for `Semigroup`, we are able to combine existing `Semigroup` instances to form a new `Semigroup` by using the `Catesian` syntax: + +```tut:silent +import cats.std.all._ +import cats.syntax.all._ + +// Let's build a Semigroup for this case class +case class Foo(a: String, c: List[Double]) + +implicit val fooSemigroup: Semigroup[Foo] = ( + (implicitly[Semigroup[String]] |@| implicitly[Semigroup[List[Double]]]) + .imap(Foo.apply)(Function.unlift(Foo.unapply)) +) +``` + +Our new Semigroup in action: + +```tut +Foo("Hello", List(0.0)) |+| Foo("World", Nil) |+| Foo("!", List(1.1, 2.2)) +``` + +# `CsvCodec` is `InvariantMonoidal` + +We define `CsvCodec`, a type class for serialization and deserialization of CSV rows: + + +```tut:silent +type CSV = List[String] + +trait CsvCodec[A] { + def read(s: CSV): (Option[A], CSV) + def write(a: A): CSV +} +``` + +The `read` method consumes columns from a CSV row and returns an optional value and the remaining CSV. The `write` method produces the CSV representation of a given value. + +Beside the composition capabilities illustrated later in this tutorial, grouping both serialization and deserialization in a single type class has the advantage to allows the definition of a law to capture the fact that both operations play nicely together: + +```scala +forAll { (c: CsvCodec[A], a: A) => c.read(c.write(a)) == ((Some(a), List())) +``` + +Let's now see how we could define an `InvariantMonoidal` instance for `CsvCodec`. Lifting a single value into a `CsvCodec` can be done "the trivial way" by consuming nothing from CSV and producing that value, and writing this value as the empty CSV: + +```tut:silent +trait CCPure { + def pure[A](a: A): CsvCodec[A] = new CsvCodec[A] { + def read(s: CSV): (Option[A], CSV) = (Some(a), s) + def write(a: A): CSV = List.empty + } +} +``` + +Combining two `CsvCodec`s could be done by reading and writing each value of a pair sequentially, where reading succeeds if both read operations succeed: + +```tut:silent +trait CCProduct { + def product[A, B](fa: CsvCodec[A], fb: CsvCodec[B]): CsvCodec[(A, B)] = + new CsvCodec[(A, B)] { + def read(s: CSV): (Option[(A, B)], CSV) = { + val (a1, s1) = fa.read(s) + val (a2, s2) = fb.read(s1) + ((a1 |@| a2).map(_ -> _), s2) + } + + def write(a: (A, B)): CSV = + fa.write(a._1) ++ fb.write(a._2) + } +} +``` + +Changing a `CsvCodec[A]` to `CsvCodec[B]` requires two functions of type `A => B` and `B => A` to transform a value from `A` to `B` after deserialized, and from `B` to `A` before serialization: + +```tut:silent +trait CCImap { + def imap[A, B](fa: CsvCodec[A])(f: A => B)(g: B => A): CsvCodec[B] = + new CsvCodec[B] { + def read(s: CSV): (Option[B], CSV) = { + val (a1, s1) = fa.read(s) + (a1.map(f), s1) + } + + def write(a: B): CSV = + fa.write(g(a)) + } +} +``` + +Putting it all together: + +```tut:silent +import cats.InvariantMonoidal + +implicit val csvCodecIsInvariantMonoidal: InvariantMonoidal[CsvCodec] = + new InvariantMonoidal[CsvCodec] with CCPure with CCProduct with CCImap +``` + +We can now define a few `CsvCodec` instances and use the methods provided by `InvariantMonoidal` to define `CsvCodec` from existing `CsvCodec`s: + +```tut:silent +val stringCodec: CsvCodec[String] = + new CsvCodec[String] { + def read(s: CSV): (Option[String], CSV) = (s.headOption, s.drop(1)) + def write(a: String): CSV = List(a) + } + +def numericSystemCodec(base: Int): CsvCodec[Int] = + new CsvCodec[Int] { + def read(s: CSV): (Option[Int], CSV) = + (s.headOption.flatMap(head => scala.util.Try(Integer.parseInt(head, base)).toOption), s.drop(1)) + + def write(a: Int): CSV = + List(Integer.toString(a, base)) + } +``` + +```tut:silent +case class BinDec(binary: Int, decimal: Int) + +val binDecCodec: CsvCodec[BinDec] = ( + (numericSystemCodec(2) |@| numericSystemCodec(10)) + .imap(BinDec.apply)(Function.unlift(BinDec.unapply)) +) + +case class Foo(name: String, bd1: BinDec, bd2: BinDec) + +val fooCodec: CsvCodec[Foo] = ( + (stringCodec |@| binDecCodec |@| binDecCodec) + .imap(Foo.apply)(Function.unlift(Foo.unapply)) +) +``` + +Finally let's verify out CsvCodec law with an example: + +```tut +val foo = Foo("foo", BinDec(10, 10), BinDec(20, 20)) + +val fooCsv = fooCodec.write(foo) + +fooCodec.read(fooCsv) + +fooCodec.read(fooCodec.write(foo)) == ((Some(foo), List())) +``` + +# `InvariantMonoidal` as a generalization of `Invariant` + +To better understand the motivations behind the `InvariantMonoidal` type class, we show how one could naturally arrive to it's definition by generalizing the concept of `Invariant` functor. This reflection is analogous to the one presented in [Free Applicative Functors by Paolo Capriotti](http://www.paolocapriotti.com/assets/applicative.pdf) to show how [`Applicative`](applicative.md) are a generalization of [`Functor`](functor.md). + +Given an `Invariant[F]` instance for a certain *context* `F[_]`, its `imap` method gives a way to lift two *unary* pure functions `A => B` and `B => A` into *contextualized* functions `F[A] => F[B]`. But what about functions of other arity? + +For instance, a value `a` of type `A` can be seen as a pair of nullary functions, one than given no input returns `a`, and the other than give `a` return no output, which we might want to lift them into a *contextualized* `F[A]`. Similarly, given two functions of type `(A, B) => C` and `C => (A, B)`, we might want to *contextualize* them as functions of type `(F[A], F[B]) => F[C]`. + +The `Invariant` instance alone does not provide either of these lifting, and it is therefore natural to define define a type class for generalizing `Invariant`s for functions of arbitrary arity: + +```tut:silent +trait MultiInvariant[F[_]] { + def imap0[A](a: A): F[A] + def imap1[A, B](f: A => B)(g: B => A)(fa: F[A]): F[B] + def imap2[A, B, C](f: ((A, B)) => C)(g: C => (A, B))(fa: F[A], fb: F[B]): F[C] +} +``` + +Higher-arity `imapN` can be defined in terms of `imap2`, for example for `N = 3`: + +```tut:silent +trait MultiInvariantImap3[F[_]] extends MultiInvariant[F] { + def imap3[A, B, C, D]( + f: ((A, B, C)) => D, + g: D => (A, B, C), + fa: F[A], + fb: F[B], + fc: F[C] + ): F[D] = ( + imap2[A, (B, C), D] + (f compose { case (a, (b, c)) => (a, b, c) }) + (g andThen { case (a, b, c) => (a, (b, c)) }) + (fa, imap2[B, C, (B, C)](identity)(identity)(fb, fc)) + ) +} +``` + +We can observe that `MultiInvariant` is none other than an alternative formulation for `InvariantMonoidal`. Indeed, `imap0` and `pure` have exactly the same signature, `imap1` and `imap` only differ by the order of their argument, and `imap2` can easily be defined in terms of `imap` and `product`: + +```tut:silent +trait Imap2FromImapProduct[F[_]] extends cats.InvariantMonoidal[F] { + def imap2[A, B, C](f: ((A, B)) => C)(g: C => (A, B))(fa: F[A], fb: F[B]): F[C] = + imap(product(fa, fb))(f)(g) +} +``` diff --git a/laws/src/main/scala/cats/laws/InvariantMonoidalLaws.scala b/laws/src/main/scala/cats/laws/InvariantMonoidalLaws.scala new file mode 100644 index 0000000000..b335b016fd --- /dev/null +++ b/laws/src/main/scala/cats/laws/InvariantMonoidalLaws.scala @@ -0,0 +1,27 @@ +package cats +package laws + +/** + * Laws that must be obeyed by any `cats.InvariantMonoidal`. + */ +trait InvariantMonoidalLaws[F[_]] extends InvariantLaws[F] with CartesianLaws[F]{ + override implicit def F: InvariantMonoidal[F] + import cats.syntax.cartesian._ + import cats.syntax.invariant._ + + def invariantMonoidalLeftIdentity[A, B](fa: F[A], b: B): IsEq[F[A]] = + F.pure(b).product(fa).imap(_._2)(a => (b, a)) <-> fa + + def invariantMonoidalRightIdentity[A, B](fa: F[A], b: B): IsEq[F[A]] = + fa.product(F.pure(b)).imap(_._1)(a => (a, b)) <-> fa + + def invariantMonoidalAssociativity[A, B, C](fa: F[A], fb: F[B], fc: F[C]): + IsEq[F[(A, (B, C))]] = + fa.product(fb.product(fc)) <-> fa.product(fb).product(fc) + .imap { case ((a, b), c) => (a, (b, c)) } { case (a, (b, c)) => ((a, b), c) } +} + +object InvariantMonoidalLaws { + def apply[F[_]](implicit i: InvariantMonoidal[F]): InvariantMonoidalLaws[F] = + new InvariantMonoidalLaws[F] { def F: InvariantMonoidal[F] = i } +} diff --git a/laws/src/main/scala/cats/laws/discipline/Eq.scala b/laws/src/main/scala/cats/laws/discipline/Eq.scala index d79a1a6a84..56164d4121 100644 --- a/laws/src/main/scala/cats/laws/discipline/Eq.scala +++ b/laws/src/main/scala/cats/laws/discipline/Eq.scala @@ -13,7 +13,7 @@ object eq { */ implicit def function1Eq[A, B](implicit A: Arbitrary[A], B: Eq[B]): Eq[A => B] = new Eq[A => B] { def eqv(f: A => B, g: A => B): Boolean = { - val samples = List.fill(100)(A.arbitrary.sample).collect{ + val samples = List.fill(10)(A.arbitrary.sample).collect{ case Some(a) => a case None => sys.error("Could not generate arbitrary values to compare two functions") } @@ -51,4 +51,12 @@ object eq { implicit val unitEq: Eq[Unit] = new Eq[Unit] { def eqv(a: Unit, b: Unit): Boolean = true } + + // To be removed once https://github.com/non/algebra/pull/125 is published + implicit class EqAnd[A](self: Eq[A]) { + def and(that: Eq[A]): Eq[A] = + new Eq[A] { + def eqv(x: A, y: A) = self.eqv(x, y) && that.eqv(x, y) + } + } } diff --git a/laws/src/main/scala/cats/laws/discipline/InvariantMonoidalTests.scala b/laws/src/main/scala/cats/laws/discipline/InvariantMonoidalTests.scala new file mode 100644 index 0000000000..e346bf8afb --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/InvariantMonoidalTests.scala @@ -0,0 +1,38 @@ +package cats +package laws +package discipline + +import cats.laws.discipline.CartesianTests.Isomorphisms +import org.scalacheck.Arbitrary +import org.scalacheck.Prop +import org.scalacheck.Prop._ + +trait InvariantMonoidalTests[F[_]] extends InvariantTests[F] with CartesianTests[F] { + def laws: InvariantMonoidalLaws[F] + + def invariantMonoidal[A : Arbitrary, B : Arbitrary, C : Arbitrary](implicit + ArbFA: Arbitrary[F[A]], + ArbFB: Arbitrary[F[B]], + ArbFC: Arbitrary[F[C]], + EqFABC: Eq[F[(A, (B, C))]], + EqFABC2: Eq[F[(A, B, C)]], + iso: Isomorphisms[F], + EqFA: Eq[F[A]], + EqFC: Eq[F[C]] + ): RuleSet = + new RuleSet { + val name = "invariantMonoidal" + val parents = Seq(invariant[A, B, C], cartesian[A, B, C]) + val bases = Seq.empty + val props = Seq( + "invariant cartesian left identity" -> forAll((fa: F[A], b: B) => laws.invariantMonoidalLeftIdentity(fa, b)), + "invariant cartesian right identity" -> forAll((fa: F[A], b: B) => laws.invariantMonoidalRightIdentity(fa, b)), + "invariant cartesian associativity" -> forAll((fa: F[A], fb: F[B], fc: F[C]) => laws.invariantMonoidalAssociativity(fa, fb, fc)) + ) + } +} + +object InvariantMonoidalTests { + def apply[F[_] : InvariantMonoidal]: InvariantMonoidalTests[F] = + new InvariantMonoidalTests[F] { def laws: InvariantMonoidalLaws[F] = InvariantMonoidalLaws[F] } +} diff --git a/tests/src/test/scala/cats/tests/AlgebraInvariantTests.scala b/tests/src/test/scala/cats/tests/AlgebraInvariantTests.scala index e97625bf49..4cc92474b1 100644 --- a/tests/src/test/scala/cats/tests/AlgebraInvariantTests.scala +++ b/tests/src/test/scala/cats/tests/AlgebraInvariantTests.scala @@ -2,7 +2,7 @@ package cats package tests import cats.functor.Invariant -import cats.laws.discipline.{InvariantTests, SerializableTests} +import cats.laws.discipline.{InvariantTests, InvariantMonoidalTests, SerializableTests} import cats.laws.discipline.eq._ import org.scalacheck.{Arbitrary, Gen} @@ -33,4 +33,10 @@ class AlgebraInvariantTests extends CatsSuite { checkAll("Invariant[Monoid]", InvariantTests[Monoid].invariant[Int, Int, Int]) checkAll("Invariant[Monoid]", SerializableTests.serializable(Invariant[Monoid])) + + checkAll("InvariantMonoidal[Semigroup]", InvariantMonoidalTests[Semigroup].invariantMonoidal[Int, Int, Int]) + checkAll("InvariantMonoidal[Semigroup]", SerializableTests.serializable(InvariantMonoidal[Semigroup])) + + checkAll("InvariantMonoidal[Monoid]", InvariantMonoidalTests[Monoid].invariantMonoidal[Int, Int, Int]) + checkAll("InvariantMonoidal[Monoid]", SerializableTests.serializable(InvariantMonoidal[Monoid])) } diff --git a/tests/src/test/scala/cats/tests/CsvCodecInvariantMonoidalTests.scala b/tests/src/test/scala/cats/tests/CsvCodecInvariantMonoidalTests.scala new file mode 100644 index 0000000000..f31f4ba3ac --- /dev/null +++ b/tests/src/test/scala/cats/tests/CsvCodecInvariantMonoidalTests.scala @@ -0,0 +1,92 @@ +package cats +package tests + +import cats.laws.discipline.eq._ +import cats.laws.discipline.{InvariantMonoidalTests, SerializableTests} +import cats.std.all._ +import cats.syntax.cartesian._ +import cats.{Semigroup, Eq} +import org.scalacheck.{Arbitrary, Gen, Prop} + +object CsvCodecInvariantMonoidalTests { + type CSV = List[String] + + /** + * Type class to read and write objects of type A to CSV. + * + * Obeys `forAll { (c: CsvCodec[A], a: A) => c.read(c.writes(a)) == (Some(a), List())`, + * under the assumtion that `imap(f, g)` is always called with `f` and `g` such that + * `forAll { (a: A) => g(f(a)) == a }`. + */ + trait CsvCodec[A] extends Serializable { self => + /** Reads the first value of a CSV, returning an optional value of type `A` and the remaining CSV. */ + def read(s: CSV): (Option[A], CSV) + + /** Writes a value of type `A` to CSV format. */ + def write(a: A): CSV + } + + object CsvCodec { + // In tut/invariantmonoidal.md pure, product and imap are defined in + // their own trait to be introduced one by one, + trait CCPure { + def pure[A](a: A): CsvCodec[A] = new CsvCodec[A] { + def read(s: CSV): (Option[A], CSV) = (Some(a), s) + def write(a: A): CSV = List.empty + } + } + + trait CCProduct { + def product[A, B](fa: CsvCodec[A], fb: CsvCodec[B]): CsvCodec[(A, B)] = + new CsvCodec[(A, B)] { + def read(s: CSV): (Option[(A, B)], CSV) = { + val (a1, s1) = fa.read(s) + val (a2, s2) = fb.read(s1) + ((a1 |@| a2).map(_ -> _), s2) + } + + def write(a: (A, B)): CSV = + fa.write(a._1) ++ fb.write(a._2) + } + } + + trait CCImap { + def imap[A, B](fa: CsvCodec[A])(f: A => B)(g: B => A): CsvCodec[B] = + new CsvCodec[B] { + def read(s: CSV): (Option[B], CSV) = { + val (a1, s1) = fa.read(s) + (a1.map(f), s1) + } + + def write(a: B): CSV = + fa.write(g(a)) + } + } + + implicit val csvCodecIsInvariantMonoidal: InvariantMonoidal[CsvCodec] = + new InvariantMonoidal[CsvCodec] with CCPure with CCProduct with CCImap + } + + def numericSystemCodec(base: Int): CsvCodec[Int] = + new CsvCodec[Int] { + def read(s: CSV): (Option[Int], CSV) = + (s.headOption.flatMap(head => scala.util.Try(Integer.parseInt(head, base)).toOption), s.drop(1)) + + def write(a: Int): CSV = + List(Integer.toString(a, base)) + } + + implicit val arbNumericSystemCodec: Arbitrary[CsvCodec[Int]] = + Arbitrary(Gen.choose(2, 16).map(numericSystemCodec)) + + implicit def csvCodecsEq[A](implicit a: Arbitrary[A], e: Eq[A]): Eq[CsvCodec[A]] = + function1Eq[A, CSV].on[CsvCodec[A]](_.write) and function1Eq[CSV, (Option[A], CSV)].on[CsvCodec[A]](_.read) +} + +class CsvCodecInvariantMonoidalTests extends CatsSuite { + // Eveything is defined in a companion object to be serializable. + import CsvCodecInvariantMonoidalTests._ + + checkAll("InvariantMonoidal[CsvCodec]", InvariantMonoidalTests[CsvCodec].invariantMonoidal[Int, Int, Int]) + checkAll("InvariantMonoidal[CsvCodec]", SerializableTests.serializable(InvariantMonoidal[CsvCodec])) +} diff --git a/tests/src/test/scala/cats/tests/FreeInvariantMonoidalTests.scala b/tests/src/test/scala/cats/tests/FreeInvariantMonoidalTests.scala new file mode 100644 index 0000000000..d3ca0e7218 --- /dev/null +++ b/tests/src/test/scala/cats/tests/FreeInvariantMonoidalTests.scala @@ -0,0 +1,73 @@ +package cats +package tests + +import cats.arrow.NaturalTransformation +import cats.free.FreeInvariantMonoidal +import cats.laws.discipline.{InvariantMonoidalTests, SerializableTests} +import cats.laws.discipline.CartesianTests.Isomorphisms +import cats.laws.discipline.eq.{tuple3Eq, tuple2Eq} +import org.scalacheck.{Arbitrary, Gen} +import cats.tests.CsvCodecInvariantMonoidalTests._ + +class FreeInvariantMonoidalTests extends CatsSuite { + implicit def freeInvariantMonoidalArbitrary[F[_], A](implicit F: Arbitrary[F[A]], A: Arbitrary[A]): Arbitrary[FreeInvariantMonoidal[F, A]] = + Arbitrary( + Gen.oneOf( + A.arbitrary.map(FreeInvariantMonoidal.pure[F, A]), + F.arbitrary.map(FreeInvariantMonoidal.lift[F, A]))) + + implicit def freeInvariantMonoidalEq[S[_]: InvariantMonoidal, A](implicit SA: Eq[S[A]]): Eq[FreeInvariantMonoidal[S, A]] = + new Eq[FreeInvariantMonoidal[S, A]] { + def eqv(a: FreeInvariantMonoidal[S, A], b: FreeInvariantMonoidal[S, A]): Boolean = { + val nt = NaturalTransformation.id[S] + SA.eqv(a.foldMap(nt), b.foldMap(nt)) + } + } + + implicit val isoFreeCsvCodec = Isomorphisms.invariant[FreeInvariantMonoidal[CsvCodec, ?]] + + checkAll("FreeInvariantMonoidal[CsvCodec, ?]", InvariantMonoidalTests[FreeInvariantMonoidal[CsvCodec, ?]].invariantMonoidal[Int, Int, Int]) + checkAll("InvariantMonoidal[FreeInvariantMonoidal[CsvCodec, ?]]", SerializableTests.serializable(InvariantMonoidal[FreeInvariantMonoidal[CsvCodec, ?]])) + + test("FreeInvariantMonoidal#fold") { + val n = 2 + val i1 = numericSystemCodec(8) + val i2 = InvariantMonoidal[CsvCodec].pure(n) + val iExpr = i1.product(i2.imap(_ * 2)(_ / 2)) + + val f1 = FreeInvariantMonoidal.lift[CsvCodec, Int](i1) + val f2 = FreeInvariantMonoidal.pure[CsvCodec, Int](n) + val fExpr = f1.product(f2.imap(_ * 2)(_ / 2)) + + fExpr.fold should === (iExpr) + } + + implicit val idIsInvariantMonoidal: InvariantMonoidal[Id] = new InvariantMonoidal[Id] { + def product[A, B](fa: Id[A], fb: Id[B]): Id[(A, B)] = fa -> fb + def imap[A, B](fa: Id[A])(f: A => B)(g: B => A): Id[B] = f(fa) + def pure[A](a: A): Id[A] = a + } + + test("FreeInvariantMonoidal#compile") { + val x = FreeInvariantMonoidal.lift[Id, Int](1) + val y = FreeInvariantMonoidal.pure[Id, Int](2) + val p = x.imap(_ * 2)(_ / 2) + val nt = NaturalTransformation.id[Id] + val r1 = y.product(p) + val r2 = r1.compile(nt) + r1.foldMap(nt) should === (r2.foldMap(nt)) + } + + test("FreeInvariantMonoidal#analyze") { + type G[A] = List[Int] + val countingNT = new NaturalTransformation[List, G] { + def apply[A](la: List[A]): G[A] = List(la.length) + } + + val fli1 = FreeInvariantMonoidal.lift[List, Int](List(1, 3, 5, 7)) + fli1.analyze[G[Int]](countingNT) should === (List(4)) + + val fli2 = FreeInvariantMonoidal.lift[List, Int](List.empty) + fli2.analyze[G[Int]](countingNT) should === (List(0)) + } +}