Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When in Kotlin Either Hell

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

like image 664
Eric Kolotyluk Avatar asked Oct 19 '25 16:10

Eric Kolotyluk


2 Answers

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.

like image 64
Eric Kolotyluk Avatar answered Oct 21 '25 12:10

Eric Kolotyluk


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.

like image 30
kristianeaw Avatar answered Oct 21 '25 10:10

kristianeaw