Skip to content

Commit

Permalink
Define laws for Traversable zio#373
Browse files Browse the repository at this point in the history
  • Loading branch information
Badmoonz committed Nov 29, 2020
1 parent a749622 commit 8a35fa0
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 9 deletions.
14 changes: 13 additions & 1 deletion core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
*/
Expand Down
13 changes: 13 additions & 0 deletions core/shared/src/main/scala/zio/prelude/Covariant.scala
Original file line number Diff line number Diff line change
@@ -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._

Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions core/shared/src/main/scala/zio/prelude/Derive.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package zio.prelude

import zio.prelude.newtypes.Nested
import zio.{ Cause, Chunk, Exit, NonEmptyChunk }

import scala.util.Try
Expand Down Expand Up @@ -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`.
*/
Expand Down
12 changes: 12 additions & 0 deletions core/shared/src/main/scala/zio/prelude/Equal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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.
*/
Expand Down
15 changes: 14 additions & 1 deletion core/shared/src/main/scala/zio/prelude/GenFs.scala
Original file line number Diff line number Diff line change
@@ -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._
Expand Down Expand Up @@ -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
}

}
}
2 changes: 2 additions & 0 deletions core/shared/src/main/scala/zio/prelude/Invariant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
56 changes: 54 additions & 2 deletions core/shared/src/main/scala/zio/prelude/Traversable.scala
Original file line number Diff line number Diff line change
@@ -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 }

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
10 changes: 10 additions & 0 deletions core/shared/src/main/scala/zio/prelude/newtypes/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]]]
}
3 changes: 2 additions & 1 deletion core/shared/src/test/scala/zio/prelude/CovariantSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
)
)
}
7 changes: 5 additions & 2 deletions core/shared/src/test/scala/zio/prelude/IdentityBothSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package zio.prelude

import zio.test._
import zio.test.{ testM, _ }
import zio.test.laws._

object IdentityBothSpec extends DefaultRunnableSpec {
Expand All @@ -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))
}
)
)
}
5 changes: 3 additions & 2 deletions core/shared/src/test/scala/zio/prelude/TraversableSpec.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package zio.prelude

import zio.random.Random
import zio.test._
import zio.test.{ Sized, testM, _ }
import zio.test.laws._
import zio.{ Chunk, NonEmptyChunk }

Expand Down Expand Up @@ -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") {
Expand Down

0 comments on commit 8a35fa0

Please sign in to comment.