Kotlin + Either con Arrow

0
311
kotlin_either_arrow

En este breve tutorial vamos explorar un poco la librería de Arrow con Kotlin, en concreto el uso del Either. Si no estas familiarizado con Arrow, siempre puedes echar un vistazo a la documentación oficial aquí.

Índice de contenidos

1. Introducción

Kotlin es un lenguaje de programación que admite el paradigma funcional y orientada a objetos. Cuando desarrollamos líneas y líneas de código solemos realizar un conjunto de llamadas o una secuencia de llamadas para representar un estado como consecuencia de dichas llamadas. Es aquí donde entra en juego Arrow, nos ayuda simplificar ese control de flujo.

2. Entorno

  • Hardware: Portátil MacBook Pro 14 pulgadas (Apple M1 Pro, 32GB RAM)
  • Sistema Operativo: MacOS Monterey 12.5
  • Kotlin 1.7.10
  • Arrow 1.1.2
  • Kotest 1.2.5

3. Arrow

Arrow es una librería para programación funcional tipada en Kotlin que nos proporciona tipos de datos más populares como Option, Either o Validated.

4. Kotest

Kotest es un framework de test para Kotlin con aserciones extensas. Dispone de un módulo para Arrow que nos facilita las aserciones.

5. Ejemplo práctico

Partiremos de una supuesta academia con aulas y estudiantes. Resulta que nos han pedido crear un funcionalidad que dice lo siguiente: «Como administrador de la academia me gustaría poder asignar un estudiante a un aula existente o crear un aula por defecto en caso de que no exista y añadir dicho estudiante».

Para el ejemplo vamos a utilizar como tipo de dato Either<L, R> y las funciones map, flatMap, handleErrorWith y catch.

  • Either: es un tipo de dato que guarda un estado left (L) o right (R).
  • map: es una función que recibe otra función de transformación f: (R) -> R2 para pasar de un Either<L, R> a Either<L, R2>.
  • flatMap: es una función que recibe otra función de transformación f: (R) -> Either<L2, R2> para pasar de un Either<L, R> a Either<L2, R2>.
  • handleErrorWith: es una función que recibe otra funcion de transformación f: (L) -> Either<L2, R>. Su uso es bastante útil cuando queremos pasar de un left a un right.
  • catch: es una función que recibe otra función f: () -> R para evaluarla y devolver un Either<L, R>. Su uso es muy común cuando queremos convertir un estado a un Either<L, R>. En resumidas cuentas no todo devuelve un Either<L, R>, en este mismo ejemplo los servicios de dominio ClassRoomFinder o StudentFinder no tienen implementado Arrow y por ello hacemos uso del catch para realizar la conversión.

5.1. Dependencias

5.2. Diseño

Por un lado tenemos un aula:

Y por otro un estudiante:

5.2.1. Implementación sin Arrow

Para realizar la lógica de «asignación» de estudiantes disponemos de un servicio de dominio llamado StudentClassSimpleAssigner.

5.2.1.1. Servicio de dominio

Estoy casi seguro que los servicios de dominio recibidos por constructor son bastante claros, pero de todas formas dejo una breve acalaración de cada uno de ellos:

  • ClassRoomFinder: Busca un aula.
  • StudentFinder: Busca un estudiante.
  • ClassroomCreator: Crea un aula.

5.2.1.2. Test unitario

5.2.2. Implementación con Arrow

Disponemos de un servicio de dominio llamado StudentClassArrowAssigner.

5.2.2.1. Servicio de dominio

Si analizamos un poco el código.

  • Hacemos uso de Either para marcar un estado en la salida del método fun assign(email: String, classRoomName: String): Either<Throwable, Classroom>.
  • Hacemos uso de catch para evaluar y convertir la simple respuesta de cada servicio de dominio a un Either.
  • Hacemos uso de handleErrorWith para convertir de un left a un right cuando el aula no existe.
  • Hacemos uso de flatMap para hacer transformaciones sobre el Either.
  • Hacemos uso de map para hacer transformaciones solo sobre el right. Por ejemplo, en la línea 23 buscamos un estudiante y hacemos un map porque lo que queremos es un right del aula con el estudiante asignado.

5.2.2.2. Test unitario

Gracias a Kotest podemos evaluar un Either con los métodos shouldBeRight y shouldBeLeft.

6. Pequeña reflexión

Como primera impresión puede ser chocante y poco legible. Pero cuando estamos desarrollando un código más complejo con muchos flujos, estados y errores, podría ser de gran ayuda el uso de Arrow y no solo el tipo de dato Either.

Por ejemplo, podríamos tener el siguiente flujo:

Imaginaros hacer ésto mismo sin Arrow, ¿posible?, sí pero el código se convierte en algo denso y complicado de entender. Además la gestión de estados en cada operación sería un sin fin de if else o cualquier otro tipo de control de flujo, sin mencionar la gestión de errores con nuestro try catch. Aquí es donde debemos hacer una pequeña «reflexión»: ¿conviene la implementación de Arrow?.

Típica respuesta: depende. No es una cuestión de gustos.

Pero esto no es todo, otro punto a pensar es, ¿cómo manejamos la transaccionalidad con un Either?. La gestión de operaciones atómicas es diferente, deberíamos crear una pieza intermedia, y cuando sea un left realizar un rollback. La teoría parece simple pero la práctica indica lo contrario. A día de hoy los grandes frameworks como Spring o Micronaut no dispone integraciones con Arrow, por lo tanto, si estas interesado en manejar transaccionabilidad, es un buen punto a tener en cuenta antes de implementarlo.

7. Conclusión

El objetivo del tutorial no es convencer el uso de Either con Arrow, más bien una comparación. ¿Cuándo usar Either? y ¿cuándo no usar Either?, bajo mi opinión, en arquitecturas basadas en eventos donde no debería existir transaccionalidad es donde mejor encaja Either. En el resto de arquitecturas lo que añade es complejidad en el código y en la gestión de operaciones atómicas.

Y por último, si quieres crear tu propio Either, puedes echar un vistazo al siguiente tutorial pinchando aquí

8. Referencias

Dejar respuesta

Please enter your comment!
Please enter your name here