diff --git a/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala b/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala index 093f7c060..77d4f32a0 100644 --- a/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala +++ b/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala @@ -2,7 +2,7 @@ package zio.prelude import zio._ import zio.prelude.coherent.AssociativeBothDeriveEqualInvariant -import zio.prelude.newtypes.{ AndF, Failure, OrF } +import zio.prelude.newtypes.{ AndF, Failure, Nested, OrF } import zio.stm.ZSTM import zio.stream.{ ZSink, ZStream } import zio.test.TestResult @@ -1124,6 +1124,18 @@ object AssociativeBoth extends LawfulF.Invariant[AssociativeBothDeriveEqualInvar Id(Id.unwrap(fa) -> Id.unwrap(fb)) } + implicit def NestedIdentityBoth[F[+_]: IdentityBoth: Covariant, G[+_]](implicit + G: IdentityBoth[G] + ): IdentityBoth[({ type lambda[+A] = Nested[F, G, A] })#lambda] = + new IdentityBoth[({ type lambda[+A] = Nested[F, G, A] })#lambda] { + override def any: Nested[F, G, Any] = Nested(G.any.succeed[F]) + + override def both[A, B](fa: => Nested[F, G, A], fb: => Nested[F, G, B]): Nested[F, G, (A, B)] = + Nested { + Nested.unwrap[F[G[A]]](fa).zipWith(Nested.unwrap[F[G[B]]](fb))(_ zip _) + } + } + /** * The `IdentityBoth` (and `AssociativeBoth`) instance for `List`. */ diff --git a/core/shared/src/main/scala/zio/prelude/Covariant.scala b/core/shared/src/main/scala/zio/prelude/Covariant.scala index 064b6a8e8..dbde2b48f 100644 --- a/core/shared/src/main/scala/zio/prelude/Covariant.scala +++ b/core/shared/src/main/scala/zio/prelude/Covariant.scala @@ -1,6 +1,7 @@ package zio.prelude import zio.prelude.coherent.CovariantDeriveEqual +import zio.prelude.newtypes.{ Nested } import zio.test.TestResult import zio.test.laws._ @@ -95,6 +96,18 @@ object Covariant extends LawfulF.Covariant[CovariantDeriveEqual, Equal] { def apply[F[+_]](implicit covariant: Covariant[F]): Covariant[F] = covariant + implicit def NestedCovariant[F[+_], G[+_]](implicit + F: Covariant[F], + G: Covariant[G] + ): Covariant[({ type lambda[+A] = Nested[F, G, A] })#lambda] = + new Covariant[({ type lambda[+A] = Nested[F, G, A] })#lambda] { + private lazy val composedCovariant = F.compose(G) + + override def map[A, B](f: A => B): Nested[F, G, A] => Nested[F, G, B] = { x: Nested[F, G, A] => + Nested(composedCovariant.map(f)(Nested.unwrap[F[G[A]]](x))) + } + } + } trait CovariantSyntax { diff --git a/core/shared/src/main/scala/zio/prelude/Derive.scala b/core/shared/src/main/scala/zio/prelude/Derive.scala index 33292901f..f94effd84 100644 --- a/core/shared/src/main/scala/zio/prelude/Derive.scala +++ b/core/shared/src/main/scala/zio/prelude/Derive.scala @@ -1,5 +1,6 @@ package zio.prelude +import zio.prelude.newtypes.Nested import zio.{ Cause, Chunk, Exit, NonEmptyChunk } import scala.util.Try @@ -31,6 +32,26 @@ object Derive { def apply[F[_], Typeclass[_]](implicit derive: Derive[F, Typeclass]): Derive[F, Typeclass] = derive + /** + * The `DeriveEqual` instance for `Id`. + */ + implicit val IdDeriveEqual: Derive[Id, Equal] = + new Derive[Id, Equal] { + override def derive[A: Equal]: Equal[Id[A]] = Id.wrapAll(Equal[A]) + } + + /** + * The `DeriveEqual` instance for `Nested`. + */ + implicit def NestedDeriveEqual[F[+_], G[+_]](implicit + F: Derive[F, Equal], + G: Derive[G, Equal] + ): Derive[({ type lambda[A] = Nested[F, G, A] })#lambda, Equal] = + new Derive[({ type lambda[A] = Nested[F, G, A] })#lambda, Equal] { + override def derive[A: Equal]: Equal[Nested[F, G, A]] = + Equal.NestedEqual(F.derive(G.derive[A])) + } + /** * The `DeriveEqual` instance for `Chunk`. */ diff --git a/core/shared/src/main/scala/zio/prelude/Equal.scala b/core/shared/src/main/scala/zio/prelude/Equal.scala index cbafa3187..a9d7c00c5 100644 --- a/core/shared/src/main/scala/zio/prelude/Equal.scala +++ b/core/shared/src/main/scala/zio/prelude/Equal.scala @@ -2,6 +2,7 @@ package zio.prelude import zio.Exit.{ Failure, Success } import zio.prelude.coherent.HashOrd +import zio.prelude.newtypes.Nested import zio.test.TestResult import zio.test.laws.{ Lawful, Laws } import zio.{ Cause, Chunk, Exit, Fiber, NonEmptyChunk, ZTrace } @@ -204,6 +205,17 @@ object Equal extends Lawful[Equal] { def default[A]: Equal[A] = make(_ == _) + implicit def IdEqual[A: Equal]: Equal[Id[A]] = new Equal[Id[A]] { + override protected def checkEqual(l: Id[A], r: Id[A]): Boolean = + Id.unwrap[A](l) === Id.unwrap[A](r) + } + + implicit def NestedEqual[F[+_], G[+_], A](implicit eqFGA: Equal[F[G[A]]]): Equal[Nested[F, G, A]] = + new Equal[Nested[F, G, A]] { + override protected def checkEqual(l: Nested[F, G, A], r: Nested[F, G, A]): Boolean = + eqFGA.checkEqual(Nested.unwrap[F[G[A]]](l), Nested.unwrap[F[G[A]]](r)) + } + /** * `Hash` and `Ord` (and thus also `Equal`) instance for `Boolean` values. */ diff --git a/core/shared/src/main/scala/zio/prelude/GenFs.scala b/core/shared/src/main/scala/zio/prelude/GenFs.scala index db0d34677..cd0a48229 100644 --- a/core/shared/src/main/scala/zio/prelude/GenFs.scala +++ b/core/shared/src/main/scala/zio/prelude/GenFs.scala @@ -1,6 +1,6 @@ package zio.prelude -import zio.prelude.newtypes.Failure +import zio.prelude.newtypes.{ Failure, Nested } import zio.random.Random import zio.test.Gen.oneOf import zio.test._ @@ -109,4 +109,17 @@ object GenFs { def apply[R1 <: R, E](e: Gen[R1, E]): Gen[R1, Failure[Validation[E, A]]] = Gens.validation(e, a).map(Failure.wrap) } + + def nested[F[+_], G[+_], RF, RG]( + genF: GenF[RF, F], + genG: GenF[RG, G] + ): GenF[RF with RG, ({ type lambda[+A] = Nested[F, G, A] })#lambda] = + new GenF[RF with RG, ({ type lambda[+A] = Nested[F, G, A] })#lambda] { + override def apply[R1 <: RF with RG, A](gen: Gen[R1, A]): Gen[R1, Nested[F, G, A]] = { + val value: Gen[R1 with RG with RF, newtypes.Nested.newtypeF.Type[F[G[A]]]] = + genF(genG(gen)).map(Nested(_): Nested[F, G, A]) + value + } + + } } diff --git a/core/shared/src/main/scala/zio/prelude/Invariant.scala b/core/shared/src/main/scala/zio/prelude/Invariant.scala index 3334709d0..9004a64f0 100644 --- a/core/shared/src/main/scala/zio/prelude/Invariant.scala +++ b/core/shared/src/main/scala/zio/prelude/Invariant.scala @@ -677,6 +677,8 @@ object Invariant extends LowPriorityInvariantImplicits with InvariantVersionSpec new Traversable[Option] { def foreach[G[+_]: IdentityBoth: Covariant, A, B](option: Option[A])(f: A => G[B]): G[Option[B]] = option.fold[G[Option[B]]](Option.empty.succeed)(a => f(a).map(Some(_))) + + override def map[A, B](f: A => B): Option[A] => Option[B] = _.map(f) } /** diff --git a/core/shared/src/main/scala/zio/prelude/Traversable.scala b/core/shared/src/main/scala/zio/prelude/Traversable.scala index 0474ab985..4792e4094 100644 --- a/core/shared/src/main/scala/zio/prelude/Traversable.scala +++ b/core/shared/src/main/scala/zio/prelude/Traversable.scala @@ -1,7 +1,8 @@ package zio.prelude import zio.prelude.coherent.DeriveEqualTraversable -import zio.prelude.newtypes.{ And, First, Max, Min, Or, Prod, Sum } +import zio.prelude.newtypes._ +import zio.test.TestResult import zio.test.laws._ import zio.{ Chunk, ChunkBuilder, NonEmptyChunk } @@ -269,17 +270,68 @@ trait Traversable[F[+_]] extends Covariant[F] { object Traversable extends LawfulF.Covariant[DeriveEqualTraversable, Equal] { + /** + * Identity Law : + * traverse Identity ta = Identity ta + */ + val traversableIdentityLaw: LawsF.Covariant[DeriveEqualTraversable, Equal] = + new LawsF.Covariant.Law1[DeriveEqualTraversable, Equal]("traversableIdentityLaw") { + def apply[F[+_]: DeriveEqualTraversable, A: Equal](fa: F[A]): TestResult = + fa.foreach(Id(_)) <-> Id(fa) + } + + /** + * Composition Law for various kind of Applivatives + */ + val traversableCompositionLaw: LawsF.Covariant[DeriveEqualTraversable, Equal] = { + compositionLawCase[Id, Id] + compositionLawCase[Option, List] + compositionLawCase[List, Option] + } + + /** + * Composition law + * traverse (Compose . fmap g . f) ta = Compose . fmap (traverse g) . traverse f $ ta + */ + private def compositionLawCase[F[+_]: IdentityBoth: Covariant: DeriveEqual, G[ + +_ + ]: IdentityBoth: Covariant: DeriveEqual]: LawsF.Covariant[DeriveEqualTraversable, Equal] = + new LawsF.Covariant.ComposeLaw[DeriveEqualTraversable, Equal]("traversableCompositionLaw") { + def apply[T[+_]: DeriveEqualTraversable, A: Equal, B: Equal, C: Equal]( + ta: T[A], + f: A => B, + g: B => C + ): TestResult = { + val fA: A => F[B] = f.map(_.succeed[F]) + val gA: B => G[C] = g.map(_.succeed[G]) + val left: Nested[F, G, T[C]] = Nested(ta.foreach(fA).map(_.foreach(gA))) + val right: Nested[F, G, T[C]] = ta.foreach(a => Nested(fA(a).map(gA)): Nested[F, G, C]) + left <-> right + } + } + /** * The set of all laws that instances of `Traversable` must satisfy. */ val laws: LawsF.Covariant[DeriveEqualTraversable, Equal] = - Covariant.laws + traversableIdentityLaw + traversableCompositionLaw + Covariant.laws /** * Summons an implicit `Traversable`. */ def apply[F[+_]](implicit traversable: Traversable[F]): Traversable[F] = traversable + + implicit def NestedTraversable[F[+_]: Traversable, G[+_]: Traversable] + : Traversable[({ type lambda[+A] = Nested[F, G, A] })#lambda] = + new Traversable[({ type lambda[+A] = Nested[F, G, A] })#lambda] { + override def foreach[E[+_]: IdentityBoth: Covariant, A, B](ta: Nested[F, G, A])( + f: A => E[B] + ): E[Nested[F, G, B]] = + Nested.wrapAll(Nested.unwrap[F[G[A]]](ta).foreach(_.foreach(f))) + + private lazy val nestedCovariant = Covariant.NestedCovariant[F, G] + + override def map[A, B](f: A => B): Nested[F, G, A] => Nested[F, G, B] = nestedCovariant.map(f) + } } trait TraversableSyntax { diff --git a/core/shared/src/main/scala/zio/prelude/coherent/coherent.scala b/core/shared/src/main/scala/zio/prelude/coherent/coherent.scala index 59d46eda1..b9e917f62 100644 --- a/core/shared/src/main/scala/zio/prelude/coherent/coherent.scala +++ b/core/shared/src/main/scala/zio/prelude/coherent/coherent.scala @@ -210,6 +210,11 @@ object DeriveEqualTraversable { deriveEqual0.derive def foreach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]] = traversable0.foreach(fa)(f) + + // Traversable provides default implementation for Covariant.map, + // so if we want properly check Covariant laws for Traversable instance with overrode 'map' + // we need to forward it + override def map[A, B](f: A => B): F[A] => F[B] = traversable0.map(f) } } diff --git a/core/shared/src/main/scala/zio/prelude/newtypes/package.scala b/core/shared/src/main/scala/zio/prelude/newtypes/package.scala index e78ad61ee..5526efcf4 100644 --- a/core/shared/src/main/scala/zio/prelude/newtypes/package.scala +++ b/core/shared/src/main/scala/zio/prelude/newtypes/package.scala @@ -91,4 +91,14 @@ package object newtypes { object FailureOut extends NewtypeF type FailureOut[+A] = FailureOut.Type[A] + + /** + * A newtype representing Right-to-left composition of functors. + * If F[_] and G[_] are both Covariant, then Nested[F, G, *] is also a Covariant + * If F[_] and G[_] are both IdentityBoth, then Nested[F, G, *] is also an IdentityBoth + * If F[_] and G[_] are both Traversable, then Nested[F, G, *] is also a Traversable + */ + object Nested extends NewtypeF + + type Nested[F[+_], G[+_], +A] = Nested.Type[F[G[A]]] } diff --git a/core/shared/src/test/scala/zio/prelude/CovariantSpec.scala b/core/shared/src/test/scala/zio/prelude/CovariantSpec.scala index b82b72483..e28041e6c 100644 --- a/core/shared/src/test/scala/zio/prelude/CovariantSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/CovariantSpec.scala @@ -18,7 +18,8 @@ object CovariantSpec extends DefaultRunnableSpec { testM("cause")(checkAllLaws(Covariant)(GenFs.cause, Gen.anyInt)), testM("chunk")(checkAllLaws(Covariant)(GenF.chunk, Gen.anyInt)), testM("exit")(checkAllLaws(Covariant)(GenFs.exit(Gen.causes(Gen.anyInt, Gen.throwable)), Gen.anyInt)), - testM("nonEmptyChunk")(checkAllLaws(Covariant)(GenFs.nonEmptyChunk, Gen.anyInt)) + testM("nonEmptyChunk")(checkAllLaws(Covariant)(GenFs.nonEmptyChunk, Gen.anyInt)), + testM("Nested[vector,cause]")(checkAllLaws(Covariant)(GenFs.nested(GenF.vector, GenFs.cause), Gen.anyInt)) ) ) } diff --git a/core/shared/src/test/scala/zio/prelude/IdentityBothSpec.scala b/core/shared/src/test/scala/zio/prelude/IdentityBothSpec.scala index 00f8332ae..7578c1f54 100644 --- a/core/shared/src/test/scala/zio/prelude/IdentityBothSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/IdentityBothSpec.scala @@ -1,7 +1,7 @@ package zio.prelude -import zio.test._ import zio.test.laws._ +import zio.test.{ testM, _ } object IdentityBothSpec extends DefaultRunnableSpec { @@ -11,7 +11,10 @@ object IdentityBothSpec extends DefaultRunnableSpec { testM("either")(checkAllLaws(IdentityBoth)(GenF.either(Gen.anyInt), Gen.anyInt)), testM("list")(checkAllLaws(IdentityBoth)(GenF.list, Gen.anyInt)), testM("option")(checkAllLaws(IdentityBoth)(GenF.option, Gen.anyInt)), - testM("try")(checkAllLaws(IdentityBoth)(GenFs.tryScala, Gen.anyInt)) + testM("try")(checkAllLaws(IdentityBoth)(GenFs.tryScala, Gen.anyInt)), { + implicit val invariant = Covariant.NestedCovariant[List, Option] + testM("Nested[list,option]")(checkAllLaws(IdentityBoth)(GenFs.nested(GenF.list, GenF.option), Gen.anyInt)) + } ) ) } diff --git a/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala b/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala index 46e27f2a1..c03e01b03 100644 --- a/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala @@ -1,8 +1,8 @@ package zio.prelude import zio.random.Random -import zio.test._ import zio.test.laws._ +import zio.test.{ Sized, testM, _ } import zio.{ Chunk, NonEmptyChunk } object TraversableSpec extends DefaultRunnableSpec { @@ -39,7 +39,8 @@ object TraversableSpec extends DefaultRunnableSpec { testM("list")(checkAllLaws(Traversable)(GenF.list, Gen.anyInt)), testM("map")(checkAllLaws(Traversable)(GenFs.map(Gen.anyInt), Gen.anyInt)), testM("option")(checkAllLaws(Traversable)(GenF.option, Gen.anyInt)), - testM("vector")(checkAllLaws(Traversable)(GenF.vector, Gen.anyInt)) + testM("vector")(checkAllLaws(Traversable)(GenF.vector, Gen.anyInt)), + testM("Nested[vector,option]")(checkAllLaws(Traversable)(GenFs.nested(GenF.vector, GenF.option), Gen.anyInt)) ), suite("combinators")( testM("contains") {