Repository: rcardin/raise4s (scala)

Issue:
Currying DSL functions
There are a lot of functions that will be useful and ergonomic if we translate them into currying functions.
The functions are:

- [x] `Raise.ensure`
- [x] `Raise.ensureNotNull`
- [x] `Raise.recover` (version with two parameters)
- [x] `Raise.catching`
- [x] `Raise.withError`

Pull request:
Closes #12

Patch:
diff --git a/README.md b/README.md
index 4a09f0a..fea2017 100644
--- a/README.md
+++ b/README.md
@@ -112,9 +112,9 @@ def findUserByIdWithEx(id: String): User =
 
 val maybeUser: Either[Error, User] =
   Raise.either:
-    Raise.catching[User](() => findUserByIdWithEx("42"), {
+    Raise.catching[User](() => findUserByIdWithEx("42")) {
       case _: IllegalArgumentException => Raise.raise(UserNotFound("42"))
-    })
+    }
 ```
 
 There is also a version of the `catching` function defined as an extension method of any `A` type. The above code can be rewritten as follows:
@@ -136,7 +136,7 @@ def convertToUsd(amount: Double, currency: String): Double raises NegativeAmount
   else amount * 1.2
 
 val usdAmount: Double =
-  Raise.recover({ convertToUsd(-1, "EUR") }, { case NegativeAmount(amount) => 0.0D })
+  Raise.recover({ convertToUsd(-1, "EUR") }) { case NegativeAmount(amount) => 0.0D }
 ```
 
 ### Accumulating Errors
