3

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

Eric Kolotyluk
  • 1,958
  • 2
  • 21
  • 30

2 Answers2

3

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.

Eric Kolotyluk
  • 1,958
  • 2
  • 21
  • 30
  • `Arrow` is heavy machinery that should not be introduced lightly into your code base. – David Soroko Oct 22 '21 at 16:04
  • Oh Thanks for the insight. Do you have any other references for that insight? I would willing to recommend Arrow in our codebase, but it would be nice to have good reasons. To be true, I have been less than happy with it. – Eric Kolotyluk Oct 23 '21 at 20:55
  • Kotlin+Arrow is a different language from Kotlin. It's has its own idioms and quirks. Different is not bad in itself but you need a strong justification to take it up for anything other then a hobby project. FP coolness is not enough IMHO. – David Soroko Oct 23 '21 at 21:55
  • 1
    Wow, thanks for the comments. I come from a Scala background, but understand we use Kotlin because our developers are not disciplined enough to use Scala, and while I really like Kotlin, I am often sad I am not using Scala, especially Scala 3. I did approach `Arrow` with initial zeal, but now I am pretty disillusioned, and now I know why. For similar reasons I stay away with a lot of eye-candy libraries people add to Scala. No, different is not bad itself, but professionally, at work, I don't have time to reverse engineer idiomatic quirkiness. – Eric Kolotyluk Oct 24 '21 at 22:38
  • @EricKolotyluk I think you mix up two things here. Project Loom is nice, but does not solve the problem of untyped errors. Your code still does not tell, which set of types errors can have if it still throws. Also I do not understand, what your problem is with your last code example from above. If i just change the return type to Unit, as it does not return anything. It just compiles fine. And I think your idea about the sealed hierarchy is right. – Nabil A. Mar 25 '22 at 08:12
  • And I am also a main Scala dev. But I helped out another team and introduced Arrow in their Kotlin code base. It works well, but I also realised that especially Scala 3 gives you a lot of tools to solve problems in a nice way that Kotlin as a language just don't. – Nabil A. Mar 25 '22 at 08:16
  • @NabilA., I am aware that Project Loom can improve error handling, and talking to the project members, they know too, and are working to improve things. For the most part, I have given up on Arrow, as I have been spoiled by Scala just working better... – Eric Kolotyluk Mar 26 '22 at 20:04
1

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.