diff --git a/core/src/main/scala/cats/Cartesian.scala b/core/src/main/scala/cats/Cartesian.scala index a3543d009a..20c3721974 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 KernelCartesianInstances + +/** + * Cartesian instances for types that are housed in Kernel and therefore + * can't have instances for Cats type classes in their companion objects. + */ +private[cats] sealed trait KernelCartesianInstances { + implicit val catsInvariantSemigroup: Cartesian[Semigroup] = InvariantMonoidal.catsInvariantMonoidalSemigroup + implicit val catsInvariantMonoid: Cartesian[Monoid] = InvariantMonoidal.catsInvariantMonoidalMonoid +} diff --git a/core/src/main/scala/cats/FlatMap.scala b/core/src/main/scala/cats/FlatMap.scala index b914a82ff6..5a01bfb26f 100644 --- a/core/src/main/scala/cats/FlatMap.scala +++ b/core/src/main/scala/cats/FlatMap.scala @@ -3,17 +3,17 @@ package cats import simulacrum.typeclass /** - * FlatMap type class gives us flatMap, which allows us to have a value - * in a context (F[A]) and then feed that into a function that takes - * a normal value and returns a value in a context (A => F[B]). + * FlatMap type class gives us flatMap, which allows us to have a value + * in a context (F[A]) and then feed that into a function that takes + * a normal value and returns a value in a context (A => F[B]). * - * One motivation for separating this out from Monad is that there are - * situations where we can implement flatMap but not pure. For example, - * we can implement map or flatMap that transforms the values of Map[K, ?], - * but we can't implement pure (because we wouldn't know what key to use - * when instantiating the new Map). + * One motivation for separating this out from Monad is that there are + * situations where we can implement flatMap but not pure. For example, + * we can implement map or flatMap that transforms the values of Map[K, ?], + * but we can't implement pure (because we wouldn't know what key to use + * when instantiating the new Map). * - * @see See [[https://github.com/typelevel/cats/issues/3]] for some discussion. + * @see See [[https://github.com/typelevel/cats/issues/3]] for some discussion. * * Must obey the laws defined in cats.laws.FlatMapLaws. */ @@ -33,7 +33,7 @@ import simulacrum.typeclass flatMap(fa)(a => map(fb)(b => (a, b))) /** - * Pair `A` with the result of function application. + * Pair `A` with the result of function application. */ def mproduct[A, B](fa: F[A])(f: A => F[B]): F[(A, B)] = flatMap(fa)(a => map(f(a))((a, _))) @@ -41,7 +41,6 @@ import simulacrum.typeclass /** * `if` lifted into monad. */ - def ifM[B](fa: F[Boolean])(ifTrue: => F[B], ifFalse: => F[B]): F[B] = { + def ifM[B](fa: F[Boolean])(ifTrue: => F[B], ifFalse: => F[B]): F[B] = flatMap(fa)(if (_) ifTrue else ifFalse) - } } diff --git a/core/src/main/scala/cats/InvariantMonoidal.scala b/core/src/main/scala/cats/InvariantMonoidal.scala new file mode 100644 index 0000000000..436dcf3b88 --- /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 KernelInvariantMonoidalInstances + +/** + * InvariantMonoidal instances for types that are housed in cats.kernel and therefore + * can't have instances for this type class in their companion objects. + */ +private[cats] trait KernelInvariantMonoidalInstances { + implicit val catsInvariantMonoidalSemigroup: 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 catsInvariantMonoidalMonoid: 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/TransLift.scala b/core/src/main/scala/cats/TransLift.scala index e8addcb745..00d734317c 100644 --- a/core/src/main/scala/cats/TransLift.scala +++ b/core/src/main/scala/cats/TransLift.scala @@ -1,6 +1,5 @@ package cats - /** * A type class which abstracts over the ability to lift an M[A] into a * MonadTransformer diff --git a/core/src/main/scala/cats/Traverse.scala b/core/src/main/scala/cats/Traverse.scala index 7ed4324727..493009299d 100644 --- a/core/src/main/scala/cats/Traverse.scala +++ b/core/src/main/scala/cats/Traverse.scala @@ -46,7 +46,7 @@ import simulacrum.typeclass * Behaves just like sequence, but uses [[Unapply]] to find the * Applicative instance for G. */ - def sequenceU[GA](fga: F[GA])(implicit U: Unapply[Applicative,GA]): U.M[F[U.A]] = + def sequenceU[GA](fga: F[GA])(implicit U: Unapply[Applicative, GA]): U.M[F[U.A]] = traverse(fga)(U.subst)(U.TC) def compose[G[_]: Traverse]: Traverse[λ[α => F[G[α]]]] = diff --git a/core/src/main/scala/cats/data/Const.scala b/core/src/main/scala/cats/data/Const.scala index 083bd236bf..db7ee78932 100644 --- a/core/src/main/scala/cats/data/Const.scala +++ b/core/src/main/scala/cats/data/Const.scala @@ -107,6 +107,17 @@ private[data] sealed abstract class ConstInstances0 extends ConstInstances1 { } private[data] sealed abstract class ConstInstances1 { + implicit def catsConstInvariantMonoidal[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 catsDataEqForConst[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/data/Xor.scala b/core/src/main/scala/cats/data/Xor.scala index 8e69ce0104..c2943a873e 100644 --- a/core/src/main/scala/cats/data/Xor.scala +++ b/core/src/main/scala/cats/data/Xor.scala @@ -71,13 +71,13 @@ sealed abstract class Xor[+A, +B] extends Product with Serializable { def toTry(implicit ev: A <:< Throwable): Try[B] = fold(a => Failure(ev(a)), Success(_)) - def toValidated: Validated[A,B] = fold(Validated.Invalid.apply, Validated.Valid.apply) + def toValidated: Validated[A, B] = fold(Validated.Invalid.apply, Validated.Valid.apply) /** Returns a [[ValidatedNel]] representation of this disjunction with the `Left` value * as a single element on the `Invalid` side of the [[NonEmptyList]]. */ - def toValidatedNel[AA >: A]: ValidatedNel[AA,B] = fold(Validated.invalidNel, Validated.valid) + def toValidatedNel[AA >: A]: ValidatedNel[AA, B] = fold(Validated.invalidNel, Validated.valid) - def withValidated[AA,BB](f: Validated[A,B] => Validated[AA,BB]): AA Xor BB = + def withValidated[AA, BB](f: Validated[A, B] => Validated[AA, BB]): AA Xor BB = f(toValidated).toXor def to[F[_], BB >: B](implicit F: Alternative[F]): F[BB] = diff --git a/core/src/main/scala/cats/functor/Invariant.scala b/core/src/main/scala/cats/functor/Invariant.scala index 0ea0c719e0..20b361b2f1 100644 --- a/core/src/main/scala/cats/functor/Invariant.scala +++ b/core/src/main/scala/cats/functor/Invariant.scala @@ -28,26 +28,13 @@ import simulacrum.typeclass } } -object Invariant extends AlgebraInvariantInstances +object Invariant extends KernelInvariantInstances /** - * Invariant instances for types that are housed in Algebra and therefore - * can't have instances for Cats type classes in their companion objects. + * Invariant instances for types that are housed in cats.kernel and therefore + * can't have instances for this type class in their companion objects. */ -private[functor] sealed trait AlgebraInvariantInstances { - - implicit val catsFunctorInvariantForSemigroup: 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 catsFunctorInvariantForMonoid: 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))) - } - } +private[functor] sealed trait KernelInvariantInstances { + implicit val catsFunctorInvariantForSemigroup: Invariant[Semigroup] = InvariantMonoidal.catsInvariantMonoidalSemigroup + implicit val catsFunctorInvariantForMonoid: Invariant[Monoid] = InvariantMonoidal.catsInvariantMonoidalMonoid } diff --git a/core/src/main/scala/cats/syntax/bifunctor.scala b/core/src/main/scala/cats/syntax/bifunctor.scala index 404e53e001..d7e68efb9a 100644 --- a/core/src/main/scala/cats/syntax/bifunctor.scala +++ b/core/src/main/scala/cats/syntax/bifunctor.scala @@ -10,7 +10,7 @@ trait BifunctorSyntax { } final class BifunctorOps[F[_, _], A, B](fab: F[A, B])(implicit F: Bifunctor[F]) { - def bimap[C, D](f: A => C, g: B => D): F[C,D] = F.bimap(fab)(f,g) + def bimap[C, D](f: A => C, g: B => D): F[C, D] = F.bimap(fab)(f,g) def leftMap[C](f: A => C): F[C, B] = F.leftMap(fab)(f) diff --git a/docs/src/main/tut/applicative.md b/docs/src/main/tut/applicative.md index 0f8823585f..3850203682 100644 --- a/docs/src/main/tut/applicative.md +++ b/docs/src/main/tut/applicative.md @@ -11,7 +11,7 @@ scaladoc: "#cats.Applicative" `pure`: ```scala - def pure[A](x: A): F[A] +def pure[A](x: A): F[A] ```` This method takes any value and returns the value in the context of diff --git a/docs/src/main/tut/apply.md b/docs/src/main/tut/apply.md index 7d6a5b91d9..028b3d4413 100644 --- a/docs/src/main/tut/apply.md +++ b/docs/src/main/tut/apply.md @@ -11,7 +11,7 @@ scaladoc: "#cats.Apply" function) with a new function `ap`. The `ap` function is similar to `map` in that we are transforming a value in a context (a context being the `F` in `F[A]`; a context can be `Option`, `List` or `Future` for example). -However, the difference between `ap` and `map` is that for `ap` the function that +However, the difference between `ap` and `map` is that for `ap` the function that takes care of the transformation is of type `F[A => B]`, whereas for `map` it is `A => B`: ```tut:silent @@ -71,7 +71,7 @@ Apply[Option].ap(None)(None) ### ap2, ap3, etc -`Apply` also offers variants of `ap`. The functions `apN` (for `N` between `2` and `22`) +`Apply` also offers variants of `ap`. The functions `apN` (for `N` between `2` and `22`) accept `N` arguments where `ap` accepts `1`: For example: diff --git a/docs/src/main/tut/invariant.md b/docs/src/main/tut/invariant.md index f0ce5c9529..55cd3bfcda 100644 --- a/docs/src/main/tut/invariant.md +++ b/docs/src/main/tut/invariant.md @@ -17,7 +17,7 @@ def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] Every covariant (as well as [contravariant](contravariant.html)) functor gives rise to an invariant functor, by ignoring the `g` (or in case of contravariance, `f`) function. -Examples for instances of `Invariant` are `Semigroup` and `Monoid`, in +Examples for instances of `Invariant` are [`Semigroup`](semigroup.md) and [`Monoid`](monoid.md), in the following we will explain why this is the case using `Semigroup`, the reasoning for `Monoid` is analogous. diff --git a/docs/src/main/tut/invariantmonoidal.md b/docs/src/main/tut/invariantmonoidal.md new file mode 100644 index 0000000000..b2a54936c6 --- /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 `Cartesian` 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/free/src/main/scala/cats/free/FreeInvariantMonoidal.scala b/free/src/main/scala/cats/free/FreeInvariantMonoidal.scala new file mode 100644 index 0000000000..5f320bce90 --- /dev/null +++ b/free/src/main/scala/cats/free/FreeInvariantMonoidal.scala @@ -0,0 +1,79 @@ +package cats +package free + +import cats.arrow.FunctionK +import cats.data.Const + +/** + * Invariant Monoidal for Free + */ +sealed abstract class FreeInvariantMonoidal[F[_], A] extends Product with Serializable { self => + import FreeInvariantMonoidal.{FA, Zip, Imap, 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: FunctionK[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(FunctionK.id[F]) + + /** Interpret this algebra into another InvariantMonoidal */ + final def compile[G[_]](f: FunctionK[F, G]): FA[G, A] = + foldMap[FA[G, ?]] { + new FunctionK[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: FunctionK[F, λ[α => M]]): M = + foldMap[Const[M, ?]](new FunctionK[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: FunctionK[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: FunctionK[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: FunctionK[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: FunctionK[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 catsFreeInvariantMonoidal[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/free/src/test/scala/cats/free/FreeApplicativeTests.scala b/free/src/test/scala/cats/free/FreeApplicativeTests.scala index 5a593c57f4..2c9039ab0c 100644 --- a/free/src/test/scala/cats/free/FreeApplicativeTests.scala +++ b/free/src/test/scala/cats/free/FreeApplicativeTests.scala @@ -26,7 +26,7 @@ class FreeApplicativeTests extends CatsSuite { implicit val iso = CartesianTests.Isomorphisms.invariant[FreeApplicative[Option, ?]] checkAll("FreeApplicative[Option, ?]", ApplicativeTests[FreeApplicative[Option, ?]].applicative[Int, Int, Int]) - checkAll("Monad[FreeApplicative[Option, ?]]", SerializableTests.serializable(Applicative[FreeApplicative[Option, ?]])) + checkAll("Applicative[FreeApplicative[Option, ?]]", SerializableTests.serializable(Applicative[FreeApplicative[Option, ?]])) test("toString is stack-safe") { val r = FreeApplicative.pure[List, Int](333) @@ -89,7 +89,7 @@ class FreeApplicativeTests extends CatsSuite { fli1.analyze[G[Int]](countingNT) should === (List(4)) val fli2 = FreeApplicative.lift[List, Int](List.empty) - fli2.analyze[G[Int]](countingNT) should ===(List(0)) + fli2.analyze[G[Int]](countingNT) should === (List(0)) } test("foldMap order of effects - regression check for #799") { diff --git a/free/src/test/scala/cats/free/FreeInvariantMonoidalTests.scala b/free/src/test/scala/cats/free/FreeInvariantMonoidalTests.scala new file mode 100644 index 0000000000..4b56509197 --- /dev/null +++ b/free/src/test/scala/cats/free/FreeInvariantMonoidalTests.scala @@ -0,0 +1,72 @@ +package cats +package tests + +import cats.arrow.FunctionK +import cats.free.FreeInvariantMonoidal +import cats.laws.discipline.{InvariantMonoidalTests, SerializableTests} +import cats.laws.discipline.CartesianTests.Isomorphisms +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 = FunctionK.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 = FunctionK.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 FunctionK[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)) + } +} diff --git a/laws/src/main/scala/cats/laws/CartesianLaws.scala b/laws/src/main/scala/cats/laws/CartesianLaws.scala index de07d3860c..39a322d2e2 100644 --- a/laws/src/main/scala/cats/laws/CartesianLaws.scala +++ b/laws/src/main/scala/cats/laws/CartesianLaws.scala @@ -1,18 +1,17 @@ package cats package laws +/** + * Laws that must be obeyed by any `cats.Cartesian`. + */ trait CartesianLaws[F[_]] { - implicit def F: Cartesian[F] def cartesianAssociativity[A, B, C](fa: F[A], fb: F[B], fc: F[C]): (F[(A, (B, C))], F[((A, B), C)]) = (F.product(fa, F.product(fb, fc)), F.product(F.product(fa, fb), fc)) - } object CartesianLaws { - def apply[F[_]](implicit ev: Cartesian[F]): CartesianLaws[F] = new CartesianLaws[F] { val F = ev } - } 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..ba701767e2 --- /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/AlternativeTests.scala b/laws/src/main/scala/cats/laws/discipline/AlternativeTests.scala index 001b3637a1..530f24cc62 100644 --- a/laws/src/main/scala/cats/laws/discipline/AlternativeTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/AlternativeTests.scala @@ -25,7 +25,7 @@ trait AlternativeTests[F[_]] extends ApplicativeTests[F] with MonoidKTests[F] { new RuleSet { val name: String = "alternative" val bases: Seq[(String, RuleSet)] = Nil - val parents: Seq[RuleSet] = Seq(monoidK[A], applicative[A,B,C]) + val parents: Seq[RuleSet] = Seq(monoidK[A], applicative[A, B, C]) val props: Seq[(String, Prop)] = Seq( "left distributivity" -> forAll(laws.alternativeLeftDistributivity[A, B] _), "right distributivity" -> forAll(laws.alternativeRightDistributivity[A, B] _), diff --git a/laws/src/main/scala/cats/laws/discipline/CartesianTests.scala b/laws/src/main/scala/cats/laws/discipline/CartesianTests.scala index 989baeb7eb..9a328bfc0c 100644 --- a/laws/src/main/scala/cats/laws/discipline/CartesianTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/CartesianTests.scala @@ -2,6 +2,7 @@ package cats package laws package discipline +import cats.laws.discipline.CartesianTests.Isomorphisms import org.scalacheck.Arbitrary import org.scalacheck.Prop import Prop._ @@ -11,7 +12,7 @@ trait CartesianTests[F[_]] extends Laws { def laws: CartesianLaws[F] def cartesian[A : Arbitrary, B : Arbitrary, C : Arbitrary](implicit - iso: CartesianTests.Isomorphisms[F], + iso: Isomorphisms[F], ArbFA: Arbitrary[F[A]], ArbFB: Arbitrary[F[B]], ArbFC: Arbitrary[F[C]], 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..1bdaf07803 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/InvariantMonoidalTests.scala @@ -0,0 +1,37 @@ +package cats +package laws +package discipline + +import cats.laws.discipline.CartesianTests.Isomorphisms +import org.scalacheck.Arbitrary +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/laws/src/main/scala/cats/laws/discipline/InvariantTests.scala b/laws/src/main/scala/cats/laws/discipline/InvariantTests.scala index 6ac3632ca9..36a7b8b29a 100644 --- a/laws/src/main/scala/cats/laws/discipline/InvariantTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/InvariantTests.scala @@ -16,7 +16,6 @@ trait InvariantTests[F[_]] extends Laws { EqFA: Eq[F[A]], EqFC: Eq[F[C]] ): RuleSet = { - new DefaultRuleSet( name = "invariant", parent = None, diff --git a/laws/src/main/scala/cats/laws/discipline/MonadCombineTests.scala b/laws/src/main/scala/cats/laws/discipline/MonadCombineTests.scala index e7ba25b1fb..be6ec6ed11 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonadCombineTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonadCombineTests.scala @@ -25,7 +25,7 @@ trait MonadCombineTests[F[_]] extends MonadFilterTests[F] with AlternativeTests[ new RuleSet { def name: String = "monadCombine" def bases: Seq[(String, RuleSet)] = Nil - def parents: Seq[RuleSet] = Seq(monadFilter[A, B, C], alternative[A,B,C]) + def parents: Seq[RuleSet] = Seq(monadFilter[A, B, C], alternative[A, B, C]) def props: Seq[(String, Prop)] = Seq( "monadCombine left distributivity" -> forAll(laws.monadCombineLeftDistributivity[A, B] _) ) diff --git a/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala b/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala index 83f888ce83..3ea259d0d0 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonoidKTests.scala @@ -11,17 +11,12 @@ trait MonoidKTests[F[_]] extends SemigroupKTests[F] { def monoidK[A: Arbitrary](implicit ArbFA: Arbitrary[F[A]], EqFA: Eq[F[A]] - ): RuleSet = { - new RuleSet { - val name = "monoidK" - val bases = Nil - val parents = Seq(semigroupK[A]) - val props = Seq( - "monoidK left identity" -> forAll(laws.monoidKLeftIdentity[A] _), - "monoidK right identity" -> forAll(laws.monoidKRightIdentity[A] _) - ) - } - } + ): RuleSet = + new DefaultRuleSet( + "monoidK", + Some(semigroupK[A]), + "monoidK left identity" -> forAll(laws.monoidKLeftIdentity[A] _), + "monoidK right identity" -> forAll(laws.monoidKRightIdentity[A] _)) } object MonoidKTests { diff --git a/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala b/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala index 937a050627..ca4525eea8 100644 --- a/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/SemigroupKTests.scala @@ -12,16 +12,11 @@ trait SemigroupKTests[F[_]] extends Laws { def semigroupK[A: Arbitrary](implicit ArbFA: Arbitrary[F[A]], EqFA: Eq[F[A]] - ): RuleSet = { - new RuleSet { - val name = "semigroupK" - val bases = Nil - val parents = Nil - val props = Seq( - "semigroupK associative" -> forAll(laws.semigroupKAssociative[A] _) - ) - } - } + ): RuleSet = + new DefaultRuleSet( + "semigroupK", + None, + "semigroupK associative" -> forAll(laws.semigroupKAssociative[A] _)) } object SemigroupKTests { diff --git a/tests/src/test/scala/cats/tests/AlgebraInvariantTests.scala b/tests/src/test/scala/cats/tests/AlgebraInvariantTests.scala index cb5d003c70..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} @@ -11,13 +11,11 @@ class AlgebraInvariantTests extends CatsSuite { val intMultiplication: Monoid[Int] = new Monoid[Int] { val empty = 1 - def combine(x: Int, y: Int): Int = x * y } val maxInt: Monoid[Int] = new Monoid[Int] { val empty = Int.MinValue - def combine(x: Int, y: Int): Int = if (x > y) x else y } @@ -35,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..cdda2cce36 --- /dev/null +++ b/tests/src/test/scala/cats/tests/CsvCodecInvariantMonoidalTests.scala @@ -0,0 +1,92 @@ +package cats +package tests + +import cats.laws.discipline.eq.catsLawsEqForFn1 +import cats.laws.discipline.{InvariantMonoidalTests, SerializableTests} +import cats.std.all._ +import cats.syntax.cartesian._ +import cats.Eq +import org.scalacheck.{Arbitrary, Gen} + +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]] = + catsLawsEqForFn1[A, CSV].on[CsvCodec[A]](_.write) and catsLawsEqForFn1[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/TransLiftTests.scala b/tests/src/test/scala/cats/tests/TransLiftTests.scala index c1eab6aae5..fbc354b7d7 100644 --- a/tests/src/test/scala/cats/tests/TransLiftTests.scala +++ b/tests/src/test/scala/cats/tests/TransLiftTests.scala @@ -10,7 +10,7 @@ class TransLiftTests extends CatsSuite { case class JustFunctor[A](a: A) implicit val jfFunctor: Functor[JustFunctor] = new Functor[JustFunctor] { - override def map[A,B](fa: JustFunctor[A])(f: A => B): JustFunctor[B] = JustFunctor(f(fa.a)) + override def map[A, B](fa: JustFunctor[A])(f: A => B): JustFunctor[B] = JustFunctor(f(fa.a)) } case class JustAp[A](a: A)