ZIO III: testing

Continuamos con al serie de la librería ZIO. En la entrada que estamos tratando, ZIO III: testing, me centraré en la definición de test. Las entradas publicadas hasta la fechas son las siguientes:

Los ejemplos mostrados en las entradas anteriores, se han realizado utilizando aserciones de test de prueba o bien mediante código no definido en un test. Para definir test y aserciones claras y concisas, definiremos unos patrones y ejemplos en los siguientes apartados, lo cuáles son:

  • Ejemplo de plantillas.
  • Generación de propiedades en los test.
  • Ejemplo de aserciones.

1.- Ejemplo de plantillas

Las pruebas unitarias tienen que ser categorizadas por funcionalidad y, para conseguir categorias funciones de test, empleamos la función suite. La función suite permite definir pruebas agrupados por una funcionalidad a probar. El conjunto de todas las agrupaciones forman las pruebas de una entidad.

Un requerimiento para la definición de test es que cada clase de test debe de heredar de la clase DefaultRunnableSpec la cual proporciona todos los módulos de ZIO; como por ejemplo: Clock o Random. Un ejemplo de test es el descrito en la siguiente entrada:

import zio.test._
import zio.clock.nanoTime
import Assertion._

import zio.test.DefaultRunnableSpec

object TemplateZioTest extends DefaultRunnableSpec {

    val suite1 = suite("suite1")(
      testM("s1.t1") { assertM(nanoTime)(isGreaterThanEqualTo(0L)) },
      testM("s1.t2") { assertM(nanoTime)(isGreaterThanEqualTo(0L)) }
    )

    val suite2 = suite("suite2")(
      testM("s2.t1") { assertM(nanoTime)(isGreaterThanEqualTo(0L)) },
      testM("s2.t2") { assertM(nanoTime)(isGreaterThanEqualTo(0L)) },
      testM("s2.t3") { assertM(nanoTime)(isGreaterThanEqualTo(0L)) }
    )

    val suite3 = suite("suite3")(
      testM("s3.t1") { assertM(nanoTime)(isGreaterThanEqualTo(0L)) }
    )

    def spec = suite("All test")(suite1, suite2, suite3)

}

2.- Generación de propiedades en los test.

En cierto tipo de test requerimos de datos para ejecutar las pruebas. Los datos pueden ser generados de forma automática por generadores los cuales pueden generar datos primitivos, case class o bien objetos. La entidad para la generación de datos es la entidad Gen definida en zio.test.Gen.

Un requisito fundamental es la necesidad de utilizar el módulo Random con Sized en la definición de los generadores.
Las dependencias de los módulos de los ejemplos es el siguiente:

import zio.test.Assertion.{equalTo, isTrue}
import zio.test.{DefaultRunnableSpec, Gen, Sized, assert, check, suite, testM}
import zio.random.Random
import zio.test.magnolia._
  • Ejemplo de generación de tipos primitivos.

Para la generación de tipos primitivos invocaremos a la función anyXXX, siendo XXX un tipo primitivo, en la definición de test. Un ejemplo de uso de generadores primitivos es el que se define en el siguiente snippet.

testM("Gen Int") {
   check(Gen.anyInt, Gen.anyInt, Gen.anyInt) { (x, y, z) =>
     assert((x + y) + z)(equalTo(x + (y + z)))
   }
},
  • Ejemplo de generación de una case class.

Sea una case class que represente una entidad con nombre Point. Para poder definir una generador de la clase Point, utilizamos la entidad DeriveGen cuyo tipo sea la case class Point. La definición de la clase y el generador de la clase Point es la siguiente:

final case class Point(x: Double, y: Double) {
   def isValid(): Boolean = true
}
val genPoint: Gen[Random with Sized, Point] = DeriveGen[Point]

Para definir el test de la entidad Point con su generador utilizaremos la función check como se muestra en el siguiente ejemplo:

testM("Gen Point") {
  check(genPoint) { (point) =>
     assert(point.isValid())(equalTo(true))
   }
},
  • Ejemplo de generación de objetos.

