From 5c09ab7f3a870d6ae7f486f8a65698ea73d7b4b2 Mon Sep 17 00:00:00 2001 From: Kai <450507+neko-kai@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:01:35 +0100 Subject: [PATCH 1/3] BIO: Add multiple collection operators, port error-accumulating collection operators from IzEither, add `IO2#suspendSafe` Short-circuiting operators: `foldLeft`, `flatTraverse`, `flatSequence`, `collect`, `find`, `collectFirst`, `filter` Error-accumulating operators: `partition`, `traverseAccumErrors/_`, `sequenceAccumErrors/_`, `flatTraverseAccumErrors`, `flatSequenceAccumErrors`, `sequenceAccumErrorsNEList` --- build.sbt | 3 +- .../izumi/functional/bio/Applicative3.scala | 4 + .../scala/izumi/functional/bio/Error3.scala | 10 +- .../bio/ErrorAccumulatingOps3.scala | 147 ++++++++++++++++++ .../main/scala/izumi/functional/bio/IO3.scala | 51 +++++- .../scala/izumi/functional/bio/Monad3.scala | 34 ++++ .../izumi/functional/bio/impl/AsyncZio.scala | 21 +++ .../izumi/functional/bio/impl/BioEither.scala | 127 +++++++++++++-- .../bio/ErrorAccumulatingOpsTest.scala | 115 ++++++++++++++ .../scala/izumi/functional/IzEither.scala | 13 +- .../scala/izumi/functional/IzEitherTest.scala | 2 +- project/Deps.sc | 1 + 12 files changed, 507 insertions(+), 21 deletions(-) create mode 100644 fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/ErrorAccumulatingOps3.scala create mode 100644 fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala diff --git a/build.sbt b/build.sbt index 9377fbdba5..64c91cdf1a 100644 --- a/build.sbt +++ b/build.sbt @@ -1347,7 +1347,8 @@ lazy val `fundamentals-reflection` = project.in(file("fundamentals/fundamentals- lazy val `fundamentals-bio` = project.in(file("fundamentals/fundamentals-bio")) .dependsOn( `fundamentals-language` % "test->compile;compile->compile", - `fundamentals-orphans` % "test->compile;compile->compile" + `fundamentals-orphans` % "test->compile;compile->compile", + `fundamentals-collections` % "test->compile;compile->compile" ) .settings( libraryDependencies ++= Seq( diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Applicative3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Applicative3.scala index bdc8f3ba35..f921961d4a 100644 --- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Applicative3.scala +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Applicative3.scala @@ -24,6 +24,10 @@ trait Applicative3[F[-_, +_, +_]] extends Functor3[F] { def traverse_[R, E, A](l: Iterable[A])(f: A => F[R, E, Unit]): F[R, E, Unit] = void(traverse(l)(f)) def sequence[R, E, A](l: Iterable[F[R, E, A]]): F[R, E, List[A]] = traverse(l)(identity) def sequence_[R, E](l: Iterable[F[R, E, Unit]]): F[R, E, Unit] = void(traverse(l)(identity)) + def flatTraverse[R, E, A, B](l: Iterable[A])(f: A => F[R, E, Iterable[B]]): F[R, E, List[B]] = map(traverse(l)(f))(_.flatten) + def flatSequence[R, E, A](l: Iterable[F[R, E, Iterable[A]]]): F[R, E, List[A]] = flatTraverse(l)(identity) + def collect[R, E, A, B](l: Iterable[A])(f: A => F[R, E, Option[B]]): F[R, E, List[B]] = map(traverse(l)(f))(_.flatten) + def filter[R, E, A](l: Iterable[A])(f: A => F[R, E, Boolean]): F[R, E, List[A]] = collect(l)(a => map(f(a))(if (_) Some(a) else None)) def unit: F[Any, Nothing, Unit] = pure(()) @inline final def traverse[R, E, A, B](o: Option[A])(f: A => F[R, E, B]): F[R, E, Option[B]] = o match { diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Error3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Error3.scala index c02706c485..21a503fff3 100644 --- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Error3.scala +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Error3.scala @@ -2,7 +2,9 @@ package izumi.functional.bio import izumi.fundamentals.platform.language.SourceFilePositionMaterializer -trait Error3[F[-_, +_, +_]] extends ApplicativeError3[F] with Monad3[F] { +import scala.annotation.nowarn + +trait Error3[F[-_, +_, +_]] extends ApplicativeError3[F] with Monad3[F] with ErrorAccumulatingOps3[F] { def catchAll[R, E, A, E2](r: F[R, E, A])(f: E => F[R, E2, A]): F[R, E2, A] @@ -56,6 +58,12 @@ trait Error3[F[-_, +_, +_]] extends ApplicativeError3[F] with Monad3[F] { catchAll(r: F[R1, E, A])(e => flatMap(f(e))(if (_) fail(e) else retryUntilF(r)(f))) } + @nowarn("msg=Unused import") + def partition[R, E, A](l: Iterable[F[R, E, A]]): F[R, Nothing, (List[E], List[A])] = { + import scala.collection.compat.* + map(traverse(l)(attempt[R, E, A]))(_.partitionMap(identity)) + } + /** for-comprehensions sugar: * * {{{ diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/ErrorAccumulatingOps3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/ErrorAccumulatingOps3.scala new file mode 100644 index 0000000000..694b12294b --- /dev/null +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/ErrorAccumulatingOps3.scala @@ -0,0 +1,147 @@ +package izumi.functional.bio + +import izumi.fundamentals.collections.nonempty.NEList + +import scala.collection.compat.* +import scala.collection.compat.immutable.LazyList +import scala.collection.compat.immutable.LazyList.#:: +import scala.collection.immutable.Queue + +trait ErrorAccumulatingOps3[F[-_, +_, +_]] { this: Error3[F] => + + /** `traverse` with error accumulation */ + def traverseAccumErrors[ColR[x] <: IterableOnce[x], ColL[_], R, E, A, B]( + col: ColR[A] + )(f: A => F[R, ColL[E], B] + )(implicit + buildR: Factory[B, ColR[B]], + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): F[R, ColL[E], ColR[B]] = { + accumulateErrorsImpl(col)( + effect = f, + onLeft = (l: ColL[E]) => iterL(l), + init = Queue.empty[B], + onRight = (acc: Queue[B], v: B) => acc :+ v, + end = (acc: Queue[B]) => acc.to(buildR), + ) + } + + /** `traverse_` with error accumulation */ + def traverseAccumErrors_[ColR[x] <: IterableOnce[x], ColL[_], R, E, A]( + col: ColR[A] + )(f: A => F[R, ColL[E], Unit] + )(implicit + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): F[R, ColL[E], Unit] = { + accumulateErrorsImpl(col)( + effect = f, + onLeft = (l: ColL[E]) => iterL(l), + init = (), + onRight = (acc: Unit, _: Unit) => acc, + end = (acc: Unit) => acc, + ) + } + + /** `sequence` with error accumulation */ + def sequenceAccumErrors[ColR[x] <: IterableOnce[x], ColL[_], R, E, A]( + col: ColR[F[R, ColL[E], A]] + )(implicit + buildR: Factory[A, ColR[A]], + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): F[R, ColL[E], ColR[A]] = { + traverseAccumErrors(col)(identity) + } + + /** `sequence_` with error accumulation */ + def sequenceAccumErrors_[ColR[x] <: IterableOnce[x], ColL[_], R, E, A]( + col: ColR[F[R, ColL[E], A]] + )(implicit + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): F[R, ColL[E], Unit] = { + traverseAccumErrors_(col)(void(_)) + } + + /** `sequence` with error accumulation */ + def sequenceAccumErrorsNEList[ColR[x] <: IterableOnce[x], R, E, A]( + col: ColR[F[R, E, A]] + )(implicit buildR: Factory[A, ColR[A]] + ): F[R, NEList[E], ColR[A]] = { + accumulateErrorsImpl(col)( + effect = identity, + onLeft = (e: E) => Seq(e), + init = Queue.empty[A], + onRight = (ac: Queue[A], a: A) => ac :+ a, + end = (ac: Queue[A]) => ac.to(buildR), + ) + } + + /** `flatTraverse` with error accumulation */ + def flatTraverseAccumErrors[ColR[x] <: IterableOnce[x], ColIn[x] <: IterableOnce[x], ColL[_], R, E, A, B]( + col: ColR[A] + )(f: A => F[R, ColL[E], ColIn[B]] + )(implicit + buildR: Factory[B, ColR[B]], + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): F[R, ColL[E], ColR[B]] = { + accumulateErrorsImpl(col)( + effect = f, + onLeft = (l: ColL[E]) => iterL(l), + init = Queue.empty[B], + onRight = (acc: Queue[B], v: IterableOnce[B]) => acc ++ v, + end = (acc: Queue[B]) => acc.to(buildR), + ) + } + + /** `flatSequence` with error accumulation */ + def flatSequenceAccumErrors[ColR[x] <: IterableOnce[x], ColIn[x] <: IterableOnce[x], ColL[_], R, E, A]( + col: ColR[F[R, ColL[E], ColIn[A]]] + )(implicit + buildR: Factory[A, ColR[A]], + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): F[R, ColL[E], ColR[A]] = { + flatTraverseAccumErrors(col)(identity) + } + + protected[this] def accumulateErrorsImpl[ColL[_], ColR[x] <: IterableOnce[x], R, E, E1, A, B, B1, AC]( + col: ColR[A] + )(effect: A => F[R, E, B], + onLeft: E => IterableOnce[E1], + init: AC, + onRight: (AC, B) => AC, + end: AC => B1, + )(implicit buildL: Factory[E1, ColL[E1]] + ): F[R, ColL[E1], B1] = { + def go( + bad: Queue[E1], + good: AC, + lazyList: LazyList[A], + allGood: Boolean, + ): F[R, ColL[E1], B1] = { + lazyList match { + case h #:: tail => + redeem(effect(h))( + e => go(bad ++ onLeft(e), good, tail, allGood = false), + v => { + val newGood = onRight(good, v) + go(bad, newGood, tail, allGood) + }, + ) + case _ => + if (allGood) { + pure(end(good)) + } else { + fail(bad.to(buildL)) + } + } + } + + go(Queue.empty[E1], init, col.iterator.to(LazyList), allGood = true) + } + +} diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/IO3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/IO3.scala index e0c2c8021b..b13153b5d0 100644 --- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/IO3.scala +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/IO3.scala @@ -1,5 +1,6 @@ package izumi.functional.bio +import scala.collection.compat.* import scala.util.Try trait IO3[F[-_, +_, +_]] extends Panic3[F] { @@ -22,7 +23,7 @@ trait IO3[F[-_, +_, +_]] extends Panic3[F] { * {{{ * import izumi.functional.bio.F * - * val referentiallyTransparentArrayAllocation: F[Nothing, Array[Byte]] = { + * val exceptionSafeArrayAllocation: F[Nothing, Array[Byte]] = { * F.sync(new Array(256)) * } * }}} @@ -34,8 +35,12 @@ trait IO3[F[-_, +_, +_]] extends Panic3[F] { */ def sync[A](effect: => A): F[Any, Nothing, A] + /** Capture a side-effectful block of code that can throw exceptions and returns another effect */ def suspend[R, A](effect: => F[R, Throwable, A]): F[R, Throwable, A] = flatten(syncThrowable(effect)) + /** Capture an _exception-safe_ side-effect that returns another effect */ + def suspendSafe[R, E, A](effect: => F[R, E, A]): F[R, E, A] = flatten(sync(effect)) + @inline final def apply[A](effect: => A): F[Any, Throwable, A] = syncThrowable(effect) // defaults @@ -49,4 +54,48 @@ trait IO3[F[-_, +_, +_]] extends Panic3[F] { override def fromTry[A](effect: => Try[A]): F[Any, Throwable, A] = { syncThrowable(effect.get) } + + override protected[this] def accumulateErrorsImpl[ColL[_], ColR[x] <: IterableOnce[x], R, E, E1, A, B, B1, AC]( + col: ColR[A] + )(effect: A => F[R, E, B], + onLeft: E => IterableOnce[E1], + init: AC, + onRight: (AC, B) => AC, + end: AC => B1, + )(implicit buildL: Factory[E1, ColL[E1]] + ): F[R, ColL[E1], B1] = { + suspendSafe { + val bad = buildL.newBuilder + + val iterator = col.iterator + var good = init + var allGood = true + + def go(): F[R, ColL[E1], B1] = { + suspendSafe(if (iterator.hasNext) { + redeem(effect(iterator.next()))( + e => + suspendSafe { + allGood = false + bad ++= onLeft(e) + go() + }, + v => + suspendSafe { + good = onRight(good, v) + go() + }, + ) + } else { + if (allGood) { + pure(end(good)) + } else { + fail(bad.result()) + } + }) + } + + go() + } + } } diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Monad3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Monad3.scala index e23b707594..07769a83d5 100644 --- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Monad3.scala +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/Monad3.scala @@ -40,6 +40,40 @@ trait Monad3[F[-_, +_, +_]] extends Applicative3[F] { } } + def foldLeft[R, E, A, AC](l: Iterable[A])(z: AC)(f: (AC, A) => F[R, E, AC]): F[R, E, AC] = { + def go(l: List[A], ac: AC): F[R, E, AC] = { + l match { + case head :: tail => flatMap(f(ac, head))(go(tail, _)) + case Nil => pure(ac) + } + } + go(l.toList, z) + } + + def find[R, E, A](l: Iterable[A])(f: A => F[R, E, Boolean]): F[R, E, Option[A]] = { + def go(l: List[A]): F[R, E, Option[A]] = { + l match { + case head :: tail => flatMap(f(head))(if (_) pure(Some(head)) else go(tail)) + case Nil => pure(None) + } + } + go(l.toList) + } + + def collectFirst[R, E, A, B](l: Iterable[A])(f: A => F[R, E, Option[B]]): F[R, E, Option[B]] = { + def go(l: List[A]): F[R, E, Option[B]] = { + l match { + case head :: tail => + flatMap(f(head)) { + case res @ Some(_) => pure(res) + case None => go(tail) + } + case Nil => pure(None) + } + } + go(l.toList) + } + /** * Execute an action repeatedly until its result fails to satisfy the given predicate * and return that result, discarding all others. diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/AsyncZio.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/AsyncZio.scala index c040f0503b..2c75a9bafe 100644 --- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/AsyncZio.scala +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/AsyncZio.scala @@ -39,6 +39,12 @@ open class AsyncZio extends Async3[ZIO] /*with Local3[ZIO]*/ { ZIO.suspend(effect) } + @inline override final def suspendSafe[R, E, A](effect: => ZIO[R, E, A]): ZIO[R, E, A] = { + val byName: () => ZIO[R, E, A] = () => effect + implicit val trace: zio.Trace = InteropTracer.newTrace(byName) + + ZIO.suspendSucceed(effect) + } @inline override final def fail[E](v: => E): ZIO[Any, E, Nothing] = { val byName: () => E = () => v @@ -184,6 +190,21 @@ open class AsyncZio extends Async3[ZIO] /*with Local3[ZIO]*/ { @inline override final def sequence[R, E, A](l: Iterable[ZIO[R, E, A]]): ZIO[R, E, List[A]] = ZIO.collectAll(l.toList)(implicitly, Tracer.instance.empty) @inline override final def traverse_[R, E, A](l: Iterable[A])(f: A => ZIO[R, E, Unit]): ZIO[R, E, Unit] = ZIO.foreachDiscard(l)(f)(InteropTracer.newTrace(f)) @inline override final def sequence_[R, E](l: Iterable[ZIO[R, E, Unit]]): ZIO[R, E, Unit] = ZIO.foreachDiscard(l)(identity)(Tracer.instance.empty) + @inline override final def filter[R, E, A](l: Iterable[A])(f: A => ZIO[R, E, Boolean]): ZIO[R, E, List[A]] = ZIO.filter(l.toList)(f)(implicitly, InteropTracer.newTrace(f)) + + @inline override final def foldLeft[R, E, A, AC](l: Iterable[A])(z: AC)(f: (AC, A) => ZIO[R, E, AC]): ZIO[R, E, AC] = { + ZIO.foldLeft(l)(z)(f)(InteropTracer.newTrace(f)) + } + + @inline override final def find[R, E, A](l: Iterable[A])(f: A => ZIO[R, E, Boolean]): ZIO[R, E, Option[A]] = { + val trace = InteropTracer.newTrace(f) + + ZIO.collectFirst(l)(a => f(a).map(if (_) Some(a) else None)(trace))(trace) + } + + @inline override final def collectFirst[R, E, A, B](l: Iterable[A])(f: A => ZIO[R, E, Option[B]]): ZIO[R, E, Option[B]] = { + ZIO.collectFirst(l)(f)(InteropTracer.newTrace(f)) + } @inline override final def sandbox[R, E, A](r: ZIO[R, E, A]): ZIO[R, Exit.Failure[E], A] = { implicit val trace: zio.Trace = Tracer.instance.empty diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BioEither.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BioEither.scala index 93c4758e38..33d8822df7 100644 --- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BioEither.scala +++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BioEither.scala @@ -2,30 +2,137 @@ package izumi.functional.bio.impl import izumi.functional.bio.Error2 +import scala.collection.compat.* import scala.util.Try object BioEither extends BioEither open class BioEither extends Error2[Either] { - @inline override def pure[A](a: A): Either[Nothing, A] = Right(a) - @inline override def map[R, E, A, B](r: Either[E, A])(f: A => B): Either[E, B] = r.map(f) + @inline override final def pure[A](a: A): Either[Nothing, A] = Right(a) + @inline override final def map[R, E, A, B](r: Either[E, A])(f: A => B): Either[E, B] = r.map(f) /** execute two operations in order, map their results */ - @inline override def map2[R, E, A, B, C](firstOp: Either[E, A], secondOp: => Either[E, B])(f: (A, B) => C): Either[E, C] = { + @inline override final def map2[R, E, A, B, C](firstOp: Either[E, A], secondOp: => Either[E, B])(f: (A, B) => C): Either[E, C] = { firstOp.flatMap(a => secondOp.map(b => f(a, b))) } - @inline override def flatMap[R, E, A, B](r: Either[E, A])(f: A => Either[E, B]): Either[E, B] = r.flatMap(f) + @inline override final def flatMap[R, E, A, B](r: Either[E, A])(f: A => Either[E, B]): Either[E, B] = r.flatMap(f) - @inline override def catchAll[R, E, A, E2](r: Either[E, A])(f: E => Either[E2, A]): Either[E2, A] = r.left.flatMap(f) - @inline override def fail[E](v: => E): Either[E, Nothing] = Left(v) + @inline override final def catchAll[R, E, A, E2](r: Either[E, A])(f: E => Either[E2, A]): Either[E2, A] = r.left.flatMap(f) + @inline override final def fail[E](v: => E): Either[E, Nothing] = Left(v) - @inline override def fromEither[E, V](effect: => Either[E, V]): Either[E, V] = effect - @inline override def fromOption[E, A](errorOnNone: => E)(effect: => Option[A]): Either[E, A] = effect match { + @inline override final def fromEither[E, V](effect: => Either[E, V]): Either[E, V] = effect + @inline override final def fromOption[E, A](errorOnNone: => E)(effect: => Option[A]): Either[E, A] = effect match { case Some(value) => Right(value) case None => Left(errorOnNone) } - @inline override def fromTry[A](effect: => Try[A]): Either[Throwable, A] = effect.toEither + @inline override final def fromTry[A](effect: => Try[A]): Either[Throwable, A] = effect.toEither + + @inline override final def guarantee[R, E, A](f: Either[E, A], cleanup: Either[Nothing, Unit]): Either[E, A] = f + + override def traverse[R, E, A, B](l: Iterable[A])(f: A => Either[E, B]): Either[E, List[B]] = { + val b = List.newBuilder[B] + val i = l.iterator + + while (i.hasNext) { + f(i.next()) match { + case Left(error) => + return Left(error) + case Right(v) => + b += v + } + } + Right(b.result()) + } + + override def foldLeft[R, E, A, AC](col: Iterable[A])(z: AC)(op: (AC, A) => Either[E, AC]): Either[E, AC] = { + val i = col.iterator + var acc: Either[E, AC] = Right(z) + + while (i.hasNext && acc.isRight) { + val nxt = i.next() + (acc, nxt) match { + case (Right(a), n) => + acc = op(a, n) + case _ => + } + } + acc + } + + override def find[R, E, A](l: Iterable[A])(f: A => Either[E, Boolean]): Either[E, Option[A]] = { + val i = l.iterator + + while (i.hasNext) { + val a = i.next() + f(a) match { + case Left(value) => + return Left(value) + case Right(true) => + return Right(Some(a)) + case Right(_) => + } + } + Right(None) + } + + override def collectFirst[R, E, A, B](l: Iterable[A])(f: A => Either[E, Option[B]]): Either[E, Option[B]] = { + val i = l.iterator + + while (i.hasNext) { + val a = i.next() + f(a) match { + case Left(value) => + return Left(value) + case Right(res @ Some(_)) => + return Right(res) + case Right(_) => + } + } + Right(None) + } + + override def partition[R, E, A](l: Iterable[Either[E, A]]): Right[Nothing, (List[E], List[A])] = { + val bad = List.newBuilder[E] + val good = List.newBuilder[A] + + l.iterator.foreach { + case Left(e) => bad += e + case Right(v) => good += v + } + + Right((bad.result(), good.result())) + } + + override protected[this] def accumulateErrorsImpl[ColL[_], ColR[x] <: IterableOnce[x], R, E, E1, A, B, B1, AC]( + col: ColR[A] + )(effect: A => Either[E, B], + onLeft: E => IterableOnce[E1], + init: AC, + onRight: (AC, B) => AC, + end: AC => B1, + )(implicit buildL: Factory[E1, ColL[E1]] + ): Either[ColL[E1], B1] = { + val bad = buildL.newBuilder + + val iterator = col.iterator + var good = init + var allGood = true + while (iterator.hasNext) { + effect(iterator.next()) match { + case Left(e) => + allGood = false + bad ++= onLeft(e) + case Right(v) => + good = onRight(good, v) + } + } + + if (allGood) { + Right(end(good)) + } else { + Left(bad.result()) + } + } - @inline override def guarantee[R, E, A](f: Either[E, A], cleanup: Either[Nothing, Unit]): Either[E, A] = f } diff --git a/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala b/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala new file mode 100644 index 0000000000..9747692445 --- /dev/null +++ b/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala @@ -0,0 +1,115 @@ +package izumi.functional.bio + +import izumi.fundamentals.collections.nonempty.{NEList, NESet} +import org.scalatest.wordspec.AnyWordSpec + +import scala.annotation.nowarn + +@nowarn("msg=Unused import") +class ErrorAccumulatingOpsTest extends AnyWordSpec { + import scala.collection.compat.* + + type BuilderFail + type IzType + type Result[+T] = Either[List[BuilderFail], T] + type TList = Result[List[IzType]] + + def listTList: List[TList] = Nil + def x(t: TList): Result[Unit] = Right { val _ = t } + + def F: Error2[Either] = Root.BIOEither + + /** `flatSequence` with error accumulation */ + def flatSequenceAccumErrors[ColR[x] <: IterableOnce[x], ColL[_], E, A]( + col: ColR[Either[ColL[E], IterableOnce[A]]] + )(implicit + buildR: Factory[A, ColR[A]], + buildL: Factory[E, ColL[E]], + iterL: ColL[E] => IterableOnce[E], + ): Either[ColL[E], ColR[A]] = { + F.flatSequenceAccumErrors(col) + } + + "ErrorAccumulatingOps" should { + + "have biFlatAggregate callable with typealiases" in { + + def test: Either[List[BuilderFail], Unit] = { + val ret = F.flatSequenceAccumErrors /*[List, List, Any, BuilderFail, IzType]*/ (listTList: List[Either[List[BuilderFail], List[IzType]]]) + x(ret) + } + + assert(test.isRight) + } + + "support NonEmptyCollections" in { + val nel0 = List(Right(1), Right(2), Left(NEList("error"))) + + assert(implicitly[Factory[String, NEList[String]]] ne null) + assert(F.sequenceAccumErrors(nel0) == Left(NEList("error"))) + + val nes0 = List(Right(()), Left(NESet("error"))) + assert(F.sequenceAccumErrors(nes0) == Left(NESet("error"))) + + assert(F.traverseAccumErrors_(nes0) { + case Left(value) => Left(value + "error1") + case Right(value) => Right(value) + } == Left(NESet("error", "error1"))) + + val nel1 = List(Right(List(1)), Left(NEList("error"))) + assert(F.flatSequenceAccumErrors(nel1) == Left(NEList("error"))) + + assert(F.traverseAccumErrors(nel1) { + case Left(value) => Left(Set(value ++ NEList("a"))) + case Right(value) => Right(Set(value ++ List(1))) + } == Left(Set(NEList("error", "a")))) + + val nel2 = List(Right(1), Left(NEList("error"))) + assert(F.flatTraverseAccumErrors(nel2)(_.map(i => List(i))) == Left(NEList("error"))) + assert(F.flatTraverseAccumErrors(nel2)(_.map(i => List(i))).map(_.to(Set)) == Left(NEList("error"))) + } + + "support the happy path" in { + val l0: Seq[Either[List[String], Int]] = List(Right(1), Right(2), Right(3)) + assert((F.sequenceAccumErrors(l0): Either[List[String], Seq[Int]]) == Right(Seq(1, 2, 3))) + assert(F.sequenceAccumErrors_(l0) == Right(())) + assert(F.traverseAccumErrors(l0)(identity) == Right(Seq(1, 2, 3))) + assert(F.traverseAccumErrors(l0)(identity).map(_.to(List)) == Right(List(1, 2, 3))) + assert(F.traverseAccumErrors_(l0)(_.map(_ => ())) == Right(())) + + val l1: Seq[Either[List[String], Seq[Int]]] = List(Right(Seq(1)), Right(Seq(2)), Right(Seq(3))) + assert(F.flatTraverseAccumErrors(l1)(identity) == Right(Seq(1, 2, 3))) + assert(F.flatTraverseAccumErrors(l1)(identity).map(_.to(List)) == Right(List(1, 2, 3))) + + assert(F.flatSequenceAccumErrors(l1) == Right(List(1, 2, 3))) + + val l2: Seq[Either[String, Int]] = List(Right(1), Right(2), Right(3), Left("error")) + assert(F.sequenceAccumErrorsNEList(l2) == Left(NEList("error"))) + + val l3: Seq[Either[List[String], Int]] = List(Right(1), Right(2), Right(3), Left(List("error"))) + assert(F.sequenceAccumErrors(l3) == Left(List("error"))) + + assert(Right(Seq(1, 2, 3)).map(_.to(List)) == Right(List(1, 2, 3))) + } + + "support search" in { + assert(F.find(List(1, 2, 3))(i => Right(i == 2)) == Right(Some(2))) + assert(F.find(List(1, 2, 3))(i => Right(i == 4)) == Right(None)) + assert(F.find(List(1, 2, 3))(_ => Left("error")) == Left("error")) + } + + "support fold" in { + assert(F.foldLeft(List(1, 2, 3))("") { case (acc, v) => Right(s"$acc.$v") } == Right(".1.2.3")) + assert(F.foldLeft(List(1, 2, 3))("") { case (_, _) => Left("error") } == Left("error")) + } + + "support partitioning" in { + val lst = List(Right(1), Right(2), Left(3)) + val Right((l, r)) = F.partition(lst) + assert(l == List(3)) + assert(r == List(1, 2)) + } + + } + +} diff --git a/fundamentals/fundamentals-collections/src/main/scala/izumi/functional/IzEither.scala b/fundamentals/fundamentals-collections/src/main/scala/izumi/functional/IzEither.scala index 29f1eb81d4..8eed4bd3bc 100644 --- a/fundamentals/fundamentals-collections/src/main/scala/izumi/functional/IzEither.scala +++ b/fundamentals/fundamentals-collections/src/main/scala/izumi/functional/IzEither.scala @@ -85,10 +85,10 @@ object IzEither extends IzEither { } final class EitherBiAggregate[L, R, ColL[_], ColR[x] <: IterableOnce[x]](private val col: ColR[Either[ColL[L], R]]) extends AnyVal { - /** `sequence` with error accumulation */ @deprecated("use .biSequence") def biAggregate(implicit iterL: ColL[L] => IterableOnce[L], buildR: Factory[R, ColR[R]], buildL: Factory[L, ColL[L]]): Either[ColL[L], ColR[R]] = { biSequence } + /** `sequence` with error accumulation */ def biSequence(implicit iterL: ColL[L] => IterableOnce[L], buildR: Factory[R, ColR[R]], buildL: Factory[L, ColL[L]]): Either[ColL[L], ColR[R]] = { val good = buildR.newBuilder col.accumulateErrors(identity, (l: ColL[L]) => iterL(l), (v: R) => good += v, () => good.result()) @@ -105,16 +105,16 @@ object IzEither extends IzEither { } } - /** `sequence` with error accumulation */ final class EitherScalarOps[L, R, ColR[x] <: IterableOnce[x]](private val col: ColR[Either[L, R]]) extends AnyVal { @deprecated("use .biSequenceScalar") def biAggregateScalar(implicit buildR: Factory[R, ColR[R]]): Either[NEList[L], ColR[R]] = { biSequenceScalar } + /** `sequence` with error accumulation */ def biSequenceScalar(implicit buildR: Factory[R, ColR[R]]): Either[NEList[L], ColR[R]] = { val good = buildR.newBuilder - col.accumulateErrors(identity, (l: L) => Seq(l), (v: R) => good ++= Seq(v), () => good.result()) + col.accumulateErrors(identity, (l: L) => Seq(l), (v: R) => good += v, () => good.result()) } } @@ -175,8 +175,6 @@ object IzEither extends IzEither { } final class EitherBiFlatMapAggregate[ColR[x] <: IterableOnce[x], T](private val col: ColR[T]) extends AnyVal { - /** `flatTraverse` with error accumulation */ - @deprecated("use .biFlatTraverse") def biFlatMapAggregate[ColL[_], L, A]( f: T => Either[ColL[L], IterableOnce[A]] @@ -187,6 +185,7 @@ object IzEither extends IzEither { biFlatTraverse(f) } + /** `flatTraverse` with error accumulation */ def biFlatTraverse[ColL[_], L, A]( f: T => Either[ColL[L], IterableOnce[A]] )(implicit buildR: Factory[A, ColR[A]], @@ -211,11 +210,11 @@ object IzEither extends IzEither { final class EitherBiFlatAggregate[L, R, ColR[x] <: IterableOnce[x], ColIn[x] <: IterableOnce[x], ColL[_]](private val col: ColR[Either[ColL[L], ColIn[R]]]) extends AnyVal { - /** `flatSequence` with error accumulation */ @deprecated("use .biFlatten") def biFlatAggregate(implicit buildR: Factory[R, ColR[R]], buildL: Factory[L, ColL[L]], iterL: ColL[L] => IterableOnce[L]): Either[ColL[L], ColR[R]] = biFlatten + /** `flatSequence` with error accumulation */ def biFlatten(implicit buildR: Factory[R, ColR[R]], buildL: Factory[L, ColL[L]], iterL: ColL[L] => IterableOnce[L]): Either[ColL[L], ColR[R]] = { col.biFlatTraverse(identity) } @@ -238,7 +237,7 @@ object IzEither extends IzEither { Right(None) } - /** monadic `foldLeft` with error accumulation */ + /** monadic `foldLeft` with short-circuiting error */ def biFoldLeft[E, A](z: A)(op: (A, T) => Either[E, A]): Either[E, A] = { val i = col.iterator var acc: Either[E, A] = Right(z) diff --git a/fundamentals/fundamentals-collections/src/test/scala/izumi/functional/IzEitherTest.scala b/fundamentals/fundamentals-collections/src/test/scala/izumi/functional/IzEitherTest.scala index eb5015f918..1ffc951e37 100644 --- a/fundamentals/fundamentals-collections/src/test/scala/izumi/functional/IzEitherTest.scala +++ b/fundamentals/fundamentals-collections/src/test/scala/izumi/functional/IzEitherTest.scala @@ -32,7 +32,7 @@ class IzEitherTest extends AnyWordSpec { "support NonEmptyCollections" in { val nel0 = List(Right(1), Right(2), Left(NEList("error"))) - implicitly[Factory[String, NEList[String]]] + assert(implicitly[Factory[String, NEList[String]]] ne null) assert(nel0.biSequence == Left(NEList("error"))) val nes0 = List(Right(()), Left(NESet("error"))) diff --git a/project/Deps.sc b/project/Deps.sc index 66ac630705..40b4d75c2b 100644 --- a/project/Deps.sc +++ b/project/Deps.sc @@ -503,6 +503,7 @@ object Izumi { depends = Seq( Projects.fundamentals.language, Projects.fundamentals.orphans, + Projects.fundamentals.collections, ), ), ), From 1e06c6a873b36de8f4002692c5949f4de191db12 Mon Sep 17 00:00:00 2001 From: Kai <450507+neko-kai@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:04:35 +0100 Subject: [PATCH 2/3] Test error accumulation with ZIO --- .../bio/ErrorAccumulatingOpsTest.scala | 134 ++++++++++-------- 1 file changed, 72 insertions(+), 62 deletions(-) diff --git a/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala b/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala index 9747692445..2fe90a90d7 100644 --- a/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala +++ b/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala @@ -3,109 +3,119 @@ package izumi.functional.bio import izumi.fundamentals.collections.nonempty.{NEList, NESet} import org.scalatest.wordspec.AnyWordSpec -import scala.annotation.nowarn +import scala.annotation.{nowarn, unused} + +final class ErrorAccumulatingOpsTestEither extends ErrorAccumulatingOpsTest[Either] { + override implicit def F: Error2[Either] = Root.BIOEither + override def unsafeRun[E, A](f: Either[E, A]): Either[E, A] = f +} + +final class ErrorAccumulatingOpsTestZIO extends ErrorAccumulatingOpsTest[zio.IO] { + private val runner: UnsafeRun2[zio.IO] = UnsafeRun2.createZIO() + + override implicit def F: Error2[zio.IO] = Root.Convert3To2(Root.BIOZIO) + override def unsafeRun[E, A](f: zio.IO[E, A]): Either[E, A] = runner.unsafeRun(f.attempt) +} @nowarn("msg=Unused import") -class ErrorAccumulatingOpsTest extends AnyWordSpec { +abstract class ErrorAccumulatingOpsTest[F[+_, +_]] extends AnyWordSpec { import scala.collection.compat.* type BuilderFail type IzType - type Result[+T] = Either[List[BuilderFail], T] + type Result[+T] = F[List[BuilderFail], T] type TList = Result[List[IzType]] def listTList: List[TList] = Nil - def x(t: TList): Result[Unit] = Right { val _ = t } - - def F: Error2[Either] = Root.BIOEither - - /** `flatSequence` with error accumulation */ - def flatSequenceAccumErrors[ColR[x] <: IterableOnce[x], ColL[_], E, A]( - col: ColR[Either[ColL[E], IterableOnce[A]]] - )(implicit - buildR: Factory[A, ColR[A]], - buildL: Factory[E, ColL[E]], - iterL: ColL[E] => IterableOnce[E], - ): Either[ColL[E], ColR[A]] = { - F.flatSequenceAccumErrors(col) + def x(@unused t: TList): Result[Unit] = F.unit + + implicit def F: Error2[F] + def unsafeRun[E, A](f: F[E, A]): Either[E, A] + + implicit final class Run[+E, +A](f: F[E, A]) { + def run(): Either[E, A] = unsafeRun(f) } "ErrorAccumulatingOps" should { "have biFlatAggregate callable with typealiases" in { - def test: Either[List[BuilderFail], Unit] = { - val ret = F.flatSequenceAccumErrors /*[List, List, Any, BuilderFail, IzType]*/ (listTList: List[Either[List[BuilderFail], List[IzType]]]) + def test: F[List[BuilderFail], Unit] = { + val ret = F.flatSequenceAccumErrors(listTList) x(ret) } - assert(test.isRight) + assert(test.run().isRight) } "support NonEmptyCollections" in { - val nel0 = List(Right(1), Right(2), Left(NEList("error"))) + val nel0 = List(F.pure(1), F.pure(2), F.fail(NEList("error"))) assert(implicitly[Factory[String, NEList[String]]] ne null) - assert(F.sequenceAccumErrors(nel0) == Left(NEList("error"))) - - val nes0 = List(Right(()), Left(NESet("error"))) - assert(F.sequenceAccumErrors(nes0) == Left(NESet("error"))) - - assert(F.traverseAccumErrors_(nes0) { - case Left(value) => Left(value + "error1") - case Right(value) => Right(value) - } == Left(NESet("error", "error1"))) - - val nel1 = List(Right(List(1)), Left(NEList("error"))) - assert(F.flatSequenceAccumErrors(nel1) == Left(NEList("error"))) - - assert(F.traverseAccumErrors(nel1) { - case Left(value) => Left(Set(value ++ NEList("a"))) - case Right(value) => Right(Set(value ++ List(1))) - } == Left(Set(NEList("error", "a")))) - - val nel2 = List(Right(1), Left(NEList("error"))) - assert(F.flatTraverseAccumErrors(nel2)(_.map(i => List(i))) == Left(NEList("error"))) - assert(F.flatTraverseAccumErrors(nel2)(_.map(i => List(i))).map(_.to(Set)) == Left(NEList("error"))) + assert(F.sequenceAccumErrors(nel0).run() == Left(NEList("error"))) + + val nes0 = List(F.pure(()), F.fail(NESet("error"))) + assert(F.sequenceAccumErrors(nes0).run() == Left(NESet("error"))) + + assert( + F.traverseAccumErrors_(nes0)(_.attempt.flatMap { + case Left(value) => F.fail(value + "error1") + case Right(value) => F.pure(value) + }).run() == Left(NESet("error", "error1")) + ) + + val nel1 = List(F.pure(List(1)), F.fail(NEList("error"))) + assert(F.flatSequenceAccumErrors(nel1).run() == Left(NEList("error"))) + + assert( + F.traverseAccumErrors(nel1)(_.attempt.flatMap { + case Left(value) => F.fail(Set(value ++ NEList("a"))) + case Right(value) => F.pure(Set(value ++ List(1))) + }).run() == Left(Set(NEList("error", "a"))) + ) + + val nel2 = List(F.pure(1), F.fail(NEList("error"))) + assert(F.flatTraverseAccumErrors(nel2)(_.map(i => List(i))).run() == Left(NEList("error"))) + assert(F.flatTraverseAccumErrors(nel2)(_.map(i => List(i))).map(_.to(Set)).run() == Left(NEList("error"))) } "support the happy path" in { - val l0: Seq[Either[List[String], Int]] = List(Right(1), Right(2), Right(3)) - assert((F.sequenceAccumErrors(l0): Either[List[String], Seq[Int]]) == Right(Seq(1, 2, 3))) - assert(F.sequenceAccumErrors_(l0) == Right(())) - assert(F.traverseAccumErrors(l0)(identity) == Right(Seq(1, 2, 3))) - assert(F.traverseAccumErrors(l0)(identity).map(_.to(List)) == Right(List(1, 2, 3))) - assert(F.traverseAccumErrors_(l0)(_.map(_ => ())) == Right(())) + val l0: Seq[F[List[String], Int]] = List(F.pure(1), F.pure(2), F.pure(3)) + assert(F.sequenceAccumErrors(l0).run() == Right(Seq(1, 2, 3))) + assert(F.sequenceAccumErrors_(l0).run() == Right(())) + assert(F.traverseAccumErrors(l0)(identity).run() == Right(Seq(1, 2, 3))) + assert(F.traverseAccumErrors(l0)(identity).map(_.to(List)).run() == Right(List(1, 2, 3))) + assert(F.traverseAccumErrors_(l0)(_.map(_ => ())).run() == Right(())) - val l1: Seq[Either[List[String], Seq[Int]]] = List(Right(Seq(1)), Right(Seq(2)), Right(Seq(3))) - assert(F.flatTraverseAccumErrors(l1)(identity) == Right(Seq(1, 2, 3))) - assert(F.flatTraverseAccumErrors(l1)(identity).map(_.to(List)) == Right(List(1, 2, 3))) + val l1: Seq[F[List[String], Seq[Int]]] = List(F.pure(Seq(1)), F.pure(Seq(2)), F.pure(Seq(3))) + assert(F.flatTraverseAccumErrors(l1)(identity).run() == Right(Seq(1, 2, 3))) + assert(F.flatTraverseAccumErrors(l1)(identity).map(_.to(List)).run() == Right(List(1, 2, 3))) - assert(F.flatSequenceAccumErrors(l1) == Right(List(1, 2, 3))) + assert(F.flatSequenceAccumErrors(l1).run() == Right(List(1, 2, 3))) - val l2: Seq[Either[String, Int]] = List(Right(1), Right(2), Right(3), Left("error")) - assert(F.sequenceAccumErrorsNEList(l2) == Left(NEList("error"))) + val l2: Seq[F[String, Int]] = List(F.pure(1), F.pure(2), F.pure(3), F.fail("error")) + assert(F.sequenceAccumErrorsNEList(l2).run() == Left(NEList("error"))) - val l3: Seq[Either[List[String], Int]] = List(Right(1), Right(2), Right(3), Left(List("error"))) - assert(F.sequenceAccumErrors(l3) == Left(List("error"))) + val l3: Seq[F[List[String], Int]] = List(F.pure(1), F.pure(2), F.pure(3), F.fail(List("error"))) + assert(F.sequenceAccumErrors(l3).run() == Left(List("error"))) assert(Right(Seq(1, 2, 3)).map(_.to(List)) == Right(List(1, 2, 3))) } "support search" in { - assert(F.find(List(1, 2, 3))(i => Right(i == 2)) == Right(Some(2))) - assert(F.find(List(1, 2, 3))(i => Right(i == 4)) == Right(None)) - assert(F.find(List(1, 2, 3))(_ => Left("error")) == Left("error")) + assert(F.find(List(1, 2, 3))(i => F.pure(i == 2)).run() == Right(Some(2))) + assert(F.find(List(1, 2, 3))(i => F.pure(i == 4)).run() == Right(None)) + assert(F.find(List(1, 2, 3))(_ => F.fail("error")).run() == Left("error")) } "support fold" in { - assert(F.foldLeft(List(1, 2, 3))("") { case (acc, v) => Right(s"$acc.$v") } == Right(".1.2.3")) - assert(F.foldLeft(List(1, 2, 3))("") { case (_, _) => Left("error") } == Left("error")) + assert(F.foldLeft(List(1, 2, 3))("") { case (acc, v) => F.pure(s"$acc.$v") }.run() == Right(".1.2.3")) + assert(F.foldLeft(List(1, 2, 3))("") { case (_, _) => F.fail("error") }.run() == Left("error")) } "support partitioning" in { - val lst = List(Right(1), Right(2), Left(3)) - val Right((l, r)) = F.partition(lst) + val lst = List(F.pure(1), F.pure(2), F.fail(3)) + val Right((l, r)) = F.partition(lst).run(): @unchecked assert(l == List(3)) assert(r == List(1, 2)) } From 34498de80d44d8c72d8f38a7139f4b247f99cf69 Mon Sep 17 00:00:00 2001 From: Kai <450507+neko-kai@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:53:36 +0100 Subject: [PATCH 3/3] move ZIO test to JVM-only --- .../functional/bio/ErrorAccumulatingOpsTestZIO.scala | 8 ++++++++ .../izumi/functional/bio/ErrorAccumulatingOpsTest.scala | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 fundamentals/fundamentals-bio/.jvm/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTestZIO.scala diff --git a/fundamentals/fundamentals-bio/.jvm/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTestZIO.scala b/fundamentals/fundamentals-bio/.jvm/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTestZIO.scala new file mode 100644 index 0000000000..2fa5d03b44 --- /dev/null +++ b/fundamentals/fundamentals-bio/.jvm/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTestZIO.scala @@ -0,0 +1,8 @@ +package izumi.functional.bio + +final class ErrorAccumulatingOpsTestZIO extends ErrorAccumulatingOpsTest[zio.IO] { + private val runner: UnsafeRun2[zio.IO] = UnsafeRun2.createZIO() + + override implicit def F: Error2[zio.IO] = Root.Convert3To2(Root.BIOZIO) + override def unsafeRun[E, A](f: zio.IO[E, A]): Either[E, A] = runner.unsafeRun(f.attempt) +} diff --git a/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala b/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala index 2fe90a90d7..b5aee73353 100644 --- a/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala +++ b/fundamentals/fundamentals-bio/src/test/scala/izumi/functional/bio/ErrorAccumulatingOpsTest.scala @@ -10,13 +10,6 @@ final class ErrorAccumulatingOpsTestEither extends ErrorAccumulatingOpsTest[Eith override def unsafeRun[E, A](f: Either[E, A]): Either[E, A] = f } -final class ErrorAccumulatingOpsTestZIO extends ErrorAccumulatingOpsTest[zio.IO] { - private val runner: UnsafeRun2[zio.IO] = UnsafeRun2.createZIO() - - override implicit def F: Error2[zio.IO] = Root.Convert3To2(Root.BIOZIO) - override def unsafeRun[E, A](f: zio.IO[E, A]): Either[E, A] = runner.unsafeRun(f.attempt) -} - @nowarn("msg=Unused import") abstract class ErrorAccumulatingOpsTest[F[+_, +_]] extends AnyWordSpec { import scala.collection.compat.*