Manejo de errores funcionales en Kotlin usando Arrow

imagen

Hola Habr!

Todos aman las excepciones de tiempo de ejecución. No hay mejor manera de descubrir que algo no se tuvo en cuenta al escribir el código. Especialmente, si las excepciones están dejando caer la aplicación entre millones de usuarios, y esta noticia viene en un correo electrónico de pánico desde el portal de análisis. Sábado por la mañana Cuando estás en un viaje al campo.

Después de esto, piensa seriamente en el manejo de errores, ¿y cuáles son las posibilidades que Kotlin nos brinda?

Lo primero que viene a la mente es try-catch. Para mí, una gran opción, pero tiene dos problemas:

  1. Después de todo, esto es un código adicional (un envoltorio forzado alrededor del código no afecta la legibilidad de la mejor manera).
  2. No siempre (especialmente cuando se usan bibliotecas de terceros) del bloque catch es posible obtener un mensaje informativo sobre qué causó exactamente el error.

Veamos en qué try-catch convierte el código al intentar resolver los problemas anteriores.

Por ejemplo, la función de ejecución de consulta de red más simple

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

se vuelve como

 fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (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) return null } } 

"No es tan malo", alguien puede decir, "usted y su kotlin quieren todo el azúcar de código", agrega (esta es una cita), y él estará ... dos veces en lo cierto. No, no habrá holivars hoy, todos deciden por sí mismos. Yo personalmente dictaminé el código del analizador json autoescrito, donde el análisis de cada campo estaba envuelto en try-catch, mientras que cada uno de los bloques de captura estaba vacío. Si alguien está satisfecho con este estado de cosas, una bandera en sus manos. Quiero ofrecer una mejor manera.

La mayoría de los lenguajes de programación funcional tecleados ofrecen dos clases para manejar errores y excepciones: Try and Either . Intente manejar excepciones, y ya sea para manejar errores de lógica de negocios.

La biblioteca Arrow le permite usar estas abstracciones con Kotlin. Por lo tanto, puede volver a escribir la solicitud anterior de la siguiente manera:

 fun makeRequest(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() } } 

¿En qué se diferencia este enfoque del uso de try-catch?

En primer lugar, cualquiera que lea este código después de usted (y probablemente lo hará) ya podrá comprender mediante la firma que la ejecución del código puede provocar un error, y escribir un código para procesarlo. Además, el compilador jurará si esto no se hace.

En segundo lugar, hay flexibilidad en cómo se puede manejar el error.

Dentro de Try, el error o el éxito de la ejecución se representan como las clases Failure y Success, respectivamente. Si queremos que la función siempre devuelva algo por error, podemos establecer el valor predeterminado:

 makeRequest(request).getOrElse { emptyList() } 

Si se requiere un manejo de errores más complejo, fold viene al rescate:

 makeRequest(request).fold( {ex -> //  -       emptyList() }, { data -> /*    */ } ) 

Puede usar la función de recuperación: su contenido se ignorará por completo si Try devuelve Success.

 makeRequest(request).recover { emptyList() } 

Puede usarlo para comprensiones (prestado por los creadores de Arrow de Scala) si necesita procesar el resultado de Éxito utilizando una secuencia de comandos llamando a la fábrica .monad () en Try:

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

La opción anterior se puede escribir sin usar el enlace, pero luego se leerá de una manera diferente:

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

Al final, el resultado de la función puede procesarse usando cuando:

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

Por lo tanto, con Arrow, puede reemplazar la construcción de try-catch lejos de ser ideal con algo flexible y muy conveniente. Una ventaja adicional de usar Arrow es que a pesar del hecho de que la biblioteca se posiciona como funcional, puede usar abstracciones individuales desde allí (por ejemplo, el mismo Try) mientras continúa escribiendo un buen código OOP. Pero te advierto: puede que te guste y te involucres, en un par de semanas comenzarás a estudiar Haskell, y tus colegas pronto dejarán de entender tu razonamiento sobre la estructura del código.

PD: Vale la pena :)

Source: https://habr.com/ru/post/es435254/


All Articles