De la misma manera que el caso anterior para definir un generador de unos objetos a partir de un trait, se realiza de la misma manera. En el siguiente ejemplo, se define el test en donde se utiliza un generador de objetos basados en la definición de un trait:

sealed trait Color {
  def isValid(): Boolean = true
}
case object Red   extends Color
case object Green extends Color
case object Blue  extends Color
val genColor: Gen[Random with Sized, Color] = DeriveGen[Color]

testM("Gen Color") {
   check(genColor) { (color: Color) =>
      assert(color.isValid())(isTrue)
   }
}

3.- Ejemplo de aserciones.

La capacidad de poder verificar todo tipo de dato en una prueba permite definir con más exactitud la ejecución de una prueba. En ZIO empleamos las funciones definidas en la entidad zio.test.Assertion; como pueden ser: equalTo, hasField, isRight,etc…

A continuación, muestro unos ejemplos de pruebas con diferentes tipos de aserciones:

  • Ejemplo de un String.

Supongamos que necesitamos verificar el resultado de un efecto cuyo resultado es un String y, del valor del resultado,
necesitamos verificar que contenga un determinado valor y finalice con otro. La aserción la realizamos empleando la función assert y las funciones containsString y endsWithString de la siguiente manera:

testM("Assertion examples: string") {
  for {
    word <- IO.succeed("The StringTest")
  } yield {
    assert(word)(
       Assertion.containsString("StringTest") &&
          Assertion.endsWithString("Test")
     )
   }
},
  • Ejemplo de un Either.

Supongamos que necesitamos verificar el resultado de un efecto cuyo resultado es un Either. El esquema del test es parecido al anterior pero empleando funciones específicas para el contenedor binario. El ejemplo del snippet es el siguiente:

testM("Assertion examples: either") {
  for {
     either <- IO.succeed(Right(Some(2)))
  } yield {
     assert(either)(isRight(isSome(equalTo(2))))
  }
},
  • Ejemplo de una case class.

Supongamos que necesitamos verificar el resultado de un efecto que retorna una entidad definida en una case class. El esquema del test es como los anteriores pero utilizando la función hasField para acceder a los atributos de la entidad. El ejemplo del snippet es el siguiente:

testM("Assertion examples: case class") {
   final case class Address(country: String, city: String)
   final case class User(name: String, age: Int, address: Address)

   for {
      test <- IO.succeed(User("Nat", 25, Address("France", "Paris")))
   } yield {
      assert(test)(
        hasField("age", (u: User) => u.age, isGreaterThanEqualTo(18)) &&
          hasField("country", (u: User) => u.address.country, not(equalTo("USA")))
      )
   }
},

En la siguiente entrada, ZIO IV: modularización, me centraré en la definición de módulos funcionales.

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.

ZIO I: presentación

Inicio una serie de entradas de la librería ZIO. En la presente entrada, ZIO I: presentación, realizaré una presentación y realizaré unos ejemplos básicos introductorios.

ZIO es aquella librería en Scala para ejecutar tareas asíncronas y tareas de programación concurrente la cual es una librería funcional pura. La librería está inspirada en la mónada IO de Haskell.

El tipo de dato ZIO está compuesta por tres parámetros como sigue: ZIO[R, E, A]. Los tipos tienen la siguiente definición semántica:

  • R , Tipo de entorno.- El efecto requiere un tipo de entorno representado por R. Si el tipo está definido como Any, significa que no tiene requerimiento porque no necesitas un valor.
  • E , Tipo de fallo.- El efecto puede terminar en error con un tipo definido en E. Si puede terminar con error, se define con el tipo Throwable; si no puede terminar con error, se define con el tipo Nothing.
  • A, Tipo de éxito.- El efecto puede terminar con un tipo de éxito representado por el tipo A. Si el tipo es Unit, significa que el efecto no retorna información; si el tipo es Nothing, significa que el efecto está corriendo de forma indefinida

