Kurt
Kurt

Reputation: 545

Kotlin Result Function Chaining Patterns

Question for you smart people. I'm glad to see some functional objects in Kotlin such as Result, but I'm struggling how to use it effectively. Here's a couple of patterns that I came up with, but their must be better.

I'll just call this the imperative pattern for calling a series of functions that return a Result<*> instance:

fun myFunc(): Result<String> { 
   return runCatching { 
       val x = f1().getOrThrow() 
       val y = f2(x).getOrThrow()
       val z = f3(y).getOrThrow() 
       forTheWin(x, y, z).getOrThrow()
   } 
}

This works and it's certainly easy for imperative programmers to pick up and use.

However, it doesn't feel right to me. I would prefer some sort of chaining operation such as f(g()), but what do you do with a Result<Result<T>>, so I came up with some simple flattening functions:

fun <T> Result<Result<T>>.flatten(): Result<T> = when {
    isSuccess -> getOrNull()!!
    else -> Result.failure(this.exceptionOrNull()!!)
}

fun <T, R> Result<Result<T>>.flatmap(f: (T) -> R): Result<R> = this.flatten().map(f)

fun <T> Result<T>.mapFailure(f: (Throwable) -> Throwable): Result<T> = 
    this.recoverCatching { throw f.invoke(it) }

such that f(g()).flatten() returns a Result<T>. (I've tried and failed to write a flatten() that will flatten a Result of arbitrary depth.)

Taking the above example, I can rewrite it as:

fun myFunc(): Result<String> { 
   return f1().map { x -> 
          f2(x).flatMap { y -> 
             f3(y).flatMap { z -> 
                forTheWin(x, y, z)
       }.flatten
}

which still isn't satisfying as I usually have to do some careful massaging to get the map, flatmaps, and flatten to balance into a simple Result<T> result. If I could just call a single flatten(), it would go a long way to being a usable pattern.

Question: Does anybody have a pattern to chain functions that return Result types? Or, are there better functional objects in Kotlin?

Upvotes: 0

Views: 662

Answers (1)

Slimer
Slimer

Reputation: 1163

flatMap should be something like this:

inline fun <T, R> Result<T>.flatMap(f: (T) -> Result<R>): Result<R> = when {
  isSuccess -> (getOrNull() as T).let(f)
  isFailure -> this as Result<R>
  else -> {
    throw IllegalStateException("Unknown state")
  }
}

then you can do this:

fun myFun(): Result<String> {...}
fun anotherFun(s: String): Result<Int> {...}
fun andAnotherOne(i: Int): Result<Boolean> {...}

val result: Result<Boolean> = myFun().flatMap(::anotherFun).flatMap(::andAnotherOne)

Upvotes: 0

Related Questions