Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  1. 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.
  2. If so, how do you achieve rollbacks with them?
  3. If the answer to question 2. is by using the transactionManager programatically - could you avoid that?
  4. I'll squeeze this one in, how do you avoid nesting Eithers?
like image 521
edu Avatar asked Apr 28 '21 11:04

edu


2 Answers

Some of your questions don't have straight forward answers but I'll do my best :D

  1. 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.

  1. 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 or either { } 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.
like image 198
nomisRev Avatar answered Oct 19 '22 15:10

nomisRev


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.

like image 22
Bastian Stein Avatar answered Oct 19 '22 16:10

Bastian Stein