Unos ejemplos de definición de un tipo ZIO pueden ser los siguientes:

  • ZIO[Any, IOException, String].- Definición de un tipo que no tiene un requerimiento, retorna un valor de tipo String y, si se produce un error, retorna un elemento de tipo IOException.
  • ZIO[String, Throwable, Int].- Definición de un tipo con un requerimiento de tipo String, retorna un valor de tipo entero y, si se produce un error, retorna un elemento de tipo Throwable.

El requerimiento hay que visualizarlo como el valor de entrada al efecto para que sea procesado.

La librería ZIO define un conjunto de alias para poder trabajar de forma sencilla. Los alias definidos son las siguientes:

  • IO[E, A].- IO es el alias de ZIO[Any, E, A]. Define un efecto que no tiene requerimientos, el error puede ser de tipo E y el resultado es de tipo A.
  • UIO[A].- UIO es el alias de ZIO[Any, Nothing, A]. Define un efecto que no tiene requerimientos,
  • URIO[R, A].- URIO es el alias de ZIO[R, Nothing, A]. Define un efecto que tiene un requerimiento de tipo R, no puede tener un error y el resultado es de tipo A.
  • Task[A].- Task es el alias de ZIO[Any, Throwable, A]. Define un efecto que no tiene un requerimiento, el tipo de error es de tipo Throwable y el resultado es de tipo A.
  • RIO[R, A].- RIO es el alias de ZIO[R, Throwable, A]. Define un efecto que tiene un requerimiento de tipo R, el tipo de error es de tipo Throwable y el resultado de de tipo A.

La definición de las dependencias de la librería ZIO en un proyecto gestionado con sbt son las siguientes:

val zio = "1.0.3"
lazy val zio_core  = "dev.zio" %% "zio" % Versions.zio
lazy val zio_streams  = "dev.zio" %% "zio-streams" % Versions.zio
lazy val zio_test = "dev.zio" %% "zio-test"  % Versions.zio % "test"
lazy val zio_test_sbt = "dev.zio" %% "zio-test-sbt"  % Versions.zio % "test"
lazy val zio_test_magnolia = "dev.zio" %% "zio-test-magnolia" % Versions.zio % "test" 

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

  1. Creación de efectos.
  2. Operaciones básicas.

1.- Creación de efectos

En el presente apartado, mostraré ejemplos básicos para la definición de efectos con ZIO. Son ejemplos muy simples pero son aclaratorios para dar los primeros pasos. La definición de efectos se muestra en los siguientes puntos:

  • Efecto succeed.- empleamos la función succeed para crear un efecto cuyo resultado es exitoso.
val int42 = for {
   intS1 <- ZIO.succeed(42)
} yield (intS1)
val resultInt42 = Runtime.default.unsafeRun(int42)
assert(42 === resultInt42)
  • Efecto fail.- empleamos la función fail para crear un efecto cuyo resultado no es satisfactorio.
val f1: zio.URIO[Any, Either[String, Nothing]] = ZIO.fail("Uh oh!").either
val resultFailf1: Either[String, Nothing]      = Runtime.default.unsafeRun(f1)
assertResult(resultFailf1)(Left("Uh oh!"))
  • Efecto effectTotal.- empleamos la función effectTotal cuando estamos seguro que el efecto no tiene un efecto de lado.
val effectTotal: Task[Long] = ZIO.effectTotal(System.currentTimeMillis())
val resultEffectTotal: Long = Runtime.default.unsafeRun(effectTotal)
assert(resultEffectTotal > 0)
  • Efecto fromOption.- empleamos la función fromOption para crear un efecto a partir de un tipo Option.
val zoption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(2))
val resultZOption: Int                = Runtime.default.unsafeRun(zoption)
assert(2 === resultZOption)
  • Efecto fromEither.- empleamos la función fromEither para crear un efecto a partir de un tipo Either.
