Kotlin Arrow make piece of code functional

112 Views Asked by At

I am having trouble refactoring this function here. How can I make it functional, in a way that instead of nested when-is you have sequential mappings/aggregations of LEFT=ErrorCodes? And if I wanted to add a try-catch-block to it, how would I make that functional? I am using 1.2.0-RC but have no objections downgrading it.

when(val maybeValid = ValidInput.create(uncheckedInput)) {
    is Either.Right -> {
        val validInput = maybeValid.value
        when(val maybeResult = Result.create(validInput)) {
            is Either.Right -> {
                TRY/CATCH, RETURN LEFT ON CATCH, RESULT ON RIGHT
            }
            is Either.Left -> { RETURN LEFT })) }
        }
    }
    is Either.Left -> maybeValid.leftOrNull()!!.map { RETURN LEFT }
}

I am just a bit overwhelmed of the toolset Arrow offers, originally I was just using Either as an advanced tuple, but I might as well aggregate the results in the way it is meant to be used.

2

There are 2 best solutions below

1
AudioBubble On BEST ANSWER

You can refactor the nested when expressions to a more functional style by using map, flatMap, and fold. Additionally, you can use Try to handle exceptions in a functional manner. Here's a refactored version of your code that's more functional and includes a try-catch block using the Try class:

fun processInput(uncheckedInput: String): Either<ErrorCodes, Result> {
return ValidInput.create(uncheckedInput)
    .flatMap { validInput ->
        Result.create(validInput)
            .flatMap { result ->
                Try {
                    // Perform the operation that might throw an exception
                    // Replace with your actual operation
                }.fold(
                    { throwable -> ErrorCodes.ExceptionError(throwable).left() },
                    { result.right() }
                )
            }
    }

}

Here's what the code does:

  • It starts by creating a ValidInput from the uncheckedInput. If it's successful, it proceeds to the next step using flatMap.

  • It tries to create a Result from the validInput. If it's successful, it proceeds to the next step using flatMap.

  • It uses the Try class to handle the exception in a functional manner. If the operation within the Try block throws an exception, it returns a Left with an ErrorCodes.ExceptionError. If the operation is successful, it returns a Right with the result.

This approach should give you a more functional and cleaner code structure, and you can easily add more steps to the process using flatMap if needed.

0
CLOVIS On

Since the accepted answer has been written, Arrow has changed quite a bit, and although the accepted answer is still correct, it is no longer the best way to write this.

As of Arrow 1.2.3 (with no expected changes before Arrow 2.0), the Raise DSL can simplify this code quite a bit:

fun processInput(uncheckedInput: String): Either<ErrorCodes, Result> = either {
    val validInput = ValidInput.create(uncheckedInput).bind()
    val result = Result.create(validInput).bind()
    
    withError(ErrorCodes::ExceptionError) {
        catch {
            // Perform the operation that might throw an exception
            // Replace with your actual operation
        }
    }
}

The main points are:

  • flatMap is replaced by bind, which is semantically identical but doesn't require a lambda, which makes the code much easier to read.
  • withError maps error types
  • Try is replaced by catch

When Context Receivers/Parameters are stabilized, this example will be further simplifiable, because bind will become implicit (in a very similar way to how suspend makes await after each function call implicit).

Assuming ValidInput.create and Result.create are rewritten to use context parameters, the solution will become:

context(Raise<ErrorCodes>)
fun processInput(uncheckedInput: String): Result {
    val validInput = ValidInput.create(uncheckedInput)
    val result = Result.create(validInput)
    
    return withError(ErrorCodes::ExceptionError) {
        catch {
            // Perform the operation that might throw an exception
            // Replace with your actual operation
        }
    }
}

Notice how this is basically the same code as you would have written in regular Kotlin code, with just the context declaration at the top. This is the end goal for Arrow: "idiomatic functional programming".