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.