Repository: rcardin/raise4s (scala)

Issue:
Add a `combineErrors` function for `Iterable[Raise[Error] ?=> A]` type
Add a `combineErrors` function for `Iterable[Raise[Error] ?=> A]` type. The function must behave like the following:

```scala 3
val intermediateResults: List[Raise[String] ?=> Int] = List(1, 2, 3, 4).map(i => raise(i.toString))
val results: Raise[String] ?=> List[Int] = intermediateResults.combineErrors(_ + _)
```

Pull request description:
Closes #97

Patch:
diff --git a/core/src/main/scala/in/rcard/raise4s/Fold.scala b/core/src/main/scala/in/rcard/raise4s/Fold.scala
index da89a20..4e65267 100644
--- a/core/src/main/scala/in/rcard/raise4s/Fold.scala
+++ b/core/src/main/scala/in/rcard/raise4s/Fold.scala
@@ -1,7 +1,5 @@
 package in.rcard.raise4s
 
-import in.rcard.raise4s.Raise.mapOrAccumulate
-
 private[raise4s] inline def _mapOrAccumulate[Error, A, B](iterable: Iterable[A])(
     inline transform: Raise[Error] ?=> A => B
 )(using r: RaiseAcc[Error]): List[B] =
@@ -58,24 +56,24 @@ object RaiseIterableDef:
 
     /** Accumulates the errors of executions of the elements of the iterable and returns a list of
       * the values or the accumulated errors.
-     * 
-     * <h2>Example</h2>
-     * {{{
-     * val iterableWithInnerRaise: List[Int raises String] = List(1, 2, 3, 4, 5).map(value =>
-     *   if (value % 2 == 0) {
-     *     Raise.raise(value.toString)
-     *   } else {
-     *     value
-     *   }
-     * )
-     * val iterableWithRaiseAcc: List[Int] raises List[String] = iterableWithInnerRaise.values
-     * val actual = Raise.fold(
-     *   iterableWithRaiseAcc,
-     *   identity,
-     *   identity
-     * )
-     * actual shouldBe List("2", "4")
-     * }}}
+      *
+      * <h2>Example</h2>
+      * {{{
+      * val iterableWithInnerRaise: List[Int raises String] = List(1, 2, 3, 4, 5).map(value =>
+      *   if (value % 2 == 0) {
+      *     Raise.raise(value.toString)
+      *   } else {
+      *     value
+      *   }
+      * )
+      * val iterableWithRaiseAcc: List[Int] raises List[String] = iterableWithInnerRaise.values
+      * val actual = Raise.fold(
+      *   iterableWithRaiseAcc,
+      *   identity,
+      *   identity
+      * )
+      * actual shouldBe List("2", "4")
+      * }}}
       *
       * @return
       *   The list of the values or the accumulated errors
@@ -83,6 +81,37 @@ object RaiseIterableDef:
     inline def values: RaiseAcc[Error] ?=> List[A] = {
       Raise.mapOrAccumulate(iterable)(identity)
     }
+
+    /** Accumulates all the occurred errors using `combine` and returns the list of the values or
+      * the accumulated errors.
+      *
+      * <h2>Example</h2>
+      * {{{
+      * val iterableWithInnerRaise: List[Int raises MyError2] = List(1, 2, 3, 4, 5).map(value =>
+      *   if (value % 2 == 0) {
+      *     Raise.raise(MyError2(List(value.toString)))
+      *   } else {
+      *     value
+      *   }
+      * )
+      * val iterableWithErrorsCombined: List[Int] raises MyError2 =
+      *   iterableWithInnerRaise.combineErrors(combineErrors)
+      * val actual = Raise.fold(
+      *   iterableWithErrorsCombined,
+      *   identity,
+      *   identity
+      * )
+      * actual shouldBe MyError2(List("2", "4"))
+      * }}}
+      *
+      * @param combine
+      *   The function to combine two errors
+      * @return
+      *   The list of the values or the accumulated errors
+      */
+    inline def combineErrors(inline combine: (Error, Error) => Error): Raise[Error] ?=> List[A] = {
+      Raise.mapOrAccumulate(iterable, combine)(identity)
+    }
   }
 
 private[raise4s] inline def _zipOrAccumulate[Error, A, B, C, D, E, F, G, H, I, J](


Test patch:
diff --git a/core/src/test/scala/in/rcard/raise4s/FoldSpec.scala b/core/src/test/scala/in/rcard/raise4s/FoldSpec.scala
index 8feed98..60302c1 100644
--- a/core/src/test/scala/in/rcard/raise4s/FoldSpec.scala
+++ b/core/src/test/scala/in/rcard/raise4s/FoldSpec.scala
@@ -1,7 +1,6 @@
 package in.rcard.raise4s
 
-import in.rcard.raise4s.RaiseIterableDef.mapOrAccumulate
-import in.rcard.raise4s.RaiseIterableDef.values
+import in.rcard.raise4s.RaiseIterableDef.{combineErrors, mapOrAccumulate, values}
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
@@ -117,7 +116,7 @@ class FoldSpec extends AnyFlatSpec with Matchers {
   }
 
   "Values extension method" should "extract the values from the iterable" in {
-    val iterableWithInnerRaise: List[Int raises String] = List(1, 2, 3, 4, 5)
+    val iterableWithInnerRaise: List[Int raises String]     = List(1, 2, 3, 4, 5)
     val iterableWithRaiseAcc: List[Int] raises List[String] = iterableWithInnerRaise.values
 
     val actual = Raise.fold(
@@ -217,6 +216,41 @@ class FoldSpec extends AnyFlatSpec with Matchers {
     actual shouldBe List("2", "4")
   }
 
+  "combineErrors extension function" should "map all the element of the iterable" in {
+    val iterableWithInnerRaise: List[Int raises MyError2] = List(1, 2, 3, 4, 5)
+    val iterableWithErrorsCombined: List[Int] raises MyError2 =
+      iterableWithInnerRaise.combineErrors(combineErrors)
+
+    val actual = Raise.fold(
+      iterableWithErrorsCombined,
+      error => fail(s"An error occurred: $error"),
+      identity
+    )
+
+    actual shouldBe List(1, 2, 3, 4, 5)
+  }
+
+  it should "accumulate all the errors using the combine function" in {
+    val iterableWithInnerRaise: List[Int raises MyError2] = List(1, 2, 3, 4, 5).map(value =>
+      if (value % 2 == 0) {
+        Raise.raise(MyError2(List(value.toString)))
+      } else {
+        value
+      }
+    )
+
+    val iterableWithErrorsCombined: List[Int] raises MyError2 =
+      iterableWithInnerRaise.combineErrors(combineErrors)
+
+    val actual = Raise.fold(
+      iterableWithErrorsCombined,
+      identity,
+      identity
+    )
+
+    actual shouldBe MyError2(List("2", "4"))
+  }
+
   "zipOrAccumulate" should "zip 9 elements" in {
     val block: List[Int] raises List[String] = Raise.zipOrAccumulate(
       { 1 },