diff --git a/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala b/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala index d48e429fa..91080db0a 100644 --- a/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala +++ b/core/shared/src/main/scala/zio/prelude/AssociativeBoth.scala @@ -1201,10 +1201,11 @@ object AssociativeBoth extends LawfulF.Invariant[AssociativeBothDeriveEqualInvar } /** - * The `AssociativeBoth` instance for `ZIO`. + * The `IdentityBoth` instance for `ZIO`. */ - implicit def ZIOAssociativeBoth[R, E]: AssociativeBoth[({ type lambda[+a] = ZIO[R, E, a] })#lambda] = - new AssociativeBoth[({ type lambda[+a] = ZIO[R, E, a] })#lambda] { + implicit def ZIOIdentityBoth[R, E]: IdentityBoth[({ type lambda[+a] = ZIO[R, E, a] })#lambda] = + new IdentityBoth[({ type lambda[+a] = ZIO[R, E, a] })#lambda] { + def any: ZIO[R, E, Any] = ZIO.unit def both[A, B](fa: => ZIO[R, E, A], fb: => ZIO[R, E, B]): ZIO[R, E, (A, B)] = fa zip fb } diff --git a/core/shared/src/main/scala/zio/prelude/Traversable.scala b/core/shared/src/main/scala/zio/prelude/Traversable.scala index 0474ab985..2096f6d95 100644 --- a/core/shared/src/main/scala/zio/prelude/Traversable.scala +++ b/core/shared/src/main/scala/zio/prelude/Traversable.scala @@ -122,6 +122,24 @@ trait Traversable[F[+_]] extends Covariant[F] { } } + def forany[G[+ _] : AssociativeEither : IdentityBoth : Covariant, A, B](fa: F[A])(f: A => G[B]): G[Option[B]] = + foldLeft(fa)(None: Option[G[B]]) { + case (Some(s), v) => Some(s.orElse(f(v))) + case (None, v) => Some(f(v)) + } match { + case Some(x) => x.map[Option[B]](Some(_)) + case None => IdentityBoth[G].any.as(None) + } + + def foranyPar[G[+_] : CommutativeEither : IdentityBoth : Covariant, A, B](fa: F[A])(f: A => G[B]): G[Option[B]] = + foldLeft(fa)(None : Option[G[B]]) { + case (Some(s), v) => Some(s.orElsePar(f(v))) + case (None, v) => Some(f(v)) + } match { + case Some(x) => x.map[Option[B]](Some(_)) + case None => IdentityBoth[G].any.as(None) + } + /** * Returns whether the collection is empty. */ diff --git a/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala b/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala index 46e27f2a1..d3121ea0f 100644 --- a/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala +++ b/core/shared/src/test/scala/zio/prelude/TraversableSpec.scala @@ -2,8 +2,11 @@ package zio.prelude import zio.random.Random import zio.test._ +import zio.test.Assertion._ import zio.test.laws._ -import zio.{ Chunk, NonEmptyChunk } +import zio.{Chunk, IO, NonEmptyChunk} + +import scala.collection.Set object TraversableSpec extends DefaultRunnableSpec { @@ -25,6 +28,12 @@ object TraversableSpec extends DefaultRunnableSpec { val genIntFunction: Gen[Random, Int => Int] = Gen.function(genInt) + val genOptIntFunction: Gen[Random, Int => Option[Int]] = + Gen.boolean.zipWith(Gen.function(genInt)) { + case (true, f) => x => Some(f(x)) + case (false, _) => _ => None + } + val genIntFunction2: Gen[Random, (Int, Int) => Int] = Gen.function2(genInt) @@ -115,6 +124,49 @@ object TraversableSpec extends DefaultRunnableSpec { assert(actual)(equalTo(expected)) } }, + testM("forany") { + check(genList, genOptIntFunction) { case (as, f) => + val actual = Traversable[List].forany(as)(f).flatten + val expected = as.map(f).flatten.headOption + assert(actual)(equalTo(expected)) + } + }, + testM("forany IO") { + check(genList, genOptIntFunction) { case (as, f) => + val actual = zio.Runtime.default.unsafeRun(Traversable[List].forany(as)(f.map { + case Some(x) => IO.succeedNow(x) + case None => IO.fail(()) + }(Invariant.Function1Covariant)).option) + val expected : Option[Option[Int]] = { + val allResults = as.map(f) + if(allResults.nonEmpty && allResults.forall(_.isEmpty)) + None + else + Some(allResults.flatten.headOption) + } + assert(actual)(equalTo(expected)) + } + }, + testM("foranyPar IO") { + check(genList, genOptIntFunction) { case (as, f) => + val actual = zio.Runtime.default.unsafeRun(Traversable[List].foranyPar(as)(f.map { + case Some(x) => IO.succeedNow(x) + case None => IO.fail(()) + }(Invariant.Function1Covariant)).option) + val expected : Option[Set[Int]] = { + val allResults = as.map(f) + if(allResults.nonEmpty && allResults.forall(_.isEmpty)) + None + else + Some(allResults.flatten.toSet) + } + (actual, expected) match { + case (None , None) => assertCompletes + case (Some(Some(result)), Some(allPossible)) => assert(List(result))(hasOneOf(allPossible)) + case (Some(None), Some(allPossible)) => assert(allPossible)(isEmpty) + } + } + }, testM("isEmpty") { check(genList) { (as) => val actual = Traversable[List].isEmpty(as)