diff --git a/core/src/main/scala/cats/data/AndThen.scala b/core/src/main/scala/cats/data/AndThen.scala index 4a383ba38b..4ebf081110 100644 --- a/core/src/main/scala/cats/data/AndThen.scala +++ b/core/src/main/scala/cats/data/AndThen.scala @@ -1,6 +1,9 @@ -package cats.data +package cats +package data import java.io.Serializable +import cats.arrow.{ArrowChoice, CommutativeArrow} + /** * A function type of a single input that can do function composition @@ -16,8 +19,48 @@ import java.io.Serializable * // This should not trigger stack overflow ;-) * f(0) * }}} + * + * This can be used to build stack safe data structures that make + * use of lambdas. The perfect candidates for usage with `AndThen` + * are the data structures using a signature like this (where + * `F[_]` is a monadic type): + * + * {{{ + * A => F[B] + * }}} + * + * As an example, if we described this data structure, the naive + * solution for that `map` is stack unsafe: + * + * {{{ + * case class Resource[F[_], A, B]( + * acquire: F[A], + * use: A => F[B], + * release: A => F[Unit]) { + * + * def flatMap[C](f: B => C)(implicit F: Functor[F]): Resource[F, A, C] = { + * Resource( + * ra.acquire, + * // Stack Unsafe! + * a => ra.use(a).map(f), + * ra.release) + * } + * } + * }}} + * + * To describe a `flatMap` operation for this data type, `AndThen` + * can save the day: + * + * {{{ + * def flatMap[C](f: B => C)(implicit F: Functor[F]): Resource[F, A, C] = { + * Resource( + * ra.acquire, + * AndThen(ra.use).andThen(_.map(f)), + * ra.release) + * } + * }}} */ -private[cats] sealed abstract class AndThen[-T, +R] +sealed abstract class AndThen[-T, +R] extends (T => R) with Product with Serializable { import AndThen._ @@ -97,7 +140,7 @@ private[cats] sealed abstract class AndThen[-T, +R] "AndThen$" + System.identityHashCode(this) } -private[cats] object AndThen { +object AndThen extends AndThenInstances0 { /** Builds an [[AndThen]] reference by wrapping a plain function. */ def apply[A, B](f: A => B): AndThen[A, B] = f match { @@ -124,3 +167,81 @@ private[cats] object AndThen { */ private final val fusionMaxStackDepth = 127 } + +private[data] abstract class AndThenInstances0 extends AndThenInstances1 { + /** + * [[cats.Monad]] instance for [[AndThen]]. + */ + implicit def catsDataMonadForAndThen[T]: Monad[AndThen[T, ?]] = + new Monad[AndThen[T, ?]] { + // Piggybacking on the instance for Function1 + private[this] val fn1 = instances.all.catsStdMonadForFunction1[T] + + def pure[A](x: A): AndThen[T, A] = + AndThen(fn1.pure[A](x)) + + def flatMap[A, B](fa: AndThen[T, A])(f: A => AndThen[T, B]): AndThen[T, B] = + AndThen(fn1.flatMap(fa)(f)) + + override def map[A, B](fa: AndThen[T, A])(f: A => B): AndThen[T, B] = + AndThen(f).compose(fa) + + def tailRecM[A, B](a: A)(f: A => AndThen[T, Either[A, B]]): AndThen[T, B] = + AndThen(fn1.tailRecM(a)(f)) + } + + /** + * [[cats.ContravariantMonoidal]] instance for [[AndThen]]. + */ + implicit def catsDataContravariantMonoidalForAndThen[R : Monoid]: ContravariantMonoidal[AndThen[?, R]] = + new ContravariantMonoidal[AndThen[?, R]] { + // Piggybacking on the instance for Function1 + private[this] val fn1 = instances.all.catsStdContravariantMonoidalForFunction1[R] + + def unit: AndThen[Unit, R] = + AndThen(fn1.unit) + + def contramap[A, B](fa: AndThen[A, R])(f: B => A): AndThen[B, R] = + fa.compose(f) + + def product[A, B](fa: AndThen[A, R], fb: AndThen[B, R]): AndThen[(A, B), R] = + AndThen(fn1.product(fa, fb)) + } + + /** + * [[cats.arrow.ArrowChoice ArrowChoice]] and + * [[cats.arrow.CommutativeArrow CommutativeArrow]] instances + * for [[AndThen]]. + */ + implicit val catsDataArrowForAndThen: ArrowChoice[AndThen] with CommutativeArrow[AndThen] = + new ArrowChoice[AndThen] with CommutativeArrow[AndThen] { + // Piggybacking on the instance for Function1 + private[this] val fn1 = instances.all.catsStdInstancesForFunction1 + + def choose[A, B, C, D](f: AndThen[A, C])(g: AndThen[B, D]): AndThen[Either[A, B], Either[C, D]] = + AndThen(fn1.choose(f)(g)) + + def lift[A, B](f: A => B): AndThen[A, B] = + AndThen(f) + + def first[A, B, C](fa: AndThen[A, B]): AndThen[(A, C), (B, C)] = + AndThen(fn1.first(fa)) + + override def split[A, B, C, D](f: AndThen[A, B], g: AndThen[C, D]): AndThen[(A, C), (B, D)] = + AndThen(fn1.split(f, g)) + + def compose[A, B, C](f: AndThen[B, C], g: AndThen[A, B]): AndThen[A, C] = + f.compose(g) + } +} + +private[data] abstract class AndThenInstances1 { + /** + * [[cats.Contravariant]] instance for [[AndThen]]. + */ + implicit def catsDataContravariantForAndThen[R]: Contravariant[AndThen[?, R]] = + new Contravariant[AndThen[?, R]] { + def contramap[T1, T0](fa: AndThen[T1, R])(f: T0 => T1): AndThen[T0, R] = + fa.compose(f) + } +} diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index c3675b7e4e..4fa3633025 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -250,6 +250,11 @@ object arbitrary extends ArbitraryInstances0 { F: Arbitrary[(E, SA) => F[(L, SB, A)]]): Arbitrary[IndexedReaderWriterStateT[F, E, L, SA, SB, A]] = Arbitrary(F.arbitrary.map(IndexedReaderWriterStateT(_))) + implicit def catsLawsArbitraryForAndThen[A, B](implicit F: Arbitrary[A => B]): Arbitrary[AndThen[A, B]] = + Arbitrary(F.arbitrary.map(AndThen(_))) + + implicit def catsLawsCogenForAndThen[A, B](implicit F: Cogen[A => B]): Cogen[AndThen[A, B]] = + Cogen((seed, x) => F.perturb(seed, x)) } private[discipline] sealed trait ArbitraryInstances0 { diff --git a/laws/src/main/scala/cats/laws/discipline/Eq.scala b/laws/src/main/scala/cats/laws/discipline/Eq.scala index 13f495b29a..3993ccbac7 100644 --- a/laws/src/main/scala/cats/laws/discipline/Eq.scala +++ b/laws/src/main/scala/cats/laws/discipline/Eq.scala @@ -3,6 +3,8 @@ package laws package discipline import catalysts.Platform +import cats.Eq +import cats.data.AndThen import cats.instances.boolean._ import cats.instances.int._ import cats.instances.string._ @@ -28,6 +30,10 @@ object eq { } } + /** `Eq[AndThen]` instance, built by piggybacking on [[catsLawsEqForFn1]]. */ + implicit def catsLawsEqForAndThen[A, B](implicit A: Arbitrary[A], B: Eq[B]): Eq[AndThen[A, B]] = + Eq.instance(catsLawsEqForFn1[A, B].eqv(_, _)) + /** * Create an approximation of Eq[(A, B) => C] by generating 100 values for A and B * and comparing the application of the two functions. diff --git a/tests/src/test/scala/cats/tests/AndThenSuite.scala b/tests/src/test/scala/cats/tests/AndThenSuite.scala index eaabcaab59..a1825f9d09 100644 --- a/tests/src/test/scala/cats/tests/AndThenSuite.scala +++ b/tests/src/test/scala/cats/tests/AndThenSuite.scala @@ -1,9 +1,42 @@ -package cats.tests +package cats +package tests import catalysts.Platform import cats.data._ +import cats.kernel.laws.discipline.SerializableTests +import cats.laws.discipline._ +import cats.arrow._ +import cats.laws.discipline.eq._ +import cats.laws.discipline.arbitrary._ class AndThenSuite extends CatsSuite { + { + implicit val iso = SemigroupalTests.Isomorphisms.invariant[AndThen[Int, ?]] + checkAll("AndThen[Int, Int]", SemigroupalTests[AndThen[Int, ?]].semigroupal[Int, Int, Int]) + checkAll("Semigroupal[AndThen[Int, ?]]", SerializableTests.serializable(Semigroupal[AndThen[Int, ?]])) + } + + { + implicit val iso = SemigroupalTests.Isomorphisms.invariant[AndThen[?, Int]] + checkAll("AndThen[Int, Int]", ContravariantMonoidalTests[AndThen[?, Int]].contravariantMonoidal[Int, Int, Int]) + checkAll("ContravariantMonoidal[AndThen[?, Int]]", SerializableTests.serializable(ContravariantMonoidal[AndThen[?, Int]])) + } + + checkAll("AndThen[Int, Int]", MonadTests[AndThen[Int, ?]].monad[Int, Int, Int]) + checkAll("Monad[AndThen[Int, ?]]", SerializableTests.serializable(Monad[AndThen[Int, ?]])) + + checkAll("AndThen[Int, Int]", CommutativeArrowTests[AndThen].commutativeArrow[Int, Int, Int, Int, Int, Int]) + checkAll("Arrow[AndThen]", SerializableTests.serializable(CommutativeArrow[AndThen])) + + checkAll("AndThen[Int, Int]", ChoiceTests[AndThen].choice[Int, Int, Int, Int]) + checkAll("Choice[AndThen]", SerializableTests.serializable(Choice[AndThen])) + + checkAll("AndThen[Int, Int]", ArrowChoiceTests[AndThen].arrowChoice[Int, Int, Int, Int, Int, Int]) + checkAll("ArrowChoice[AndThen]", SerializableTests.serializable(ArrowChoice[AndThen])) + + checkAll("AndThen[Int, Int]", ContravariantTests[AndThen[?, Int]].contravariant[Int, Int, Int]) + checkAll("Contravariant[AndThen[?, Int]]", SerializableTests.serializable(Contravariant[AndThen[?, Int]])) + test("compose a chain of functions with andThen") { check { (i: Int, fs: List[Int => Int]) => val result = fs.map(AndThen(_)).reduceOption(_.andThen(_)).map(_(i))