diff --git a/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala b/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala index a47e0ef17..bab8a0785 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, BothF, Failure, NestedF, OrF } import zio.stm.ZSTM import zio.stream.{ ZSink, ZStream } import zio.test.TestResult @@ -17,12 +17,19 @@ import scala.util.{ Success, Try } * and `F[B]` to produce an `F[(A, B)]`. */ @implicitNotFound("No implicit AssociativeBoth defined for ${F}.") -trait AssociativeBoth[F[_]] { +trait AssociativeBoth[F[_]] { self => /** * Combines two values of types `F[A]` and `F[B]` to produce an `F[(A, B)]`. */ def both[A, B](fa: => F[A], fb: => F[B]): F[(A, B)] + + /** Combine with another `AssociativeBoth` to produce an `AssociativeBoth[(F[A], G[A])]`. */ + final def bothF[G[_]](g: AssociativeBoth[G]): AssociativeBoth[({ type lambda[A] = (F[A], G[A]) })#lambda] = + new AssociativeBoth[({ type lambda[A] = (F[A], G[A]) })#lambda] { + def both[A, B](faga: => (F[A], G[A]), fbgb: => (F[B], G[B])): (F[(A, B)], G[(A, B)]) = + (self.both(faga._1, fbgb._1), g.both(faga._2, fbgb._2)) + } } object AssociativeBoth extends LawfulF.Invariant[AssociativeBothDeriveEqualInvariant, Equal] { @@ -1144,6 +1151,57 @@ object AssociativeBoth extends LawfulF.Invariant[AssociativeBothDeriveEqualInvar def both[A, B](fa: => List[A], fb: => List[B]): List[(A, B)] = fa.flatMap(a => fb.map(b => (a, b))) } + final def composeF[F[+_]: IdentityBoth: Covariant, G[+_]](implicit + G: IdentityBoth[G] + ): IdentityBoth[({ type lambda[+A] = F[G[A]] })#lambda] = + new IdentityBoth[({ type lambda[+A] = F[G[A]] })#lambda] { + def any: F[G[Any]] = + G.any.succeed[F] + + def both[A, B](fa: => F[G[A]], fb: => F[G[B]]): F[G[(A, B)]] = + fa.zipWith(fb)(_ zip _) + } + + final def bothF[F[+_], G[+_]](implicit + F: IdentityBoth[F], + G: IdentityBoth[G] + ): IdentityBoth[({ type lambda[+A] = (F[A], G[A]) })#lambda] = + new IdentityBoth[({ type lambda[+A] = (F[A], G[A]) })#lambda] { + private val bothFG = F.bothF[G](G) + + def any: (F[Any], G[Any]) = + (F.any, G.any) + + def both[A, B](fa: => (F[A], G[A]), fb: => (F[B], G[B])): (F[(A, B)], G[(A, B)]) = + bothFG.both(fa, fb) + } + + implicit def NestedFIdentityBoth[F[+_]: IdentityBoth: Covariant, G[+_]: IdentityBoth] + : IdentityBoth[({ type lambda[+A] = NestedF[F, G, A] })#lambda] = + new IdentityBoth[({ type lambda[+A] = NestedF[F, G, A] })#lambda] { + private val FG = composeF[F, G] + + def any: NestedF[F, G, Any] = + NestedF(FG.any) + + def both[A, B](fa: => NestedF[F, G, A], fb: => NestedF[F, G, B]): NestedF[F, G, (A, B)] = + NestedF(FG.both(NestedF.unwrap[F[G[A]]](fa), NestedF.unwrap[F[G[B]]](fb))) + } + + implicit def BothFIdentityBoth[F[+_], G[+_]](implicit + F: IdentityBoth[F], + G: IdentityBoth[G] + ): IdentityBoth[({ type lambda[+A] = BothF[F, G, A] })#lambda] = + new IdentityBoth[({ type lambda[+A] = BothF[F, G, A] })#lambda] { + private val FG = AssociativeBoth.bothF[F, G] + + def any: BothF[F, G, Any] = + BothF(FG.any) + + def both[A, B](fga: => BothF[F, G, A], fgb: => BothF[F, G, B]): BothF[F, G, (A, B)] = + BothF(FG.both(BothF.unwrap(fga), BothF.unwrap(fgb))) + } + /** * The `AssociativeBoth` instance for `NonEmptyChunk`. */ diff --git a/core/shared/src/main/scala/zio/prelude/Covariant.scala b/core/shared/src/main/scala/zio/prelude/Covariant.scala index 81584e974..932feef6e 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.{ BothF, NestedF } import zio.test.TestResult import zio.test.laws._ @@ -60,6 +61,13 @@ trait Covariant[F[+_]] extends CovariantSubset[F, AnyType] with Invariant[F] { s new Contravariant[({ type lambda[-A] = F[G[A]] })#lambda] { def contramap[A, B](f: B => A): F[G[A]] => F[G[B]] = self.map(g.contramap(f)) } + + /** Construct the product of two covariant functors. */ + final def both[G[+_]](g: Covariant[G]): Covariant[({ type lambda[+A] = (F[A], G[A]) })#lambda] = + new Covariant[({ type lambda[+A] = (F[A], G[A]) })#lambda] { + def map[A, B](f: A => B): ((F[A], G[A])) => (F[B], G[B]) = (faga: (F[A], G[A])) => + (self.map(f)(faga._1), g.map(f)(faga._2)) + } } object Covariant extends LawfulF.Covariant[CovariantDeriveEqual, Equal] { @@ -95,6 +103,27 @@ object Covariant extends LawfulF.Covariant[CovariantDeriveEqual, Equal] { def apply[F[+_]](implicit covariant: Covariant[F]): Covariant[F] = covariant + implicit def NestedFCovariant[F[+_], G[+_]](implicit + F: Covariant[F], + G: Covariant[G] + ): Covariant[({ type lambda[+A] = NestedF[F, G, A] })#lambda] = + new Covariant[({ type lambda[+A] = NestedF[F, G, A] })#lambda] { + private lazy val FG = F.compose(G) + + def map[A, B](f: A => B): NestedF[F, G, A] => NestedF[F, G, B] = (fga: NestedF[F, G, A]) => + NestedF(FG.map(f)(NestedF.unwrap[F[G[A]]](fga))) + } + + implicit def BothFCovariant[F[+_], G[+_]](implicit + F: Covariant[F], + G: Covariant[G] + ): Covariant[({ type lambda[+A] = BothF[F, G, A] })#lambda] = + new Covariant[({ type lambda[+A] = BothF[F, G, A] })#lambda] { + private lazy val FG = F.both(G) + + def map[A, B](f: A => B): BothF[F, G, A] => BothF[F, G, B] = (fga: BothF[F, G, A]) => + BothF(FG.map(f)(BothF.unwrap[(F[A], G[A])](fga))) + } } 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..26e04c2f4 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.NestedF 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] = NestedF[F, G, A] })#lambda, Equal] = + new Derive[({ type lambda[A] = NestedF[F, G, A] })#lambda, Equal] { + override def derive[A: Equal]: Equal[NestedF[F, G, A]] = + Equal.NestedFEqual(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..9dcc8af27 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.{ BothF, NestedF } import zio.test.TestResult import zio.test.laws.{ Lawful, Laws } import zio.{ Cause, Chunk, Exit, Fiber, NonEmptyChunk, ZTrace } @@ -204,6 +205,18 @@ object Equal extends Lawful[Equal] { def default[A]: Equal[A] = make(_ == _) + /** `Equal` instance for `Id` */ + implicit def IdEqual[A: Equal]: Equal[Id[A]] = (l: Id[A], r: Id[A]) => Id.unwrap[A](l) === Id.unwrap[A](r) + + /** Constructs an `Equal` instance for two nested type constructors. */ + implicit def NestedFEqual[F[+_], G[+_], A](implicit eq0: Equal[F[G[A]]]): Equal[NestedF[F, G, A]] = + (l: NestedF[F, G, A], r: NestedF[F, G, A]) => eq0.checkEqual(NestedF.unwrap[F[G[A]]](l), NestedF.unwrap[F[G[A]]](r)) + + /** Constructs an `Equal` instance for the product of two type constructors. */ + implicit def BothEqual[F[+_], G[+_], A](implicit eq0: Equal[(F[A], G[A])]): Equal[BothF[F, G, A]] = + (l: BothF[F, G, A], r: BothF[F, G, A]) => + eq0.checkEqual(BothF.unwrap[(F[A], G[A])](l), BothF.unwrap[(F[A], 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..26260d5df 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, NestedF } import zio.random.Random import zio.test.Gen.oneOf import zio.test._ @@ -51,12 +51,30 @@ object GenFs { oneOf(Gen.throwable.map(Future.failed), gen.map(Future.successful)) } + def id: GenF[Random with Sized, Id] = new GenF[Random with Sized, Id] { + def apply[R1 <: Random with Sized, A](gen: Gen[R1, A]): Gen[R1, Id[A]] = + gen.map(Id[A]) + } + def map[R <: Random with Sized, K](k: Gen[R, K]): GenF[R, ({ type lambda[+v] = Map[K, v] })#lambda] = new GenF[R, ({ type lambda[+v] = Map[K, v] })#lambda] { def apply[R1 <: R, V](v: Gen[R1, V]): Gen[R1, Map[K, V]] = Gen.mapOf(k, v) } + def nested[F[+_], G[+_], RF, RG]( + genF: GenF[RF, F], + genG: GenF[RG, G] + ): GenF[RF with RG, ({ type lambda[+A] = NestedF[F, G, A] })#lambda] = + new GenF[RF with RG, ({ type lambda[+A] = NestedF[F, G, A] })#lambda] { + override def apply[R1 <: RF with RG, A](gen: Gen[R1, A]): Gen[R1, NestedF[F, G, A]] = { + val value: Gen[R1 with RG with RF, newtypes.NestedF.newtypeF.Type[F[G[A]]]] = + genF(genG(gen)).map(NestedF(_): NestedF[F, G, A]) + value + } + + } + /** * A generator of `NonEmptyChunk` values. */ diff --git a/core/shared/src/main/scala/zio/prelude/Instances.scala b/core/shared/src/main/scala/zio/prelude/Instances.scala new file mode 100644 index 000000000..b82f3405c --- /dev/null +++ b/core/shared/src/main/scala/zio/prelude/Instances.scala @@ -0,0 +1,72 @@ +package zio.prelude + +import zio.prelude.classic.Applicative +import zio.prelude.newtypes.{ BothF, NestedF } + +// not sure about putting this here +object Instances { + + object Applicative { + def apply[F[+_]](implicit covariant: Covariant[F], identityBoth: IdentityBoth[F]): Applicative[F] = new Covariant[F] + with IdentityBoth[F] { + def map[A, B](f: A => B): F[A] => F[B] = + covariant.map(f) + def any: F[Any] = + identityBoth.any + def both[A, B](fa: => F[A], fb: => F[B]): F[(A, B)] = + identityBoth.both(fa, fb) + } + + implicit def nestedF[F[+_], G[+_]](implicit + C: Covariant[({ type lambda[+A] = NestedF[F, G, A] })#lambda], + IB: IdentityBoth[({ type lambda[+A] = NestedF[F, G, A] })#lambda] + ): Applicative[({ type lambda[+A] = NestedF[F, G, A] })#lambda] = + new Covariant[({ type lambda[+A] = NestedF[F, G, A] })#lambda] + with IdentityBoth[({ type lambda[+A] = NestedF[F, G, A] })#lambda] { + + def map[A, B](f: A => B): NestedF[F, G, A] => NestedF[F, G, B] = + C.map(f) + def any: NestedF[F, G, Any] = + IB.any + def both[A, B](fa: => NestedF[F, G, A], fb: => NestedF[F, G, B]): NestedF[F, G, (A, B)] = + IB.both(fa, fb) + } + + implicit def applicative[F[+_]](implicit c0: Covariant[F], i0: IdentityBoth[F]): Applicative[F] = + Applicative[F](c0, i0) + + implicit def bothF[F[+_], G[+_]](implicit + C: Covariant[({ type lambda[+A] = BothF[F, G, A] })#lambda], + IB: IdentityBoth[({ type lambda[+A] = BothF[F, G, A] })#lambda] + ): Applicative[({ type lambda[+A] = BothF[F, G, A] })#lambda] = + new Covariant[({ type lambda[+A] = BothF[F, G, A] })#lambda] + with IdentityBoth[({ type lambda[+A] = BothF[F, G, A] })#lambda] { + + def map[A, B](f: A => B): BothF[F, G, A] => BothF[F, G, B] = + C.map(f) + def any: BothF[F, G, Any] = + IB.any + def both[A, B](fga: => BothF[F, G, A], fgb: => BothF[F, G, B]): BothF[F, G, (A, B)] = + IB.both(fga, fgb) + } + } + + trait ApplicativeDeriveEqual[F[+_]] extends Covariant[F] with IdentityBoth[F] with DeriveEqual[F] + + object ApplicativeDeriveEqual { + implicit def derive[F[+_]](implicit + applicative0: Applicative[F], + deriveEqual0: DeriveEqual[F] + ): ApplicativeDeriveEqual[F] = + new ApplicativeDeriveEqual[F] { + def any: F[Any] = + applicative0.any + def both[A, B](fa: => F[A], fb: => F[B]): F[(A, B)] = + applicative0.both(fa, fb) + def derive[A: Equal]: Equal[F[A]] = + deriveEqual0.derive + def map[A, B](f: A => B): F[A] => F[B] = + applicative0.map(f) + } + } +} diff --git a/core/shared/src/main/scala/zio/prelude/NonEmptyTraversable.scala b/core/shared/src/main/scala/zio/prelude/NonEmptyTraversable.scala index 05c7591c0..fc4dbab8d 100644 --- a/core/shared/src/main/scala/zio/prelude/NonEmptyTraversable.scala +++ b/core/shared/src/main/scala/zio/prelude/NonEmptyTraversable.scala @@ -1,8 +1,8 @@ package zio.prelude import zio.prelude.coherent.DeriveEqualNonEmptyTraversable +import zio.prelude.laws._ import zio.prelude.newtypes.{ Max, Min } -import zio.test.laws._ import zio.{ ChunkBuilder, NonEmptyChunk } /** @@ -134,12 +134,14 @@ trait NonEmptyTraversable[F[+_]] extends Traversable[F] { def toNonEmptyList[A](fa: F[A]): NonEmptyList[A] = reduceMapLeft(fa)(NonEmptyList.single)((as, a) => NonEmptyList.cons(a, as)).reverse } -object NonEmptyTraversable extends LawfulF.Covariant[DeriveEqualNonEmptyTraversable, Equal] { + +import zio.prelude.Instances.ApplicativeDeriveEqual +object NonEmptyTraversable extends LawfulF.Traversable[DeriveEqualNonEmptyTraversable, ApplicativeDeriveEqual, Equal] { /** * The set of all laws that instances of `NonEmptyTraversable` must satisfy. */ - val laws: LawsF.Covariant[DeriveEqualNonEmptyTraversable, Equal] = + val laws: LawsF.Traversable[DeriveEqualNonEmptyTraversable, ApplicativeDeriveEqual, Equal] = Traversable.laws /** diff --git a/core/shared/src/main/scala/zio/prelude/Traversable.scala b/core/shared/src/main/scala/zio/prelude/Traversable.scala index fa976e964..1ddcdf9db 100644 --- a/core/shared/src/main/scala/zio/prelude/Traversable.scala +++ b/core/shared/src/main/scala/zio/prelude/Traversable.scala @@ -1,8 +1,8 @@ package zio.prelude import zio.prelude.coherent.DeriveEqualTraversable -import zio.prelude.newtypes.{ And, First, Max, Min, Or, Prod, Sum } -import zio.test.laws._ +import zio.prelude.newtypes.{ And, BothF, First, Max, Min, NestedF, Or, Prod, Sum } +import zio.test.TestResult import zio.{ Chunk, ChunkBuilder, NonEmptyChunk } /** @@ -287,13 +287,87 @@ trait Traversable[F[+_]] extends Covariant[F] { self => } } -object Traversable extends LawfulF.Covariant[DeriveEqualTraversable, Equal] { +object Traversable + extends zio.prelude.laws.LawfulF.Traversable[DeriveEqualTraversable, Instances.ApplicativeDeriveEqual, Equal] { + import zio.prelude.Instances.Applicative + import zio.prelude.Instances.Applicative.nestedF + import zio.prelude.Instances.ApplicativeDeriveEqual + import zio.prelude.laws.ZLawsF + + /** Traversing by `Id` is equivalent to mapping. */ + val identityLaw: zio.test.laws.LawsF.Covariant[DeriveEqualTraversable, Equal] = + new ZLawsF.Covariant.MapLaw[DeriveEqualTraversable, Equal]("identityLaw") { + def apply[F[+_]: DeriveEqualTraversable, A: Equal, B: Equal](fa: F[A], f: A => B): TestResult = + Id.unwrap(fa.foreach(f.andThen(Id[B]))) <-> fa.map(f) + } + + /** Two sequentially dependent effects can be fused into their composition */ + val sequentialFusionLaw: zio.prelude.laws.LawsF.Traversable[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal] = + new ZLawsF.Traversable.SequentialFusionLaw[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal]( + "sequentialFusionLaw" + ) { + def apply[F[+_], G[+_], H[+_], A, B, C](fa: F[A], agb: A => G[B], bhc: B => H[C])(implicit + F: DeriveEqualTraversable[F], + G: ApplicativeDeriveEqual[G], + H: ApplicativeDeriveEqual[H], + C: Equal[C] + ): TestResult = { + val GH = Applicative.nestedF[G, H] + val left = NestedF(fa.foreach(agb).map(_.foreach(bhc))) + val right = fa.foreach(a => (GH.both(NestedF(agb(a).map(bhc)), GH.any): NestedF[G, H, (C, Any)]).map(_._1)) + left <-> right + } + } + + /** Traversal with `succeed` is the same as applying `succeed` directly */ + val purityLaw: zio.prelude.laws.LawsF.Traversable[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal] = + new ZLawsF.Traversable.PurityLaw[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal]("purityLaw") { + def apply[F[+_]: DeriveEqualTraversable, G[+_]: ApplicativeDeriveEqual, A: Equal](fa: F[A]): TestResult = + fa.foreach(Applicative[G].any.as[A](_)) <-> fa.succeed[G] + } + + /** Flipping each of two effects sequentially is the same as flipping once on a composed effect. */ + val naturalityLaw: zio.prelude.laws.LawsF.Traversable[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal] = + new ZLawsF.Traversable.NaturalityFusionLaw[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal]("naturalityLaw") { + def apply[F[+_]: DeriveEqualTraversable, G[+_]: ApplicativeDeriveEqual, H[+_]: ApplicativeDeriveEqual, A: Equal]( + fa: F[A], + aga: A => G[A], + aha: A => H[A] + ): TestResult = { + val GH = Applicative.nestedF[G, H] + val left = NestedF(fa.map(aga).flip.map(a => a.map(aha).flip)) + val right = fa.map(a => (GH.both(NestedF(aga(a).map(aha)), GH.any): NestedF[G, H, (A, Any)]).map(_._1)).flip + left <-> right + } + } + + /** Two independent effects can be fused into their product */ + val parallelFusionLaw: zio.prelude.laws.LawsF.Traversable[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal] = + new ZLawsF.Traversable.ParallelFusionLaw[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal]( + "parallelFusionLaw" + ) { + def apply[F[+_], G[+_], H[+_], A, B]( + fa: F[A], + agb: A => G[B], + ahb: A => H[B] + )(implicit + F: DeriveEqualTraversable[F], + G: ApplicativeDeriveEqual[G], + H: ApplicativeDeriveEqual[H], + B: Equal[B] + ): TestResult = { + val GH = Applicative.bothF[G, H] + val left = BothF((fa.foreach(agb), fa.foreach(ahb))) + val right = fa.foreach(a => (GH.both(BothF((agb(a), ahb(a))), GH.any): BothF[G, H, (B, Any)]).map(_._1)) + left <-> right + } + } /** * The set of all laws that instances of `Traversable` must satisfy. */ - val laws: LawsF.Covariant[DeriveEqualTraversable, Equal] = - Covariant.laws + val laws: zio.prelude.laws.LawsF.Traversable[DeriveEqualTraversable, ApplicativeDeriveEqual, Equal] = + purityLaw + identityLaw + sequentialFusionLaw + naturalityLaw + parallelFusionLaw + Covariant.laws /** * Summons an implicit `Traversable`. diff --git a/core/shared/src/main/scala/zio/prelude/laws/ZLawfulF.scala b/core/shared/src/main/scala/zio/prelude/laws/ZLawfulF.scala new file mode 100644 index 000000000..efe2b15b8 --- /dev/null +++ b/core/shared/src/main/scala/zio/prelude/laws/ZLawfulF.scala @@ -0,0 +1,15 @@ +package zio.prelude.laws + +object ZLawfulF { + + /** `ZLawful` for traversable type constructors. */ + trait Traversable[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_], -R] { self => + def laws: ZLawsF.Traversable[CapsF, CapsG, Caps, R] + def +[CapsF1[x[+_]] <: CapsF[x], CapsG1[x[+_]] <: CapsG[x], Caps1[x] <: Caps[x], R1 <: R]( + that: Traversable[CapsF1, CapsG1, Caps1, R1] + ): Traversable[CapsF1, CapsG1, Caps1, R1] = + new Traversable[CapsF1, CapsG1, Caps1, R1] { + val laws = self.laws + that.laws + } + } +} diff --git a/core/shared/src/main/scala/zio/prelude/laws/ZLawsF.scala b/core/shared/src/main/scala/zio/prelude/laws/ZLawsF.scala new file mode 100644 index 000000000..07117fe56 --- /dev/null +++ b/core/shared/src/main/scala/zio/prelude/laws/ZLawsF.scala @@ -0,0 +1,122 @@ +package zio.prelude.laws + +import zio.test.laws.GenF +import zio.test.laws.ZLawsF.Covariant +import zio.test.{ Gen, TestConfig, TestResult, check } +import zio.{ URIO, ZIO } + +object ZLawsF { + + abstract class Traversable[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_], -R] { self => + + def run[R1 <: R with TestConfig, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + genF: GenF[R1, F], + genG: GenF[R1, G], + gen: Gen[R1, A] + ): ZIO[R1, Nothing, TestResult] + + def +[CapsF1[x[+_]] <: CapsF[x], CapsG1[x[+_]] <: CapsG[x], Caps1[x] <: Caps[x], R1 <: R]( + that: Traversable[CapsF1, CapsG1, Caps1, R1] + ): Traversable[CapsF1, CapsG1, Caps1, R1] = + Traversable.Both(self, that) + + def +[CapsF1[x[+_]] <: CapsF[x], Caps1[x] <: Caps[x], R1 <: R]( + that: Covariant[CapsF1, Caps1, R1] + ): Traversable[CapsF1, CapsG, Caps1, R1] = + Traversable.WithCovariant(self, that) + } + + object Covariant { + abstract class MapLaw[-CapsF[_[+_]], -Caps[_]](label: String) extends Covariant[CapsF, Caps, Any] { self => + def apply[F[+_]: CapsF, A: Caps, B: Caps](fa: F[A], f: A => B): TestResult + final def run[R <: TestConfig, F[+_]: CapsF, A: Caps](genF: GenF[R, F], gen: Gen[R, A]): URIO[R, TestResult] = + check(genF(gen), Gen.function(gen))(apply(_, _).map(_.label(label))) + } + } + + object Traversable { + private final case class Both[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_], -R]( + left: Traversable[CapsF, CapsG, Caps, R], + right: Traversable[CapsF, CapsG, Caps, R] + ) extends Traversable[CapsF, CapsG, Caps, R] { + override def run[R1 <: R with TestConfig, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + genF: GenF[R1, F], + genG: GenF[R1, G], + gen: Gen[R1, A] + ): ZIO[R1, Nothing, TestResult] = + left.run(genF, genG, gen).zipWith(right.run(genF, genG, gen))(_ && _) + } + + private final case class WithCovariant[-CapsF1[_[+_]], -CapsG[_[+_]], -Caps[_], -R]( + left: Traversable[CapsF1, CapsG, Caps, R], + right: Covariant[CapsF1, Caps, R] + ) extends Traversable[CapsF1, CapsG, Caps, R] { + override def run[R1 <: R with TestConfig, F[+_]: CapsF1, G[+_]: CapsG, A: Caps]( + genF: GenF[R1, F], + genG: GenF[R1, G], + gen: Gen[R1, A] + ): ZIO[R1, Nothing, TestResult] = + left.run(genF, genG, gen).zipWith(right.run(genF, gen))(_ && _) + } + + abstract class PurityLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]](label: String) + extends Traversable[CapsF, CapsG, Caps, Any] { self => + def apply[F[+_]: CapsF, G[+_]: CapsG, A: Caps](fa: F[A]): TestResult + + final def run[R1 <: TestConfig, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + genF: GenF[R1, F], + genG: GenF[R1, G], + gen: Gen[R1, A] + ): ZIO[R1, Nothing, TestResult] = + check(genF(gen))(apply[F, G, A](_).map(_.label(label))) + } + + abstract class NaturalityFusionLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]](label: String) + extends Traversable[CapsF, CapsG, Caps, Any] { + def apply[F[+_]: CapsF, G[+_]: CapsG, H[+_]: CapsG, A: Caps]( + fa: F[A], + aga: A => G[A], + aha: A => H[A] + ): TestResult + + final def run[R <: TestConfig, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + genF: GenF[R, F], + genG: GenF[R, G], + gen: Gen[R, A] + ): URIO[R, TestResult] = + check(genF(gen), Gen.function(genG(gen)), Gen.function(genG(gen)))(apply(_, _, _).map(_.label(label))) + } + + abstract class SequentialFusionLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]](label: String) + extends Traversable[CapsF, CapsG, Caps, Any] { self => + def apply[F[+_]: CapsF, G[+_]: CapsG, H[+_]: CapsG, A, B, C: Caps]( + fa: F[A], + agb: A => G[B], + bhc: B => H[C] + ): TestResult + + final def run[R <: TestConfig, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + genF: GenF[R, F], + genG: GenF[R, G], + gen: Gen[R, A] + ): URIO[R, TestResult] = + check(genF(gen), Gen.function(genG(gen)), Gen.function(genG(gen)))(apply(_, _, _).map(_.label(label))) + } + + abstract class ParallelFusionLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]](label: String) + extends Traversable[CapsF, CapsG, Caps, Any] { + def apply[F[+_]: CapsF, G[+_]: CapsG, H[+_]: CapsG, A, B: Caps]( + fa: F[A], + aga: A => G[B], + aha: A => H[B] + ): TestResult + + final def run[R <: TestConfig, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + genF: GenF[R, F], + genG: GenF[R, G], + gen: Gen[R, A] + ): URIO[R, TestResult] = + check(genF(gen), Gen.function(genG(gen)), Gen.function(genG(gen)))(apply(_, _, _).map(_.label(label))) + } + } +} diff --git a/core/shared/src/main/scala/zio/prelude/laws/package.scala b/core/shared/src/main/scala/zio/prelude/laws/package.scala new file mode 100644 index 000000000..2acd3a9f2 --- /dev/null +++ b/core/shared/src/main/scala/zio/prelude/laws/package.scala @@ -0,0 +1,33 @@ +package zio.prelude + +import zio.URIO +import zio.test.laws.GenF +import zio.test.{ Gen, TestConfig, TestResult } + +package object laws { + object LawfulF { + type Traversable[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]] = ZLawfulF.Traversable[CapsF, CapsG, Caps, Any] + } + + object LawsF { + type Traversable[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]] = ZLawsF.Traversable[CapsF, CapsG, Caps, Any] + + object Covariant { + type MapLaw[-CapsF[_[+_]], -Caps[_]] = ZLawsF.Covariant.MapLaw[CapsF, Caps] + } + + object Traversable { + type PurityLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]] = + ZLawsF.Traversable.PurityLaw[CapsF, CapsG, Caps] + type SequentialFusionLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]] = + ZLawsF.Traversable.SequentialFusionLaw[CapsF, CapsG, Caps] + type NaturalityFusionLaw[-CapsF[_[+_]], -CapsG[_[+_]], -Caps[_]] = + ZLawsF.Traversable.NaturalityFusionLaw[CapsF, CapsG, Caps] + } + } + + def checkAllLaws[CapsF[_[+_]], CapsG[_[+_]], Caps[_], R <: TestConfig, R1 <: R, F[+_]: CapsF, G[+_]: CapsG, A: Caps]( + lawful: ZLawfulF.Traversable[CapsF, CapsG, Caps, R] + )(genF: GenF[R1, F], genG: GenF[R1, G], gen: Gen[R1, A]): URIO[R1, TestResult] = + lawful.laws.run(genF, genG, gen) +} 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..eb04eb9e1 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 type constructors. */ + object NestedF extends NewtypeF + + type NestedF[F[+_], G[+_], +A] = NestedF.Type[F[G[A]]] + + /** A newtype representing the product of two type constructors. */ + object BothF extends NewtypeF + + type BothF[F[+_], G[+_], +A] = BothF.Type[(F[A], G[A])] } diff --git a/core/shared/src/test/scala/zio/prelude/NonEmptyListSpec.scala b/core/shared/src/test/scala/zio/prelude/NonEmptyListSpec.scala index eca687ec1..4be205ad0 100644 --- a/core/shared/src/test/scala/zio/prelude/NonEmptyListSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/NonEmptyListSpec.scala @@ -1,5 +1,6 @@ package zio.prelude +import zio.prelude.Instances.Applicative._ import zio.random.Random import zio.test._ import zio.test.laws._ @@ -50,7 +51,9 @@ object NonEmptyListSpec extends DefaultRunnableSpec { testM("hash")(checkAllLaws(Hash)(genNonEmptyList)), testM("identityBoth")(checkAllLaws(IdentityBoth)(GenFs.nonEmptyList, Gen.anyInt)), testM("identityFlatten")(checkAllLaws(IdentityFlatten)(GenFs.nonEmptyList, Gen.anyInt)), - testM("nonEmptyTraversable")(checkAllLaws(NonEmptyTraversable)(GenFs.nonEmptyList, Gen.anyInt)), + testM("nonEmptyTraversable")( + zio.prelude.laws.checkAllLaws(NonEmptyTraversable)(GenFs.nonEmptyList, GenFs.id, Gen.anyInt) + ), testM("ord")(checkAllLaws(Ord)(genNonEmptyList)) ), suite("methods")( diff --git a/core/shared/src/test/scala/zio/prelude/NonEmptyTraversableSpec.scala b/core/shared/src/test/scala/zio/prelude/NonEmptyTraversableSpec.scala index bdb9002dd..30cfd3c0d 100644 --- a/core/shared/src/test/scala/zio/prelude/NonEmptyTraversableSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/NonEmptyTraversableSpec.scala @@ -1,8 +1,9 @@ package zio.prelude +import zio.prelude.Instances.Applicative._ +import zio.prelude.laws.checkAllLaws import zio.random.Random import zio.test._ -import zio.test.laws._ object NonEmptyTraversableSpec extends DefaultRunnableSpec { @@ -21,7 +22,7 @@ object NonEmptyTraversableSpec extends DefaultRunnableSpec { def spec: ZSpec[Environment, Failure] = suite("NonEmptyTraversableSpec")( suite("instances")( - testM("nonEmptyChunk")(checkAllLaws(NonEmptyTraversable)(GenFs.nonEmptyChunk, Gen.anyInt)) + testM("nonEmptyChunk")(checkAllLaws(NonEmptyTraversable)(GenFs.nonEmptyChunk, GenFs.id, Gen.anyInt)) ), suite("combinators")( testM("max") { diff --git a/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala b/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala index fcfd0e394..d2c0972c4 100644 --- a/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala @@ -1,5 +1,7 @@ package zio.prelude +import zio.prelude.Instances.Applicative._ +import zio.prelude.laws.checkAllLaws import zio.random.Random import zio.test._ import zio.test.laws._ @@ -38,13 +40,13 @@ object TraversableSpec extends DefaultRunnableSpec { def spec: ZSpec[Environment, Failure] = suite("TraversableSpec")( suite("instances")( - testM("chunk")(checkAllLaws(Traversable)(GenF.chunk, Gen.anyInt)), - testM("either")(checkAllLaws(Traversable)(GenFs.either(Gen.anyInt), Gen.anyInt)), - 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("chunk . option")(checkAllLaws(Traversable)(chunkOptionGenF, Gen.anyInt)) + testM("chunk")(checkAllLaws(Traversable)(GenF.chunk, GenFs.id, Gen.anyInt)), + testM("either")(checkAllLaws(Traversable)(GenFs.either(Gen.anyInt), GenFs.id, Gen.anyInt)), + testM("list")(checkAllLaws(Traversable)(GenF.list, GenFs.id, Gen.anyInt)), + testM("map")(checkAllLaws(Traversable)(GenFs.map(Gen.anyInt), GenFs.id, Gen.anyInt)), + testM("option")(checkAllLaws(Traversable)(GenF.option, GenFs.id, Gen.anyInt)), + testM("vector")(checkAllLaws(Traversable)(GenF.vector, GenFs.id, Gen.anyInt)), + testM("chunk . option")(checkAllLaws(Traversable)(chunkOptionGenF, GenFs.id, Gen.anyInt)) ), suite("combinators")( testM("contains") {