Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define laws for Traversable #373 #449

Open
wants to merge 1 commit into
base: series/1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be worried that the Haskell names could confuse some users/contributors. I think we should use the ZIO Prelude terminology here, so foreach (instead of traverse) and map (instead of fmap).

*/
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]]]
}
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))
)
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package zio.prelude

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

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