Functional error handling in Kotlin using Arrow

    image

    Hi, Habr!

    Everyone loves runtime exceptions. There is no better way to know that something was not taken into account when writing code. Especially - if exceptions crash the application from millions of users, and this news comes in a panicky email from the analytics portal. Saturday morning. When you're on a country trip.

    After this, you seriously think about error handling - and what opportunities does Kotlin offer us?

    The first try-catch comes to mind. For me it's a great option, but it has two problems:

    1. This is, after all, an extra code (a forced wrapper around the code, which does not have the best effect on readability).
    2. Not always (especially when using third-party libraries) from the catch block it is possible to receive an informative message that specifically caused the error.

    Let's see what try-catch turns the code into when trying to solve the above problems.

    For example, the simplest function to perform a network request

    funmakeRequest(request: RequestBody): List<ResponseData>? {
        val response = httpClient.newCall(request).execute()
        returnif (response.isSuccessful) {
            val body = response.body()?.string()
            val json = ObjectMapper().readValue(body, MyCustomResponse::class.java)
            json?.data
        } else {
            null
        }
    }

    becomes like

    funmakeRequest(request: RequestBody): List<ResponseData>? {
        try {
            val response = httpClient.newCall(request).execute()
            returnif (response.isSuccessful) {
                val body = response.body()?.string()
                val json = ObjectMapper().readValue(body, MyCustomResponse::class.java)
                json?.data
            } else {
                null
            }
        } catch (e: Exception) {
            log.error("SON YOU DISSAPOINT: ", e.message)
            returnnull
        }
    }

    “It’s not so bad,” someone might say, “you want all the code sugar with your cotlin,” he adds (this is a quote) - and it will be ... twice right. No, there will be no holivars today - everyone decides for himself. I personally ruled the code of a self-written json parser, where the parsing of each field was wrapped in try-catch, with each of the catch blocks being empty. If someone is satisfied with this state of affairs - the flag in hand. I want to suggest a better way.

    Most typed functional programming languages ​​offer two classes for handling errors and exceptions: Try and Either . Try to handle exceptions, and Either to handle business logic errors.

    Arrow libraryallows you to use these abstractions with Kotlin. Thus, you can rewrite the above query as the following:

    funmakeRequest(request: RequestBody): Try<List<ResponseData>> = Try {
        val response = httpClient.newCall(request).execute()
        if (response.isSuccessful) {
            val body = response.body()?.string()
            val json = ObjectMapper().readValue(body, MyCustomResponse::class.java)
            json?.data
        } else {
            emptyList()
        }
    }

    How is this approach different from using try-catch?

    Firstly, anyone who will read this code after you (and there are likely to be such) will be able to understand by the signature that the execution of the code can lead to an error - and write the code for its processing. Moreover, the compiler will scream if it is not done.

    Secondly, there is flexibility in how the error can be handled.

    Inside Try, the error or success of execution is represented as the Failure and Success classes, respectively. If we want the function to always return something on error, we can set the default value:

    makeRequest(request).getOrElse { emptyList() }

    If more error handling is required, fold comes to the rescue:

    makeRequest(request).fold(
        {ex ->
            // делаем что-то с ошибкой и возвращаем дефолтное значение
            emptyList()
        },
        { data -> /* используем полученные данные */ }
    )

    You can use the recover function - its contents will be completely ignored if Try returns Success.

    makeRequest(request).recover { emptyList() }

    You can use for comprehensions (borrowed by the Arrow creators from Scala) if you need to process the result of a Success using a sequence of commands by calling the factory .monad () on Try:

    Try.monad().binding {
        val r = httpclient.makeRequest(request)
        valdata = r.recoverWith { Try.pure(emptyList()) }.bind()
        val result: MutableList<Data> = data.toMutableList()
        result.add(Data())
        yields(result)
    }

    The above option can be written without using binding, but then it will be read differently:

    httpcilent.makeRequest(request)
        .recoverWith { Try.pure(emptyList()) }
        .flatMap { data ->
        	val result: MutableList<Data> = data.toMutableList()
            result.add(Data())
            Try.pure(result)
        }

    In the end, the result of the function can be processed with when:

    when(response) {
        is Try.Success -> response.data.toString()
        is Try.Failure -> response.exception.message
    }

    Thus, using Arrow, you can replace the far from ideal try-catch with something flexible and very convenient. The additional advantage of using Arrow is that despite the fact that the library positions itself as functional, you can use separate abstractions from there (for example, the same Try) by continuing to write good old OOP code. But I warn you, you may like it and get involved, in a couple of weeks you will start to learn Haskell, and your colleagues will very soon cease to understand your reasoning about the structure of the code.

    PS: It's worth it :)

    Also popular now: