Repository: rcardin/raise4s (scala)

Issue:
Add a combineErrorsS function for `NonEmptyList[Raise[Error] ?=> A]` type that uses a `Semigroup[Error]` instance
Add a combineErrorsS function for `NonEmptyList[Raise[Error] ?=> A]` type that uses a `Semigroup[Error]` instance. The function must behave like the following:

```scala 3
import cats.Semigroup

implicit val intAdditionSemigroup: Semigroup[String] = _ + _

val intermediateResults: NonEmptyList[Raise[String] ?=> Int] = NonEmptyList.of(1, 2, 3, 4).map { i => 
  raise(i.toString)
}
val results: Raise[String] ?=> NonEmptyList[Int] = intermediateResults.combineErrorsS
```

Pull request:
Closes #101

Patch:
diff --git a/cats-raise4s/README.md b/cats-raise4s/README.md
index 3490ea4..907ad89 100644
--- a/cats-raise4s/README.md
+++ b/cats-raise4s/README.md
@@ -154,6 +154,32 @@ In general, the integration lets you use the _Cats_ type classes with the _Raise
   actual shouldBe "24"
   ```
 
+- Use the `combineErrorsS` with a `Semigroup` instance to accumulate errors into a `E` from a `NonEmptyList[A raises E]` block.
+
+  ```scala 3
+  import cats.data.*
+  import in.rcard.raise4s.cats.CatsBind.combineErrorsS
+
+  val iterableWithInnerRaise: NonEmptyList[Int raises String] =
+    NonEmptyList.of(1, 2, 3, 4, 5).map { value =>
+      if (value % 2 == 0) {
+        Raise.raise(value.toString)
+      } else {
+        value
+      }
+    }
+
+  val iterableWithOuterRaise: NonEmptyList[Int] raises String = iterableWithInnerRaise.combineErrorsS
+  
+  val actual = Raise.fold(
+    iterableWithOuterRaise,
+    identity,
+    identity
+  )
+  
+  actual shouldBe "24"
+  ```
+
 - Use the `Semigroup` type class to combine errors in the `zipOrAccumlateS` set of functions.
 
   ```scala 3
diff --git a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala
index 2aa65a6..17271c5 100644
--- a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala
+++ b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsBind.scala
@@ -71,7 +71,7 @@ object CatsBind {
      * }}}
      *
      * @param semigroup The semigroup to combine the errors defined on the type `Error`
-     * @return The list of the values or the accumulated errors
+     * @return The list of the values or the accumulated error
      */
     inline def combineErrorsS(using semigroup: Semigroup[Error]): Raise[Error] ?=> List[A] =
       CatsRaise.mapOrAccumulateS(iterable)(identity)
@@ -108,4 +108,34 @@ object CatsBind {
       */
     inline def values: RaiseNel[Error] ?=> NonEmptyList[A] =
       CatsRaise.mapOrAccumulate(nonEmptyList)(identity)
+
+    /**
+     * Accumulates all the occurred errors using the combine operator of the implicit [[Semigroup]]  and returns
+     * the non-empty list of the values or the accumulated errors.
+     *
+     * <h2>Example</h2>
+     * {{{
+     * val iterableWithInnerRaise: NonEmptyList[Int raises String] =
+     *   NonEmptyList.of(1, 2, 3, 4, 5).map { value =>
+     *     if (value % 2 == 0) {
+     *       Raise.raise(value.toString)
+     *     } else {
+     *       value
+     *     }
+     *   }
+     * val iterableWithOuterRaise: NonEmptyList[Int] raises String =
+     *   iterableWithInnerRaise.combineErrorsS
+     * val actual = Raise.fold(
+     *   iterableWithOuterRaise,
+     *   identity,
+     *   identity
+     * )
+     * actual shouldBe "24"
+     * }}}
+     *
+     * @param semigroup The semigroup to combine the errors defined on the type `Error`
+     * @return The non-empty list of the values or the accumulated error
+     */
+    inline def combineErrorsS(using semigroup: Semigroup[Error]): Raise[Error] ?=> NonEmptyList[A] =
+      CatsRaise.mapOrAccumulateS(nonEmptyList)(identity)
 }
diff --git a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala
index 591f775..1c04832 100644
--- a/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala
+++ b/cats-raise4s/src/main/scala/in/rcard/raise4s/cats/CatsRaise.scala
@@ -54,6 +54,52 @@ object CatsRaise {
   )(using r: Raise[Error]): List[B] =
     Raise.mapOrAccumulate(iterable, Semigroup[Error].combine)(transform)
 