@@ -196,11 +196,11 @@ We can’t use the `mapOrAccumulate` function we previously saw because we don
 object Salary {
   def apply(amount: Double, currency: String): Salary raises List[SalaryError] = {
     Raise.zipOrAccumulate(
-      { Raise.ensure(amount >= 0.0, () => NegativeAmount) }, {
-        Raise.ensure(
-          currency != null && currency.matches("[A-Z]{3}"),
-          () => InvalidCurrency("Currency must be not empty and valid")
-        )
+      { Raise.ensure(amount >= 0.0)(NegativeAmount) },
+      {
+        Raise.ensure(currency != null && currency.matches("[A-Z]{3}")) {
+          InvalidCurrency("Currency must be not empty and valid")
+        }
       }
     ) { (_, _) =>
       Salary(amount, currency)
diff --git a/src/main/scala/in/rcard/raise4s/Fold.scala b/src/main/scala/in/rcard/raise4s/Fold.scala
index 70a1f22..1ad4ea6 100644
--- a/src/main/scala/in/rcard/raise4s/Fold.scala
+++ b/src/main/scala/in/rcard/raise4s/Fold.scala
@@ -77,60 +77,33 @@ private[raise4s] def _zipOrAccumulate[Error, A, B, C, D, E, F, G, H, I, J](
     action9: Raise[Error] ?=> I
 )(block: (A, B, C, D, E, F, G, H, I) => J)(using r: Raise[List[Error]]): J = {
   val errors = collection.mutable.ArrayBuffer.empty[Error]
-  val a: A = Raise.recover(
-    action1,
-    { newError =>
-      errors += newError; null.asInstanceOf[A]
-    }
-  )
-  val b: B = Raise.recover(
-    action2,
-    { newError =>
-      errors += newError; null.asInstanceOf[B]
-    }
-  )
-  val c: C = Raise.recover(
-    action3,
-    { newError =>
-      errors += newError; null.asInstanceOf[C]
-    }
-  )
-  val d: D = Raise.recover(
-    action4,
-    { newError =>
-      errors += newError; null.asInstanceOf[D]
-    }
-  )
-  val e: E = Raise.recover(
-    action5,
-    { newError =>
-      errors += newError; null.asInstanceOf[E]
-    }
-  )
-  val f: F = Raise.recover(
-    action6,
-    { newError =>
-      errors += newError; null.asInstanceOf[F]
-    }
-  )
-  val g: G = Raise.recover(
-    action7,
-    { newError =>
-      errors += newError; null.asInstanceOf[G]
-    }
-  )
-  val h: H = Raise.recover(
-    action8,
-    { newError =>
-      errors += newError; null.asInstanceOf[H]
-    }
-  )
-  val i: I = Raise.recover(
-    action9,
-    { newError =>
-      errors += newError; null.asInstanceOf[I]
-    }
-  )
+  val a: A = Raise.recover(action1) { newError =>
+    errors += newError; null.asInstanceOf[A]
+  }
+  val b: B = Raise.recover(action2) { newError =>
+    errors += newError; null.asInstanceOf[B]
+  }
+  val c: C = Raise.recover(action3) { newError =>
+    errors += newError; null.asInstanceOf[C]
+  }
+  val d: D = Raise.recover(action4) { newError =>
+    errors += newError; null.asInstanceOf[D]
+  }
+  val e: E = Raise.recover(action5) { newError =>
+    errors += newError; null.asInstanceOf[E]
+  }
+  val f: F = Raise.recover(action6) { newError =>
+    errors += newError; null.asInstanceOf[F]
+  }
+  val g: G = Raise.recover(action7) { newError =>
+    errors += newError; null.asInstanceOf[G]
+  }
+  val h: H = Raise.recover(action8) { newError =>
+    errors += newError; null.asInstanceOf[H]
+  }
+  val i: I = Raise.recover(action9) { newError =>
+    errors += newError; null.asInstanceOf[I]
+  }
   if errors.isEmpty then block(a, b, c, d, e, f, g, h, i)
   else r.raise(errors.toList)
 }
diff --git a/src/main/scala/in/rcard/raise4s/Raise.scala b/src/main/scala/in/rcard/raise4s/Raise.scala
index ba37a16..55e4413 100644
--- a/src/main/scala/in/rcard/raise4s/Raise.scala
+++ b/src/main/scala/in/rcard/raise4s/Raise.scala
@@ -22,9 +22,10 @@ object Raise {
 
   extension [A](a: => A)
     /** Extensions method version of the [[Raise.catching]] function.
-      */
+     */
+    //noinspection NoTailRecursionAnnotation
     @targetName("catchingThis")
-    def catching(catchBlock: Throwable => A): A = Raise.catching(() => a, catchBlock)
+    def catching(catchBlock: Throwable => A): A = Raise.catching(() => a)(catchBlock)
 
   /** Raises a _logical failure_ of type `Error`. This function behaves like a <em>return
     * statement</em>, immediately short-circuiting and terminating the computation.
@@ -60,7 +61,7 @@ object Raise {
     * <h2>Example</h2>
     * {{{
     * val actual: Int = fold(
-    *   { ensure(42 < 0, () => "error") },
+    *   { ensure(42 < 0) { "error" },
     *   error => 43,
     *   value => 42
     * )
@@ -76,8 +77,8 @@ object Raise {
     * @tparam Error
     *   The type of the logical error
     */
-  def ensure[Error](condition: Boolean, raise: () => Error)(using r: Raise[Error]): Unit =
-    if !condition then r.raise(raise())
+  def ensure[Error](condition: Boolean)(raise: => Error)(using r: Raise[Error]): Unit =
+    if !condition then r.raise(raise)
 
   /** Ensures that the `value` is not null; otherwise, [[Raise.raise]]s a logical failure of type
     * `Error`.
@@ -87,7 +88,7 @@ object Raise {
     * <h2>Example</h2>
     * {{{
     * val actual: Int = fold(
-    *   { ensureNotNull(null, () => "error") },
+    *   { ensureNotNull(null) { "error" } },
     *   error => 43,
     *   value => 42
     * )
@@ -107,8 +108,8 @@ object Raise {
     * @return
     *   The value if it is not null
     */
-  def ensureNotNull[B, Error](value: B, raise: () => Error)(using r: Raise[Error]): B =
-    if value == null then r.raise(raise())
+  def ensureNotNull[B, Error](value: B)(raise: => Error)(using r: Raise[Error]): B =
+    if value == null then r.raise(raise)
     else value
 
   /** Execute the [[Raise]] context function resulting in `A` or any _logical error_ of type
@@ -116,10 +117,9 @@ object Raise {
     *
     * <h2>Example</h2>
     * {{{
-    * val actual = recover(
-    *   { raise("error") },
+    * val actual = recover({ raise("error") }) {
     *   error => 43
-    * )
+    * }
     * actual should be(43)
     * }}}
     *
@@ -134,7 +134,7 @@ object Raise {
     * @return
     *   The result of the `block` or the fallback value
     */
-  def recover[Error, A](block: Raise[Error] ?=> A, recover: Error => A): A =
+  def recover[Error, A](block: Raise[Error] ?=> A)(recover: Error => A): A =
     fold(block, ex => throw ex, recover, identity)
 
   /** Execute the [[Raise]] context function resulting in `A` or any _logical error_ of type
@@ -194,7 +194,7 @@ object Raise {
     * @return
     *   The result of the `block` or the fallback value
     */
-  def catching[A](block: () => A, catchBlock: Throwable => A): A =
+  def catching[A](block: () => A)(catchBlock: Throwable => A): A =
     try block()
     catch
       case NonFatal(e) => catchBlock(e)
@@ -207,7 +207,7 @@ object Raise {
     * <h2>Example</h2>
     * {{{
     * val actual = either {
-    *   withError[Int, String, Int](s => s.length, { raise("error") })
+    *   withError[Int, String, Int](s => s.length) { raise("error") }
     * }
     * actual should be(Left(5))
     * }}}
@@ -227,11 +227,10 @@ object Raise {
     * @return
     *   The result of the `block`
     */
-  def withError[Error, OtherError, A](
-      transform: OtherError => Error,
+  def withError[Error, OtherError, A](transform: OtherError => Error)(
       block: Raise[OtherError] ?=> A
   )(using r: Raise[Error]): A =
-    recover(block, otherError => r.raise(transform(otherError)))
+    recover(block) { otherError => r.raise(transform(otherError)) }
 
   /** The most general way to execute a computation using [[Raise]]. Depending on the outcome of the
     * `block`, one of the three continuations is run:


Test patch:
diff --git a/src/test/scala/in/rcard/raise4s/BuildersSpec.scala b/src/test/scala/in/rcard/raise4s/BuildersSpec.scala
index 569312d..0de7b45 100644
--- a/src/test/scala/in/rcard/raise4s/BuildersSpec.scala
+++ b/src/test/scala/in/rcard/raise4s/BuildersSpec.scala
@@ -1,6 +1,6 @@
 package in.rcard.raise4s
 
-import in.rcard.raise4s.RaiseAnyPredef.{succeed, raise}
+import in.rcard.raise4s.RaiseAnyPredef.{raise, succeed}
 import in.rcard.raise4s.RaiseEitherPredef.bind
 import in.rcard.raise4s.RaiseOptionPredef.bind
 import in.rcard.raise4s.RaiseTryPredef.bind
@@ -25,12 +25,9 @@ class BuildersSpec extends AnyFlatSpec with Matchers {
 
     val actual = Raise.either {
       val x = one.bind()
-      val y = Raise.recover(
-        {
-          left.bind()
-        },
-        { _ => 1 }
-      )
+      val y = Raise.recover({
+        left.bind()
+      }) { _ => 1 }
       x + y
     }
 
@@ -51,7 +48,7 @@ class BuildersSpec extends AnyFlatSpec with Matchers {
 
     val actual = Raise.option {
       val x = some.bind()
-      val y = Raise.recover({ none.bind() }, { _ => 1 })
+      val y = Raise.recover({ none.bind() }) { _ => 1 }
       x + y
     }
 
@@ -77,7 +74,7 @@ class BuildersSpec extends AnyFlatSpec with Matchers {
 
     val actual = Raise.asTry {
       val x = one.bind()
-      val y = Raise.recover({ failure.bind() }, { _ => 1 })
+      val y = Raise.recover({ failure.bind() }) { _ => 1 }
       x + y
     }
 
diff --git a/src/test/scala/in/rcard/raise4s/RaiseSpec.scala b/src/test/scala/in/rcard/raise4s/RaiseSpec.scala
index 6a9d8f8..2e16f23 100644
--- a/src/test/scala/in/rcard/raise4s/RaiseSpec.scala
+++ b/src/test/scala/in/rcard/raise4s/RaiseSpec.scala
@@ -8,7 +8,7 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
 
   "ensure" should "return Unit if the given condition is met" in {
     val actual: Int = Raise.fold(
-      { Raise.ensure(42 > 0, () => "error") },
+      { Raise.ensure(42 > 0) { "error" } },
       error => 43,
       value => 42
     )
@@ -17,7 +17,7 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
 
   it should "return the error if the condition is not met" in {
     val actual: Int = Raise.fold(
-      { Raise.ensure(42 < 0, () => "error") },
+      { Raise.ensure(42 < 0) { "error" } },
       error => 43,
       value => 42
     )
@@ -26,7 +26,7 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
 
   "ensureNotNull" should "return the value if it is not null" in {
     val actual: Int = Raise.fold(
-      { Raise.ensureNotNull(42, () => "error") },
+      { Raise.ensureNotNull(42) { "error" } },
       error => 43,
       identity
     )
@@ -35,7 +35,7 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
 
   it should "return the error if the value is null" in {
     val actual: Int = Raise.fold(
-      { Raise.ensureNotNull(null, () => "error") },
+      { Raise.ensureNotNull(null) { "error" } },
       error => 43,
       value => 42
     )
@@ -43,29 +43,20 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
   }
 
   "recover" should "return the value if it is not an error" in {
-    val actual = Raise.recover(
-      { 42 },
-      error => 43
-    )
+    val actual = Raise.recover({ 42 }) { error => 43 }
 
     actual should be(42)
   }
 
   it should "return the recovery value if the value is an error" in {
-    val actual = Raise.recover(
-      { Raise.raise("error") },
-      error => 43
-    )
+    val actual = Raise.recover({ Raise.raise("error") }) { error => 43 }
 
     actual should be(43)
   }
 
   it should "rethrow the exception" in {
     assertThrows[RuntimeException] {
-      Raise.recover(
-        { throw new RuntimeException("error") },
-        error => 43
-      )
+      Raise.recover({ throw new RuntimeException("error") }) { error => 43 }
     }
   }
 
@@ -100,35 +91,26 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
   }
 
   "catching" should "return the value if no exception is thrown" in {
-    val actual = Raise.catching(
-      () => 42,
-      ex => 43
-    )
+    val actual = Raise.catching(() => 42) { ex => 43 }
 
     actual should be(42)
   }
 
   it should "return the recovery value if an exception is thrown" in {
-    val actual = Raise.catching(
-      () => throw new RuntimeException("error"),
-      ex => 43
-    )
+    val actual = Raise.catching(() => throw new RuntimeException("error")) { ex => 43 }
 
     actual should be(43)
   }
 
   it should "rethrow any fatal exception" in {
     assertThrows[OutOfMemoryError] {
-      Raise.catching(
-        () => throw new OutOfMemoryError("error"),
-        ex => 43
-      )
+      Raise.catching(() => throw new OutOfMemoryError("error")) { ex => 43 }
     }
   }
 
   "withError" should "return the value if it is not an error" in {
     val actual = Raise.either {
-      Raise.withError[Int, String, Int](s => s.length, { 42 })
+      Raise.withError[Int, String, Int](s => s.length) { 42 }
     }
 
     actual should be(Right(42))
@@ -136,7 +118,7 @@ class RaiseSpec extends AnyFlatSpec with Matchers {
 
   it should "return the transformed error if the value is an error" in {
     val actual = Raise.either {
-      Raise.withError[Int, String, Int](s => s.length, { Raise.raise("error") })
+      Raise.withError[Int, String, Int](s => s.length) { Raise.raise("error") }
     }
 
     actual should be(Left(5))