val zeither: IO[Either[Exception, String], String] = ZIO.fromEither(Right("Success"))
val resultZeither: String                                         = Runtime.default.unsafeRun(zeither)
assert("Success" === resultZeither)
  • Efecto fromTry.- empleamos la función fromTry para crear un efecto a partir de un tipo Try.
 val ztry: Task[Int] = ZIO.fromTry(Try(40 / 2))
 val resultZTry: Int = Runtime.default.unsafeRun(ztry)
 assert(20 === resultZTry)
  • Efecto fromFunction.- empleamos la función fromFuction para crear un efecto a partir de una función.
val zfun: URIO[Int, Int] = ZIO.fromFunction((i: Int) => i * i)
val resultZfun: Int      = Runtime.default.unsafeRun(zfun.provide(5))
assert(25 === resultZfun)
  • Efecto fromFuture.- empleamos la función fromFuture para crear un efecto a partir de un Future.
lazy val future = Future.successful("Hi!")
val zfuture: Task[String] = ZIO.fromFuture { implicit ec =>
   future.map(_ => "Goodbye!")
}
val resultZFuture: String = Runtime.default.unsafeRun(zfuture)
assert("Goodbye!" === resultZFuture)
  • Efecto desde un efecto de lado.- Definimos una función con un efecto de lado, en el ejemplo, escribir un texto en la consola.
def putStrLn(line: String): UIO[Unit] =
  ZIO.effectTotal(println(line))
val resultPut: Unit = Runtime.default.unsafeRun(putStrLn("Test"))
assert(resultPut === ())

Para el lector interesado, puede acceder al código de los ejemplos del apartado mediante el siguiente enlace.

2.- Operaciones básicas

Una vez visto cómo podemos crear efectos en ZIO, estamos en disposición de mostrar ejemplos de operaciones básicas. Las operaciones consisten en la declaración de un programa con un conjunto de operaciones; esas operaciones, pueden ser puras, o bien, pueden tener efectos de lado, por ejemplo: la lectura desde consola, o bien, escribir en la salida estándar. El primer ejemplo que voy a mostrar es la definición de una función que muestre un mensaje por pantalla y la lectura de consola. Para realizar el programa, definiremos dos funciones: getStrlLn, función que realiza la lectura por consola; putStrLn, función que escribe un mensaje en la consola. La definición de las funciones son las siguientes:

val getStrLn: Task[String] = ZIO.effect(StdIn.readLine())
def putStrLn(line: String): UIO[Unit] = ZIO.effectTotal(println(line))

Una vez definido las funciones básicas, definimos el programa que realiza la concatenación de las funciones anteriores mediante la función exampleChaining y, como segunda opción, definimos el mismo programa utilizando for comprehension. Los snippet de las funciones son las siguientes:

def exampleChaining(): Unit = {
      val operation1 = getStrLn.flatMap(input => putStrLn(s"-->${input}"))
      Runtime.default.unsafeRun(operation1)
}

def exampleForComprenhensions(): Unit = {
      val program = for {
        _    <- putStrLn("Nombre")
        name <- getStrLn
        _    <- putStrLn(s"Value=${name}")
      } yield ()
      Runtime.default.unsafeRun(program)
}

Otra forma de encadenar efectos es utilizando la función zip, zipRight, o bien, zipLeft. En el siguiente ejemplo, se muestan ejemplos parecidos a los anteriores con la función zipRight y su alias *>. Hay que destacar que la función zipRight realiza la concatenación de efectos y, además, ejecuta una función map para tratar el resultado del primer efecto. El snippet del código es el siguiente:

def exampleZipping(): Unit = {
      val zipRight1               = putStrLn("Name Right 1?").zipRight(getStrLn)
      val resultZipRight1: String = Runtime.default.unsafeRun(zipRight1)
      println(s"=>${resultZipRight1}")
      val zipRight2               = putStrLn("Name Right 2?") *> getStrLn
      val resultZipRight2: String = Runtime.default.unsafeRun(zipRight2)
      println(s"=>${resultZipRight2}")
}

Para el lector interesado, puede acceder al código de los ejemplos del apartado mediante el siguiente enlace.

En la siguiente entrada, ZIO II: manejo de errores y recursos, continuaremos describiendo la librería ZIO centrándonos en cómo se manejan errores y recursos mediante.