+  /** Transform every element of `nonEmptyList` using the given `transform`, or accumulate all the
+    * occurred errors using the [[Semigroup]] type class defined on the `Error` type. The tailing
+    * `S` in the name of the function stands for <em>Semigroup</em>.
+    *
+    * <h2>Example</h2>
+    * {{{
+    * val block: NonEmptyList[Int] raises MyError2 =
+    *   CatsRaise.mapOrAccumulateS(NonEmptyList.of(1, 2, 3, 4, 5)) { value =>
+    *     if (value % 2 == 0) {
+    *       Raise.raise(MyError2(List(value.toString)))
+    *     } else {
+    *       value
+    *     }
+    *   }
+    * val actual = Raise.fold(
+    *   block,
+    *   identity,
+    *   identity
+    * )
+    * actual shouldBe MyError2(List("2", "4"))
+    * }}}
+    *
+    * @param nonEmptyList
+    *   The non-empty list of elements to transform
+    * @param transform
+    *   The transformation to apply to each element that can raise an error of type `Error`
+    * @param r
+    *   The Raise context
+    * @tparam Error
+    *   The type of the logical error that can be raised. It must have a [[Semigroup]] instance
+    *   available
+    * @tparam A
+    *   The type of the elements in the original non-empty list
+    * @tparam B
+    *   The type of the transformed elements
+    * @return
+    *   A non-empty list of transformed elements
+    */
+  inline def mapOrAccumulateS[Error: Semigroup, A, B](nonEmptyList: NonEmptyList[A])(
+      inline transform: Raise[Error] ?=> A => B
+  )(using r: Raise[Error]): NonEmptyList[B] = {
+    val result = Raise.mapOrAccumulate(nonEmptyList.toList, Semigroup[Error].combine)(transform)
+    // It's safe to call get here because we started from a non-empty list
+    NonEmptyList.fromList(result).get
+  }
+
   /** Accumulate the errors obtained by executing the `transform` over every element of `iterable`.
     * The error channel uses a [[NonEmptyList]] instance.
     *
@@ -139,7 +185,7 @@ object CatsRaise {
       inline transform: Raise[Error] ?=> A => B
   )(using r: RaiseNel[Error]): NonEmptyList[B] =
     val resultAsList = CatsRaise.mapOrAccumulate(nonEmptyList.toList)(transform)
-    // We know it's safe to call get here because we started from an non-empty list
+    // We know it's safe to call get here because we started from a non-empty list
     NonEmptyList.fromList(resultAsList).get
 
   /** Accumulate the errors from running `action1`, `action2`, `action3`, `action4`, `action5`,


Test patch:
diff --git a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala
index 5aa45f2..47ef49a 100644
--- a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala
+++ b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsBindSpec.scala
@@ -159,4 +159,42 @@ class CatsBindSpec extends AnyFlatSpec with Matchers {
 
     actual shouldBe "24"
   }
+
+  "combineErrorS on NonEmptyList" should "map all the element of the iterable" in {
+    val iterableWithInnerRaise: NonEmptyList[Int raises String] =
+      NonEmptyList.of(1, 2, 3, 4, 5).map { value1 =>
+        value1 + 1
+      }
+
+    val iterableWithOuterRaise: NonEmptyList[Int] raises String = iterableWithInnerRaise.combineErrorsS
+
+    val actual = Raise.fold(
+      iterableWithOuterRaise,
+      error => fail(s"An error occurred: $error"),
+      identity
+    )
+
+    actual shouldBe NonEmptyList.of(2, 3, 4, 5, 6)
+  }
+
+  it should "accumulate all the errors using the combine function" in {
+    val iterableWithInnerRaise: NonEmptyList[Int raises String] =
+      NonEmptyList.of(1, 2, 3, 4, 5).map { value =>
+        if (value % 2 == 0) {
+          Raise.raise(value.toString)
+        } else {
+          value
+        }
+      }
+
+    val iterableWithOuterRaise: NonEmptyList[Int] raises String = iterableWithInnerRaise.combineErrorsS
+
+    val actual = Raise.fold(
+      iterableWithOuterRaise,
+      identity,
+      identity
+    )
+
+    actual shouldBe "24"
+  }
 }
diff --git a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala
index 6065d56..11fb1e7 100644
--- a/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala
+++ b/cats-raise4s/src/test/scala/in/rcard/raise4s/cats/CatsRaiseSpec.scala
@@ -3,7 +3,7 @@ package in.rcard.raise4s.cats
 import cats.Semigroup
 import cats.data.{NonEmptyChain, NonEmptyList, Validated}
 import in.rcard.raise4s.RaiseAnyPredef.raise
-import in.rcard.raise4s.{Raise, raises, Raise as value}
+import in.rcard.raise4s.{Raise, raises}
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
@@ -49,6 +49,40 @@ class CatsRaiseSpec extends AnyFlatSpec with Matchers {
     actual shouldBe MyError2(List("2", "4"))
   }
 
+  "mapOrAccumulateS on NonEmptyList" should "map all the element of the iterable" in {
+    val block: NonEmptyList[Int] raises MyError2 =
+      CatsRaise.mapOrAccumulateS(NonEmptyList.of(1, 2, 3, 4, 5)) { value1 =>
+        value1 + 1
+      }
+
+    val actual = Raise.fold(
+      block,
+      error => fail(s"An error occurred: $error"),
+      identity
+    )
+
+    actual shouldBe NonEmptyList.of(2, 3, 4, 5, 6)
+  }
+
+  it should "accumulate all the errors using the combine function" in {
+    val block: NonEmptyList[Int] raises MyError2 =
+      CatsRaise.mapOrAccumulateS(NonEmptyList.of(1, 2, 3, 4, 5)) { value =>
+        if (value % 2 == 0) {
+          Raise.raise(MyError2(List(value.toString)))
+        } else {
+          value
+        }
+      }
+
+    val actual = Raise.fold(
+      block,
+      identity,
+      identity
+    )
+
+    actual shouldBe MyError2(List("2", "4"))
+  }
+
   "mapOrAccumulate to NonEmptyList" should "map all the element of the iterable" in {
     val block: List[Int] raises NonEmptyList[String] =
       CatsRaise.mapOrAccumulate(List(1, 2, 3, 4, 5)) { value1 =>