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

Simplify recover, and catch APIs. Add getOrElse #2954

Merged
merged 7 commits into from
Mar 3, 2023

Conversation

nomisRev
Copy link
Member

@nomisRev nomisRev commented Mar 1, 2023

Draft changes related to #2948 to facilitate the discussion. Also closes #2953

cc\ @freynder

Description about changes (copied from comments in #2948)

The lambda Raise<R>.() -> A allows to bind, or unwrap, Option and in the case None is encountered it will invoke the lambda. The lambda allows to raise a value of R into the outer interface Raise<R>, or fallback to a value of A.

  1. The Raise<R> lambda receiver is redundant because we're already inside the context of interface Raise<R>, so even without the lambda receiver we can call raise(r).
  2. Without Raise<R>, we have () -> A which matches the already existing Option API of getOrElse.
  3. Since this function is defined inside of an interface, it cannot be inline so it's inferior to the already existing getOrElse which is inline and thus allows for suspend to pass through.

So this API can be completely replaced by getOrElse and simplifies the examples of the KDoc above.

suspend fun test() {
  val empty: Option<Int> = None
  either {
-    val x: Int = empty.bind { _: None -> 1 }
+    val x: Int = empty.getOrElse { 1 }
-    val y: Int = empty.bind { _: None -> raise("Something bad happened: Boom!") }
+    val y: Int = empty.getOrElse { raise("Something bad happened: Boom!") }
-    val z: Int = empty.recover { _: None ->
+    val z: Int = empty.getOrElse {
      delay(10)
      1
-    }.bind { raise("Something bad happened: Boom!") }
+   }
    x + y + z
  } shouldBe Either.Left("Something bad happened: Boom!")
}

As the example shows we can now call suspend fun delay directly into the getOrElse lambda, and we can also still call raise & co from inside the getOrElse lambda.

The same applies for Result#bind. Since none of this code has yet been released, we can still remove these APIs in favour of the simpler inline methods on the original APIs. Fixing this issue also requires some other small changes in Builders.kt.


There is multiple benefits to this actually, and we should review the current signatures in arrow.core.raise.* to see how we can simplify them.

Looking for example at recover:

@RaiseDSL
- public inline fun <R, E, A> Raise<R>.recover(
+ public inline fun <E, A> recover(
  @BuilderInference action: Raise<E>.() -> A,
-  @BuilderInference recover: Raise<R>.(E) -> A,
+  recover: (E) -> A,
): A {
  contract {
    callsInPlace(action, EXACTLY_ONCE)
    callsInPlace(recover, AT_MOST_ONCE)
  }
  return fold<E, A, A>({ action(this) }, { throw it }, { recover(it) }, { it })
}

Besides still supporting the current use-cases it also makes this function more general purpose, because you can now use it to execute Raise<E> based programs and resolve the error completely without having to stick to Raise<E>.


@nomisRev nomisRev added the 1.2.0 Tickets belonging to 1.1.2 label Mar 1, 2023
@nomisRev nomisRev marked this pull request as ready for review March 1, 2023 16:53
}
return catch(action) { t: Throwable -> if (t is T) catch(t) else throw t }
}
public inline fun <reified T : Throwable, A> catch(action: () -> A, catch: (T) -> A): A =
Copy link
Member Author

@nomisRev nomisRev Mar 2, 2023

Choose a reason for hiding this comment

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

This also what we usedand needed in Gh Alerts Project to wrap side-effect APIs like PSQLException -> UserAlreadyExists and would allow easily writing type-safe integration libraries for JDBC or PSQL.

Wondering if this should be on Raise companion similar to Either.catch 🤔 They're basically the duality of each-other. Similarly, does Either.catch need a reified variant?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Raise sort of feels like much more of a primitive than Either, and thus it feels like catch makes more sense than Raise.catch. The name Raise.catch also just feels weird from a language standpoint

Copy link
Member Author

@nomisRev nomisRev Mar 2, 2023

Choose a reason for hiding this comment

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

Raise sort of feels like much more of a primitive than Either

It absolutely is!

The name Raise.catch also just feels weird from a language standpoint

Thanks for the feedback ☺️ catch is also applicable outside of Raise, so that makes sense!

Copy link
Member

@serras serras left a comment

Choose a reason for hiding this comment

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

Amazing simplification!

Copy link
Member

@raulraja raulraja left a comment

Choose a reason for hiding this comment

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

Looks great @nomisRev !

@nomisRev
Copy link
Member Author

nomisRev commented Mar 3, 2023

Merging this PR, all feedback and suggestions are still very welcome 🙏

@nomisRev nomisRev merged commit f6c8324 into main Mar 3, 2023
@nomisRev nomisRev deleted the sv-simplify-raise-apis branch March 3, 2023 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1.2.0 Tickets belonging to 1.1.2 discussion help wanted
Projects
None yet
Development

Successfully merging this pull request may close these issues.

["Request"] new function: Effect<R, A>.getOrElse()
4 participants