I am trying to use Arrow Either results instead of try-catch, but have gone too deep down the rabbit hole. 🙄
I have been trying to use Either<Problem,Value>
as my functional return types, where Problem
is like
sealed interface Problem
data class Caught(val cause: Throwable): Problem
data class DataKeyDisabled(val uuid: UUID, val cause: String): Problem
data class SubscriberNotFound(val uuid: UUID, val cause: String): Problem
data class NotEncrypted(val field: String): Problem
where the use case looks like
when (val result = transform(...)) {
is Right -> {}
is Left -> when (val problem = result.value) {
is Caught -> {}
is DataKeyDisabled -> {}
is SubscriberNotFound -> {}
is NotEncrypted -> {}
// else -> {} not needed...
}
}
But, there are really three types of problems, and I don't want to have to exhaust all the choices all the time.
Problem -> Caught
KeyProblem -> Caught, DataKeyDisabled, SubscriberNotFound
DataProblem -> Caught, DataKeyDisabled, SubscriberNotFound, NotEncrypted
For example, I want to have something like
sealed interface Problem
sealed interface KeyProblem : Problem
sealed interface DataProblem : KeyProblem
data class NotHandled(val cause: Throwable): Problem
data class DataKeyDisabled(val uuid: UUID, val cause: String): KeyProblem
data class SubscriberNotFound(val uuid: UUID, val cause: String): KeyProblem
data class NotEncrypted(val cause: String) : DataProblem
And I want to be able to have some code like
fun bar(input: Either<Problem,String>) : Either<KeyProblem,String> {
val something = when (input) {
is Right -> {}
is Left -> {
when (val problem = input.value) {
is NotHandled -> {}
is DataKeyDisabled -> {}
is SubscriberNotFound -> {}
is NotEncrypted -> {}
}
}
}
}
But Kotlin complains about NotHandled
, DataKeyDiabled
, and SubscriberNotFound
are not a DataProblem
In some cases, I want to return a KeyProblem
so I can drop the NotEncrypted
case from the when
, and in some cases I want to return only a Problem
such that the only case is NotHandled
.
I do not know how to express this in Kotlin. I suspect it is not possible to express this in Kotlin, so if someone tells me it is impossible, that is a solution.
I am thinking it was a bad decision to replace try-catch with Arrow Either. If so, someone please tell me so.
I wanted to stick to Functional Reactive Programming paradigms, where try-catch does not work, but with Kotlin coroutines it sort of does work. 🤔
It seems to me, the problem with sealed things is that when using when
you can only have one level of inheritance, and no more?
Maybe I am just looking at the whole problem the wrong way... help... please...
So my solution is to give up on trying to use Arrow Either and Kotlin sealed classes instead of using standard
try {
// return result
}
catch {
// handle or rethrow
}
finally {
// clean up
}
While I have been trying to practice Reactive and non-blocking programming for years, this was easy in Scala, but it's not easy in Kotlin.
After watching enough Java Project Loom videos, I am by far convinced this is the best way to go because exception handling just works... I could use Kotlin Coroutines because they also preserve correct exception handling, and may do that temporarily, but in the long run, Virtual Threads and Structured Concurrency are the way to go.
I hate using these words, but I am making a 'paradigm shift' back to cleaner code, retreating from this rabbit hole I have gone down.
It seems like you are going too far to re-use your error-types, when in fact your functions have different return-types and things that can go wrong. The simplest and cleanest solution in my opinion is to declare both the happy-case and error-case types per function. Then it should be very easy to only handle the cases than can actually go wrong per function.
For example if you have a function getPerson
, you would declare the data class Person
as the right value, and a GetPersonError
as the left value, where the GetPersonError
is an interface with only the relevant errors, like so:
private fun getPerson(identifier: String): Either<GetPersonError, Person> {...}
data class Person(name: String, ....)
sealed interface GetPersonError
sealed class PersonNotFoundError(): GetPersonError
sealed class InvalidIdentifierError(): GetPersonError
This does require you to write more code than reusing the same Problem
-class for multiple functions, but the code becomes very readable and easy to change, which is much more difficult to achieve when reusing a lot of code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With