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

Add forany and foranyPar to Traversable #392 #424

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
6 changes: 6 additions & 0 deletions core/shared/src/main/scala/zio/prelude/Assertions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ trait Assertions {
def equalTo[A: Equal](expected: A): Assertion[A] =
Assertion.assertion("equalTo")(param(expected))(_ === expected)

/**
* Failing assertion, used for unexpected cases
*/
val unexpectedResult: Assertion[Any] =
Copy link
Contributor

Choose a reason for hiding this comment

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

There is an existing nothing assertion that you can use for this.

Assertion.assertion("unexpectedResult")()(_ => false)

/**
* Makes a new assertion that requires a validation failure satisfying a
* specified assertion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1200,14 +1200,6 @@ object AssociativeBoth extends LawfulF.Invariant[AssociativeBothDeriveEqualInvar
def both[A, B](fa: => Vector[A], fb: => Vector[B]): Vector[(A, B)] = fa.flatMap(a => fb.map(b => (a, b)))
}

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean to delete this?

Copy link
Member

@sideeffffect sideeffffect Nov 23, 2020

Choose a reason for hiding this comment

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

Yes, ZIOAssociativeBoth can be fully replaced by ZIOIdentityBoth. Or should we call it ZIOCommutativeIdentityBoth? But it needs to be in the AssociativeBoth companion object 😉
More detailed explanation here: https://meta.plasm.us/posts/2019/09/30/implicit-scope-and-cats/

* The `AssociativeBoth` 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] {
def both[A, B](fa: => ZIO[R, E, A], fb: => ZIO[R, E, B]): ZIO[R, E, (A, B)] = fa zip fb
}

/**
* The `AssociativeBoth` instance for failed `ZIO`.
*/
Expand Down
9 changes: 9 additions & 0 deletions core/shared/src/main/scala/zio/prelude/IdentityBoth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ object IdentityBoth extends LawfulF.Invariant[DeriveEqualIdentityBothInvariant,
def apply[F[_]](implicit identityBoth: IdentityBoth[F]): IdentityBoth[F] =
identityBoth

/**
* The `IdentityBoth` instance for `ZIO`.
*/
implicit def ZIOIdentityBoth[R, E]: IdentityBoth[({ type lambda[+a] = ZIO[R, E, a] })#lambda] =
Copy link
Member

Choose a reason for hiding this comment

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

Actually, could you do it the other way around? 🙏
The proper way is to have ZIOIdentityBoth in AssociativeBoth's companion object.

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
}

def fromCovariantIdentityFlatten[F[+_]](implicit
covariant: Covariant[F],
identityFlatten: IdentityFlatten[F]
Expand Down
84 changes: 56 additions & 28 deletions core/shared/src/main/scala/zio/prelude/Traversable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,30 @@ trait Traversable[F[+_]] extends Covariant[F] {
}
}

/**
* Compute each element of the collection
* with specified effectual function `f`
* and then reduces the results using the
* `AssociativeEither[G].both` operation,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* `AssociativeEither[G].both` operation,
* `AssociativeEither[G].either` operation,

* returning `None` if the collection is empty.
*/
def forany[G[+_]: AssociativeEither: Covariant, A, B](fa: F[A])(f: A => G[B]): Option[G[B]] = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this collectFirst? It applies the function f to each element in the structure and returns the first result that is a "success", where the meaning of success and failure is defined by the AssociativeEither instance?

Copy link
Member

@sideeffffect sideeffffect Nov 23, 2020

Choose a reason for hiding this comment

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

Yes, that's what I was going for! Thank you @adamgfraser , I didn't know it could have this name 👍
https://superruzafa.github.io/visual-scala-reference/collectFirst/ 👈 TIL
Although I would suspect we might need IdentityEither for this.

I was pushed to make a proposal for it, when I was looking at the signature for foreach and thought there should be a dual for it -- hence the name forany.

Copy link
Contributor

Choose a reason for hiding this comment

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

@sideeffffect Wow! That Visual Scala resource is really cool!

I think we can get away with just having an AssociativeEither instance because None can serve as the default value if the structure is empty or if there is no successful value. However, I think we need to flip the order of the result types here so we return a G[Option[B]]. Otherwise we end up returning the last "failure" if the structure is non-empty and all of the results are failures.

Copy link
Member

Choose a reason for hiding this comment

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

Yes. Good point! But in that case, we would probably need IdentityBoth, to be able to create a G of None in case of an empty Traversable, right?
I've updated the ticket description #392
Please let me know, if you consider it accurate now 🙏

implicit val associative: Associative[G[B]] = Associative.make[G[B]](_ orElse _)
reduceMapOption(fa)(f)
}

/**
* Compute each element of the collection
* with specified effectual function `f`
* and then reduces the results using the
* `CommutativeEither[G].both` operation,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* `CommutativeEither[G].both` operation,
* `CommutativeEither[G].either` operation,

* returning `None` if the collection is empty.
*/
def foranyPar[G[+_]: CommutativeEither: Covariant, A, B](fa: F[A])(f: A => G[B]): Option[G[B]] = {
Copy link
Contributor

Choose a reason for hiding this comment

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

collectFirstPar?

implicit val associative: Associative[G[B]] = Associative.make[G[B]](_ orElsePar _)
reduceMapOption(fa)(f)
}

/**
* Returns whether the collection is empty.
*/
Expand Down Expand Up @@ -288,61 +312,65 @@ trait TraversableSyntax {
* Provides infix syntax for traversing collections.
*/
implicit class TraversableOps[F[+_], A](private val self: F[A]) {
def foreach[G[+_]: IdentityBoth: Covariant, B](f: A => G[B])(implicit F: Traversable[F]): G[F[B]] =
def foreach[G[+_]: IdentityBoth: Covariant, B](f: A => G[B])(implicit F: Traversable[F]): G[F[B]] =
F.foreach(self)(f)
def contains[A1 >: A](a: A1)(implicit A: Equal[A1], F: Traversable[F]): Boolean =
def contains[A1 >: A](a: A1)(implicit A: Equal[A1], F: Traversable[F]): Boolean =
F.contains[A, A1](self)(a)
def count(f: A => Boolean)(implicit F: Traversable[F]): Int =
def count(f: A => Boolean)(implicit F: Traversable[F]): Int =
F.count(self)(f)
def exists(f: A => Boolean)(implicit F: Traversable[F]): Boolean =
def exists(f: A => Boolean)(implicit F: Traversable[F]): Boolean =
F.exists(self)(f)
def find(f: A => Boolean)(implicit F: Traversable[F]): Option[A] =
def find(f: A => Boolean)(implicit F: Traversable[F]): Option[A] =
F.find(self)(f)
def foldLeft[S](s: S)(f: (S, A) => S)(implicit F: Traversable[F]): S =
def foldLeft[S](s: S)(f: (S, A) => S)(implicit F: Traversable[F]): S =
F.foldLeft(self)(s)(f)
def foldMap[B: Identity](f: A => B)(implicit F: Traversable[F]): B =
def foldMap[B: Identity](f: A => B)(implicit F: Traversable[F]): B =
F.foldMap(self)(f)
def foldRight[S](s: S)(f: (A, S) => S)(implicit F: Traversable[F]): S =
def foldRight[S](s: S)(f: (A, S) => S)(implicit F: Traversable[F]): S =
F.foldRight(self)(s)(f)
def forall(f: A => Boolean)(implicit F: Traversable[F]): Boolean =
def forall(f: A => Boolean)(implicit F: Traversable[F]): Boolean =
F.forall(self)(f)
def foreach_[G[+_]: IdentityBoth: Covariant](f: A => G[Any])(implicit F: Traversable[F]): G[Unit] =
def forany[G[+_]: AssociativeEither: Covariant, B](f: A => G[B])(implicit F: Traversable[F]): Option[G[B]] =
F.forany(self)(f)
def foranyPar[G[+_]: CommutativeEither: Covariant, B](f: A => G[B])(implicit F: Traversable[F]): Option[G[B]] =
F.foranyPar(self)(f)
def foreach_[G[+_]: IdentityBoth: Covariant](f: A => G[Any])(implicit F: Traversable[F]): G[Unit] =
F.foreach_(self)(f)
def isEmpty(implicit F: Traversable[F]): Boolean =
def isEmpty(implicit F: Traversable[F]): Boolean =
F.isEmpty(self)
def mapAccum[S, B](s: S)(f: (S, A) => (S, B))(implicit F: Traversable[F]): (S, F[B]) =
def mapAccum[S, B](s: S)(f: (S, A) => (S, B))(implicit F: Traversable[F]): (S, F[B]) =
F.mapAccum(self)(s)(f)
def maxOption(implicit A: Ord[A], F: Traversable[F]): Option[A] =
def maxOption(implicit A: Ord[A], F: Traversable[F]): Option[A] =
F.maxOption(self)
def maxByOption[B: Ord](f: A => B)(implicit F: Traversable[F]): Option[A] =
def maxByOption[B: Ord](f: A => B)(implicit F: Traversable[F]): Option[A] =
F.maxByOption(self)(f)
def minOption(implicit A: Ord[A], F: Traversable[F]): Option[A] =
def minOption(implicit A: Ord[A], F: Traversable[F]): Option[A] =
F.minOption(self)
def minByOption[B: Ord](f: A => B)(implicit F: Traversable[F]): Option[A] =
def minByOption[B: Ord](f: A => B)(implicit F: Traversable[F]): Option[A] =
F.minByOption(self)(f)
def nonEmpty(implicit F: Traversable[F]): Boolean =
def nonEmpty(implicit F: Traversable[F]): Boolean =
F.nonEmpty(self)
def reduceAssociative(implicit F: Traversable[F], A: Associative[A]): Option[A] =
def reduceAssociative(implicit F: Traversable[F], A: Associative[A]): Option[A] =
F.reduceAssociative(self)
def reduceIdempotent(implicit F: Traversable[F], ia: Idempotent[A], ea: Equal[A]): Option[A] =
def reduceIdempotent(implicit F: Traversable[F], ia: Idempotent[A], ea: Equal[A]): Option[A] =
F.reduceIdempotent(self)
def reduceIdentity(implicit F: Traversable[F], A: Identity[A]): A =
def reduceIdentity(implicit F: Traversable[F], A: Identity[A]): A =
F.reduceIdentity(self)
def product(implicit A: Identity[Prod[A]], F: Traversable[F]): A =
def product(implicit A: Identity[Prod[A]], F: Traversable[F]): A =
F.product(self)
def reduceMapOption[B: Associative](f: A => B)(implicit F: Traversable[F]): Option[B] =
def reduceMapOption[B: Associative](f: A => B)(implicit F: Traversable[F]): Option[B] =
F.reduceMapOption(self)(f)
def reduceOption(f: (A, A) => A)(implicit F: Traversable[F]): Option[A] =
def reduceOption(f: (A, A) => A)(implicit F: Traversable[F]): Option[A] =
F.reduceOption(self)(f)
def reverse(implicit F: Traversable[F]): F[A] =
def reverse(implicit F: Traversable[F]): F[A] =
F.reverse(self)
def size(implicit F: Traversable[F]): Int =
def size(implicit F: Traversable[F]): Int =
F.size(self)
def sum(implicit A: Identity[Sum[A]], F: Traversable[F]): A =
def sum(implicit A: Identity[Sum[A]], F: Traversable[F]): A =
F.sum(self)
def toChunk(implicit F: Traversable[F]): Chunk[A] =
def toChunk(implicit F: Traversable[F]): Chunk[A] =
F.toChunk(self)
def zipWithIndex(implicit F: Traversable[F]): F[(A, Int)] =
def zipWithIndex(implicit F: Traversable[F]): F[(A, Int)] =
F.zipWithIndex(self)
}

Expand Down
59 changes: 58 additions & 1 deletion core/shared/src/test/scala/zio/prelude/TraversableSpec.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package zio.prelude

import scala.collection.Set

import zio.random.Random
import zio.test.Assertion._
import zio.test._
import zio.test.laws._
import zio.{ Chunk, NonEmptyChunk }
import zio.{ Chunk, IO, NonEmptyChunk }

object TraversableSpec extends DefaultRunnableSpec {

Expand All @@ -25,6 +28,14 @@ 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)

Expand Down Expand Up @@ -115,6 +126,52 @@ 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]") {
checkM(genList, genOptIntFunction) { case (as, f) =>
for {
actual <- (as forany f.map {
case Some(x) => IO.succeedNow(x)
case None => IO.fail(())
}(Invariant.Function1Covariant)).flip.option
expected: Option[Option[Int]] = {
val allResults = as.map(f)
if (allResults.nonEmpty && allResults.forall(_.isEmpty))
None
else
Some(allResults.flatten.headOption)
}
} yield assert(actual)(equalTo(expected))
}
},
testM("foranyPar[IO]") {
checkM(genList, genOptIntFunction) { case (as, f) =>
for {
actual <- (as foranyPar f.map {
case Some(x) => IO.succeedNow(x)
case None => IO.fail(())
}(Invariant.Function1Covariant)).flip.option
expected: Option[Set[Int]] = {
val allResults = as.map(f)
if (allResults.nonEmpty && allResults.forall(_.isEmpty))
None
else
Some(allResults.flatten.toSet)
}
} yield (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)
case _ @result => assert(result)(unexpectedResult)
}
}
},
testM("isEmpty") {
check(genList) { (as) =>
val actual = Traversable[List].isEmpty(as)
Expand Down