'Kotlin's Arrow Either<Exception, X> and transactions
I am trialing the use of Kotlin's Arrow library Either
object to handle exceptions within a project.
My experience with it thus far has been OK, but I'm struggling to find a way to handle transactions with Either
- and specifically rollbacks. In Spring throwing a RuntimeException
is a sure way to cause a transaction to rollback. However, by using Either
no exception is thrown and thus no rollback is triggered.
You can view this as a multifaceted question:
- Is
Either
appropriate for trueException
handling? Not alternative control flow, I mean true error situations where the flow of the program needs to stop. - If so, how do you achieve rollbacks with them?
- If the answer to question 2. is by using the
transactionManager
programatically - could you avoid that? - I'll squeeze this one in, how do you avoid nesting
Either
s?
Solution 1:[1]
Some of your questions don't have straight forward answers but I'll do my best :D
- Is Either appropriate for true Exception handling. Not alternative control flow, I mean true error situations where the flow of the program needs to stop.
Spring uses exceptions to model trigger rollbacks, so in that case you need to abide by Spring's mechanisms.
If you prefer to use an Either
API, you can probably wrap the exception based API of Spring with an Either<RuntimeException, A>
or Either<E, A>
one.
So to answer your question, Either
is appropriate for exception handling. However, typically you'll only catch the exceptions you're interessted and model them with your own error domain. Unexpected exceptions, or exceptions you cannot resolve are often allowed to bubble trough.
- If so, how do you achieve rollbacks with them?
Pseudo code example of wrapping transaction: () -> A
with transactionEither: () -> Either<E, A>
.
class EitherTransactionException(val result: Either<Any?, Any?>): RuntimeException(..)
fun transactionEither(f: () -> Either<E, A>): Either<E, A> =
try {
val result = transaction { f() }
when(val result) {
is Either.Right -> result
is Either.Left -> throw EitherTransactionException(result)
}
} catch(e: EitherTransactionException) {
return e.result as Either<E, A>
}
And now you should be able to use Either<E, A>
while keeping the exception based model of Spring intact.
If the answer to question 2. is by using the transactionManager programatically - could you avoid that?
I answered question 2 while already avoiding it. Alternatively, by using the transactionManager
programatically you could avoid having to throw that exception and recovering the value.
I'll squeeze this one in, how do you avoid nesting Eithers
- Use
Either#flatMap
oreither { }
to chain depenent values(Either<E, A>)
+(A) -> Either<E, B>
- Use
Either#zip
to combine independent values.Either<E, A>
+Either<E, B>
+(A, B) -> C
.
Solution 2:[2]
1. Is Either appropriate for true Exception handling?
A somewhat opinionated answer: Conventional error-handling via exceptions has downsides that are similar to those of using the dreaded GOTO statement: Execution jumps to another, specific point in your code - the catch statement. It is better than the GOTO in that it doesn't resume execution in a completely arbitraty point, but has to go up the function call stack.
With the Either<Error, Value> model of errorhandling, you do not interrupt the program flow, which makes the code easier to reason with, easier to debug, easier to test.
As such, I would not only say it is appropriate, but it is better.
2. If so, how do you achieve rollbacks with them?
I suggest to use Springs transaction template: Example:
fun <A, B> runInTransaction(block: () -> Either<A, B>): Either<A, B> {
return transactionTemplate.execute {
val result = block()
return@execute when (result) {
is Either.Left -> {
it.setRollbackOnly()
result
}
is Either.Right -> result
}
}!! // execute() is a java method which may return nulls
fun usage(): Either<String, String> {
return runInTransaction {
// do stuff
return@runInTransaction "Some error".left()
}
}
Now this is naive, in that it treats any left value as requiring a rollback. You probably want to adjust this for your purposes, for example by using a sealed class which encapsulates your possible error outcomes you wish to handle for your left cases.
You will also need to provide a transactionTemplate to the class that contains this method.
3. If the answer to question 2. is by using the transactionManager programatically - could you avoid that?
I don't see how, since Springs declarative transaction management is built on the Exception errorhandling model, and its interruption of control flow.
4. I'll squeeze this one in, how do you avoid nesting Eithers?
You can use Either.fx { }
to avoid either nesting. Note the ! syntax for binding the Either values to the scope of .fx. This "unpacks" them so to speak:
fun example(): Either<Error, Unit> {
return Either.fx {
val accessToken: String = !getAccessToken()
return@fx !callHttp(accessToken)
}
}
fun getAccessToken(): Either<Error, String> {
return "accessToken".right()
}
fun callHttp(token: String): Either<Error, Unit> {
return Unit.right()
}
For this to work, the Left values have to all be of the same Type. If a left value is bound, it will be returned. This allows you to avoid nested when statements or functional chaining with map/flatmap/fold etc.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | nomisRev |
Solution 2 |