ZIO II: manejo de errores y recursos

En la entrada anterior, ZIO I: presentación, presenté la librería ZIO y ejemplos con la creación de efectos y operaciones básicas. En la presente entrada, ZIO II: manejo de errores y recursos, describiré cómo podemos manejar errores en la ejecución de efectos con ZIO y el manejo de recursos.

ZIO

La estructura de la entrada está compuesta de los siguientes apartados:

  1. Manejo de errores.
  2. Manejo de recursos.

1.- Manejo de errores

Dada la definición de un efecto en ZIO, sabemos cómo proporcionar el entorno y ejecutar dicho efecto; pero, tenemos que dar respuesta a la siguiente pregunta: ¿cómo podemos realizar el control de la ejecución si se produce un error en la ejecución del efecto? La respuesta es sencilla, el control del efecto se realiza capturando y controlando las excepciones que se puedan originar, así como, si se produce un error tener la posibilidad de poder volver a ejecutar el efecto.

  • Tratamiento de error con el contenedor binario Either.

La primera estrategia es empleando un contenedor binario Either mediante la función either en la cual podemos tener los siguientes valores: en Left, el valor de error; o bien en right, el resultado correcto. Este primer ejemplo es el más sencillo porque se asemeja al control de errores de una función.

El ejemplo más básico de la definición de un ejemplo es el siguiente:

val zeither: UIO[Either[String, Int]] = IO.fail("Boom!").either
val result: Either[String, Int]       = Runtime.default.unsafeRun(zeither)
assertResult(Left("Boom!"))(result)
  • Tratamiento de error en un efecto con el tipo explícito.

Supongamos que tenemos un efecto cuyo posible error lo conocemos; supongamos que el efecto, es la lectura de un fichero y, como conocemos, el error en el tratamiento de un fichero es la generación de una excepción de tipo IOException. La solución consiste en la definición de un efecto en el que definamos el tipo de error y su resultado; en concreto, la solución consiste en definir un efecto cuyo tipo de error es una excepción de tipo IOException y su resultado es un tipo List[String] definiendo un tipo UIO[IOException, List[String]].

En el siguiente ejemplo, se define una función que realiza la lectura de un fichero mediante un efecto de tipo UIO, su ejecución y verificación de tratamiento.

def readFile(nameFile: String): UIO[List[String]] = {
  IO.succeed(Source.fromFile(nameFile).getLines().toList)
}
val readFileResult: IO[IOException, List[String]] = readFile(getURIFileTest(nameFile).getPath)
val resultReadFileOK: List[String]                = Runtime.default.unsafeRun(readFileResult)
assert(resultReadFileOK.isEmpty === false)
assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)
  • Tratamiento de errores con la función catchAll.

Supongamos que realizamos la lectura de un fichero y queremos capturar todas las posibles excepciones que se puedan producir; para este escenario, utilizamos la función catchAll definida en ZIO. En el siguiente ejemplo, realizamos la lectura de las líneas de un fichero cuyo nombre es pasado por parámetro y, con la función catchAll, capturamos todas las excepciones. Si se produce una excepción entonces realizamos la lectura de un fichero cuyos datos son valores por defecto. El snippet de ejemplo es el siguiente:

 def readFileCatchAll(nameFile: String): Task[List[String]] = {
   ZIO(Source.fromFile(nameFile).getLines().toList).catchAll {
     case _ => {
       val uriFile = this.getClass.getClassLoader.getResource("default.data").toURI
       readFile(uriFile.getPath)
     }
   }
 }

 val readFileOK: Task[List[String]] = readFileCatchAll(getURIFileTest(nameFile).getPath)
 val resultReadFileOK: List[String] = Runtime.default.unsafeRun(readFileOK)
 assert(resultReadFileOK.isEmpty === false)
 assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)

 val readFileKO: Task[List[String]] = readFileCatchAll("errorFile.data")
 val resultReadFileKO: List[String] = Runtime.default.unsafeRun(readFileKO)
 assert(resultReadFileKO.isEmpty === false)
 assertResult(List("OK"))(resultReadFileKO)
  • Tratamiento de un error con la función catchSome.

Supongamos que queremos capturar un tipo determinado de excepción, en este supuesto utilizamos la función catchSome. En el siguiente ejemplo, se muestra el mismo ejemplo del apartado anterior pero realizando el tratamiento para la excepción FileNotFoundException. El snippet del ejemplo es el siguiente:

 def readFileOrDefault(nameFile: String): Task[List[String]] = {
   ZIO(Source.fromFile(nameFile).getLines().toList).catchSome {
     case _: FileNotFoundException => {
       val uriFile = this.getClass.getClassLoader.getResource("default.data").toURI
       readFile(uriFile.getPath)
     }
   }
 }

val readFileOK: Task[List[String]] = readFileOrDefault(getURIFileTest(nameFile).getPath)
val resultReadFileOK: List[String] = Runtime.default.unsafeRun(readFileOK)
assert(resultReadFileOK.isEmpty === false)
assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)

val readFileKO: Task[List[String]] = readFileOrDefault("errorFile.data")
val resultReadFileKO: List[String] = Runtime.default.unsafeRun(readFileKO)
assert(resultReadFileKO.isEmpty === false)
assertResult(List("OK"))(resultReadFileKO)
  • Ejecución de un efecto alternativo con la función orElse.

Supongamos que queremos ejecutar un efecto y, suponiendo que se produzca un error en el efecto, deseamos que se ejecute un efecto secundario; para este supuesto, utilizamos la función orElse. En el siguiente snippet de código se muestra el ejemplo con la función orElse.

def readFileFallback(nameFile: String): Task[List[String]] = {
   ZIO(Source.fromFile(nameFile).getLines().toList).orElse {
     val uriFile = this.getClass.getClassLoader.getResource("default.data").toURI
     readFile(uriFile.getPath)
    }
}

val readFileOK: Task[List[String]] = readFileFallback(getURIFileTest(nameFile).getPath)
val resultReadFileOK: List[String] = Runtime.default.unsafeRun(readFileOK)
assert(resultReadFileOK.isEmpty === false)
assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)

val readFileKO: Task[List[String]] = readFileFallback("errorFile.data")
val resultReadFileKO: List[String] = Runtime.default.unsafeRun(readFileKO)
assert(resultReadFileKO.isEmpty === false)
assertResult(List("OK"))(resultReadFileKO)
  • Tratamiento de un efecto de forma no pura.

Supongamos que queremos retornar el resultado y no realizar un tratamiento específico, es decir, si el efecto se ejecuta sin problemas retornamos el resultado; pero, si se produce un error retornamos un resultado del tipo esperado; para este supuesto, utilizamos la función fold. En el siguiente snippet de código se muestra el ejemplo con la función fold:

def readFileFold(nameFile: String): Task[List[String]] = {
  ZIO(Source.fromFile(nameFile).getLines().toList).fold(_ => List("OK"), data => data)
}

val readFileOK: Task[List[String]] = readFileFold(getURIFileTest(nameFile).getPath)
val resultReadFileOK: List[String] = Runtime.default.unsafeRun(readFileOK)
assert(resultReadFileOK.isEmpty === false)
assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)

val readFileKO: Task[List[String]] = readFileFold("errorFile.data")
val resultReadFileKO: List[String] = Runtime.default.unsafeRun(readFileKO)
assert(resultReadFileKO.isEmpty === false)
assertResult(List("OK"))(resultReadFileKO)
  • Tratamiento de un efecto de forma pura.

El caso contrario al ejemplo anterior es definir un efecto para el caso de éxito y caso de error mediante la función foldM. En el siguiente snippet de código se muestra el ejemplo con la función foldM.

def readFileFoldM(nameFile: String): Task[List[String]] = {
  ZIO(Source.fromFile(nameFile).getLines().toList)
    .foldM(_ => ZIO.succeed(List("OK")), data => ZIO.succeed(data))
}

val readFileOK: Task[List[String]] = readFileFoldM(getURIFileTest(nameFile).getPath)
val resultReadFileOK: List[String] = Runtime.default.unsafeRun(readFileOK)
assert(resultReadFileOK.isEmpty === false)
assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)

val readFileKO: Task[List[String]] = readFileFoldM("errorFile.data")
val resultReadFileKO: List[String] = Runtime.default.unsafeRun(readFileKO)
assert(resultReadFileKO.isEmpty === false)
assertResult(List("OK"))(resultReadFileKO)
  • Tratamiento con reintento de ejecución.

Supongamos que queremos reintentar ejecutar un efecto si se produce un error un número determinado de veces y,
si dado ese número de reintentos no tenemos éxito, capturar la excepción y retornar un efecto con un resultado por defecto; para este caso, utilizamos la función retry para definir un número de reintentos con un Schedule y la función catchAll. El snippet de código con el ejemplo es el siguiente:

import zio.clock.Clock
  [...]
  def readFileRetrying(nameFile: String): ZIO[Clock, Throwable, List[String]] = {
    ZIO(Source.fromFile(nameFile).getLines().toList)
      .retry(Schedule.recurs(5))
      .catchAll { case _ =>
        ZIO.succeed(List("OK"))
      }
}

val readFileOK: ZIO[Clock, Throwable, List[String]] = readFileRetrying(getURIFileTest(nameFile).getPath)
val resultReadFileOK: List[String]                  = Runtime.default.unsafeRun(readFileOK)
assert(resultReadFileOK.isEmpty === false)
assertResult(List("1 2 3", "4 5 6"))(resultReadFileOK)

val readFileKO: ZIO[Clock, Throwable, List[String]] = readFileRetrying("errorFile.data")
val resultReadFileKO: List[String]                  = Runtime.default.unsafeRun(readFileKO)
assert(resultReadFileKO.isEmpty === false)
assertResult(List("OK"))(resultReadFileKO)

Para el lector interesado en el código de los ejemplos, puede acceder al mismo a través del siguiente enlace.

2.- Manejo de recursos

Para el manejo de recursos es necesario definir un patrón estructural basado en la estructura try/finally. Supongamos que definimos un efecto y, una vez que finaliza su ejecución, queremos ejecutar un segundo efecto de finalización; para ello, utilizamos la función ensuring. Un ejemplo de patron try/finally con efectos en ZIO es el siguiente:

val finalizer2: UIO[Unit] = UIO.effectTotal(println("finally"))
val operation: UIO[Unit] = IO.succeed(println("Finalizing 2!")).ensuring(finalizer2)
val resultOperation      = Runtime.default.unsafeRun(operation)
assertResult(())(resultOperation)

Otra forma de aplicar el patrón try/finally es utilizando la función bracket en la cual se realiza una adquisición de un recurso, una tratamiento y un cierre de recurso. Un ejemplo de utilización de función bracket con un fichero es el siguiente:

def readFileBracket(nameFile: String): Task[List[String]] =
  UIO(Source.fromFile(nameFile)).bracket(bufferedSource => UIO(bufferedSource.close())) { file =>
    UIO(file.getLines().toList)
  }

val file: Task[List[String]] = readFileBracket(getURIFileTest(nameFile).getPath)
val resultFile               = Runtime.default.unsafeRun(file)
assertResult(List("1 2 3", "4 5 6"))(resultFile)

Para el lector interesado en el código de los ejemplos, puede acceder al mismo a través del siguiente enlace.

En el siguiente ejemplo, ZIO III: testing, describiré unos patrones para la realización de test con ZIO.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s