Translate

Mostrando las entradas con la etiqueta ZIO. Mostrar todas las entradas
Mostrando las entradas con la etiqueta ZIO. Mostrar todas las entradas

jueves, 8 de junio de 2023

Primeros pasos con ZIO parte 4


ZIO puede convertir cualquier código (como una llamada a algún método) en un efecto, ya sea que ese código se llame síncrono (que devuelve directamente un valor) o asíncrono (que pasa un valor a las devoluciones de llamada).

Si se hace correctamente, cuando convierte el código en un efecto ZIO, este código se almacenará dentro del efecto para que ZIO pueda administrarlo y beneficiarse de funciones como reintentos, tiempos de espera y registro automático de errores.

Las funciones de conversión que tiene ZIO le permiten utilizar sin problemas todas las funciones de ZIO con código que no es de ZIO escrito en Scala o Java, incluidas las bibliotecas de terceros.

El código síncrono se puede convertir en un efecto ZIO usando ZIO.attempt:


import scala.io.StdIn

val readLine: ZIO[Any, Throwable, String] = ZIO.attempt(StdIn.readLine())


El tipo de error del efecto resultante siempre será Throwable, porque el código síncrono puede generar excepciones con cualquier valor de tipo Throwable.

Si sabe con certeza que algún código no arroja excepciones (excepto quizás excepciones en tiempo de ejecución), puede convertir el código en un efecto ZIO usando ZIO.succeed:


def printLine(line: String): UIO[Unit] =  ZIO.succeed(println(line))


A veces, es posible que sepa que el código arroja un tipo de excepción específico, y es posible que desee reflejar esto en el parámetro de error de su efecto ZIO.

Para ello, puede utilizar el método ZIO#refineToOrDie:


import java.io.IOException


val readLine2: ZIO[Any, IOException, String] =

  ZIO.attempt(StdIn.readLine()).refineToOrDie[IOException]


El código asíncrono que expone una API basada en devolución de llamada se puede convertir en un efecto ZIO usando ZIO.async:

object legacy {

  def login(

    onSuccess: User => Unit,

    onFailure: AuthError => Unit): Unit = ???

}


val login: ZIO[Any, AuthError, User] =

  ZIO.async[Any, AuthError, User] { callback =>

    legacy.login(

      user => callback(ZIO.succeed(user)),

      err  => callback(ZIO.fail(err))

    )

  }


Los efectos asincrónicos son mucho más fáciles de usar que las API basadas en devolución de llamadas y se benefician de las características de ZIO como la interrupción, la seguridad de los recursos y la gestión de errores.

Algunos códigos sincrónicos pueden participar en el llamado bloqueo de E/S, que pone un subproceso en un estado de espera, mientras espera que se complete alguna llamada del sistema operativo. Para obtener el máximo rendimiento, este código no debe ejecutarse en el grupo de subprocesos principal de su aplicación, sino en un grupo de subprocesos especial dedicado a operaciones de bloqueo.

ZIO tiene un grupo de subprocesos de bloqueo integrado en el tiempo de ejecución y le permite ejecutar efectos allí con ZIO.blocking:


import scala.io.{ Codec, Source }


def download(url: String) =

  ZIO.attempt {

    Source.fromURL(url)(Codec.UTF8).mkString

  }


def safeDownload(url: String) =

  ZIO.blocking(download(url))


Como alternativa, si desea convertir el código de bloqueo directamente en un efecto ZIO, puede usar el método ZIO.attemptBlocking:


val sleeping =

  ZIO.attemptBlocking(Thread.sleep(Long.MaxValue))


El efecto resultante se ejecutará en el grupo de subprocesos de bloqueo de ZIO.

Si tiene algún código síncrono que responderá a Thread.interrupt de Java (como Thread.sleep o código basado en bloqueo), entonces puede convertir este código en un efecto ZIO interrumpible usando el método ZIO.attemptBlockingInterrupt.

Algunos códigos síncronos solo se pueden cancelar invocando algún otro código, que es responsable de cancelar el cálculo en ejecución. Para convertir dicho código en un efecto ZIO, puede usar el método ZIO.attemptBlockingCanceble:


import java.net.ServerSocket

import zio.UIO


def accept(l: ServerSocket) =

  ZIO.attemptBlockingCancelable(l.accept())(ZIO.succeed(l.close()))


lunes, 5 de junio de 2023

Mi experiencia con la programación funcional en Scala


Creo que si leen este blog saben que me encanta la programación funcional y investigar y aprender. Y en eso estoy...


Me gustaría contar un poco mi evolución y mis altibajos para que la gente que sabe me aconseje y los que no saben puedan aprender de mis exitos y derrotas. 


Bueno, todo empezó con un curso de scala de coursera y quede encantado con el lenguaje y la programación funcional. De a poco tambien me interiorice en otros lenguajes como erlang, haskell, Clojure, Elixir, F#, etc ... pero siempre en la volvía a Scala por gusto nomas. Y me iba bastante bien...


Hasta que empece a quedarme corto en scala, quería progresar y decidí ver algunos framework funcionales, leí un libro de Cats y la verdad es que si bien esta bueno, me frustre mucho... porque no sabia porque se hacían ciertas cosas, no entendía porque tan complejo o si bien entendía desconfiaba ( no hay otra forma de hacerlo más fácil) y bueno ... 


Cambie a Akka, y me pareció bueno y me sentí más en mi mundo, pero justo justo cuando volvia el entuciasmo, cambio de licencia y bueno... Se fue el entusiasmo, ahora sé que existe un fork open source llamado pekko (https://github.com/apache/incubator-pekko) y es de apache. Pero lo veo verde, y no sé cuanto va a crecer ...


Y ahora estoy con ZIO y por ahora todo va bien, por ende la conclusión es si quieren progresar en su conocimiento en Scala, y no vienen del mundo funcional como Haskell, ZIO esta bueno. 

Si son nuevos en el mundo funcional y scala, yo haría lo siguiente: 

1. Estudiar Scala (curso de scala de coursera, el libro rojo de programación funcional en scala, etc ...)

2. Leer y estudiar ZIO 

3. Leer y estudiar Cats


Pero no perder de vista Pekko, para ver como progresa. 


Que opinan? 


domingo, 4 de junio de 2023

Primeros pasos con ZIO parte 3


Vamos a crear efectos funcionales de ZIO, y podemos hacerlo a partir de valores, cálculos y tipos de datos comunes de Scala.

Usando el método ZIO.succeed, se puede crear un efecto que, cuando se ejecute, tendrá éxito con el valor especificado:

val s1 = ZIO.succeed(42)

El método de succeed toma un parámetro, que garantiza que si le pasa al método algún código para ejecutar, este código se almacenará dentro del efecto ZIO para que ZIO pueda administrarlo y beneficiarse de características como reintentos, tiempos de espera y registro automático de errores.

Usando el método ZIO.fail, puede crear un efecto que, cuando se ejecuta, fallará con el valor especificado:

val f1 = ZIO.fail("Uh oh!")

Para el tipo de datos ZIO, no hay restricción en el tipo de error. Se puede usar cadenas, excepciones o tipos de datos personalizados.

Podemos usar excepciones :

val f2 = ZIO.fail(new Exception("Uh oh!"))


La biblioteca estándar de Scala contiene varios tipos de datos que se pueden convertir en efectos ZIO.

Un Option se puede convertir en un efecto ZIO usando ZIO.fromOption:


val zoption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(2))


Option[Nothing], lo que significa que si dicho efecto falla, fallará con el valor Nothing (que tiene el tipo Opción[Nothing]).

Puede transformar una falla en algún otro valor de error usando orElseFail, uno de los muchos métodos que proporciona ZIO para la gestión de errores:


val zoption2: ZIO[Any, String, Int] = zoption.orElseFail("It wasn't there!")


ZIO tiene una variedad de otros operadores diseñados para facilitar el manejo de Option. En el siguiente ejemplo, los operadores Some y asSomeError se utilizan para facilitar la interfaz con los métodos que devuelven Option, similar al tipo OptionT en algunas bibliotecas de Scala.


val maybeId: ZIO[Any, Option[Nothing], String] = ZIO.fromOption(Some("abc123"))

def getUser(userId: String): ZIO[Any, Throwable, Option[User]] = ???

def getTeam(teamId: String): ZIO[Any, Throwable, Team] = ???


val result: ZIO[Any, Throwable, Option[(User, Team)]] = (for {

  id   <- maybeId

  user <- getUser(id).some

  team <- getTeam(user.teamId).asSomeError 

} yield (user, team)).unsome 


Un Some se puede convertirse en un efecto ZIO usando ZIO.fromEither:


val zeither: ZIO[Any, Nothing, String] = ZIO.fromEither(Right("Success!"))


El tipo de error del efecto resultante será el del caso Izquierdo (Left), mientras que el tipo de éxito será el del caso Derecho (Right).

Un valor Try se puede convertir en un efecto ZIO usando ZIO.fromTry:


import scala.util.Try


val ztry = ZIO.fromTry(Try(42 / 0))


El tipo de error del efecto resultante siempre será Throwable porque Try solo puede fallar con valores de tipo Throwable.


Un Scala Future se puede convertir en un efecto ZIO usando ZIO.fromFuture:


import scala.concurrent.Future


lazy val future = Future.successful("Hello!")

val zfuture: ZIO[Any, Throwable, String] =  ZIO.fromFuture { implicit ec =>

    future.map(_ => "Goodbye!")

  }


La función pasada a fromFuture recibe un ExecutionContext, que permite a ZIO administrar dónde se ejecuta Future (por supuesto, puede ignorar este ExecutionContext).

El tipo de error del efecto resultante siempre será Throwable, porque los valores Future solo pueden fallar con valores de tipo Throwable.






miércoles, 31 de mayo de 2023

Primeros pasos con ZIO parte 2


ZIO es framework para crear aplicaciones nativas en la nube. Con un núcleo funcional amigable para principiantes pero poderoso, ZIO permite a los desarrolladores crear rápidamente aplicaciones con las mejores prácticas que son altamente escalables, comprobables, robustas, resistentes, seguras para los recursos, eficientes y observables.

En el corazón de ZIO se encuentra un poderoso tipo de datos llamado ZIO, que es el bloque de construcción fundamental para cada aplicación ZIO.

El tipo de datos ZIO se denomina efecto funcional y representa una unidad de cálculo dentro de una aplicación ZIO. Al igual que un modelo o un flujo de trabajo, los efectos funcionales son planes precisos que describen un cálculo o una interacción. Cuando se ejecuta una aplicación ZIO, un efecto funcional fallará con algún tipo de error o tendrá éxito con algún tipo de valor.

Al igual que el tipo de datos List, el tipo de datos ZIO es un tipo de datos genérico y usa parámetros de tipo para mejorar la seguridad de tipos. El tipo de datos de Lista tiene un solo parámetro de tipo, que representa el tipo de elemento que se almacena en la Lista. El tipo de datos ZIO tiene tres parámetros de tipo: ZIO[R, E, A].

Los parámetros de tipo del tipo de datos ZIO tienen los siguientes significados:

  • R - Tipo de entorno. El parámetro de tipo de entorno representa el tipo de datos contextuales que requiere el efecto antes de que se pueda ejecutar. Por ejemplo, algunos efectos pueden requerir una conexión a una base de datos, mientras que otros pueden requerir una solicitud HTTP y otros pueden requerir una sesión de usuario. Si el parámetro de tipo de entorno es Any, entonces el efecto no tiene requisitos, lo que significa que el efecto se puede ejecutar sin proporcionarle primero un contexto específico.
  • E - Tipo de falla. El parámetro tipo de falla representa el tipo de error con el que el efecto puede fallar cuando se ejecuta. Aunque Exception o Throwable son tipos de falla comunes en las aplicaciones ZIO, ZIO no impone ningún requisito sobre el tipo de error y, a veces, es útil definir tipos de error comerciales o de dominio personalizados para diferentes partes de una aplicación. Si el parámetro de tipo de error es Nothing, significa que el efecto no puede fallar.
  • A - Tipo de éxito. El parámetro de tipo de éxito representa el tipo de éxito con el que el efecto puede tener éxito cuando se ejecuta. Si el parámetro de tipo de éxito es Unit, significa que el efecto no produce información útil (similar a un método de devolución de vacío), mientras que si es Nothing, significa que el efecto se ejecuta para siempre, a menos que falle.

Como varios ejemplos de cómo interpretar los tipos de efectos ZIO:

  • Un efecto de tipo ZIO[Any, IOException, Byte] no tiene requisitos y, cuando se ejecuta, dicho efecto puede fallar con un valor de tipo IOException o puede tener éxito con un valor de tipo Byte.
  • Un efecto de tipo ZIO[Connection, SQLException, ResultSet] requiere una conexión y, cuando se ejecuta, dicho efecto puede fallar con un valor de tipo SQLException o puede tener éxito con un valor de tipo ResultSet.
  • Un efecto de tipo ZIO[HttpRequest, HttpFailure, HttpSuccess] requiere una HttpRequest y, cuando se ejecuta, dicho efecto puede fallar con un valor de tipo HttpFailure o puede tener éxito con un valor de tipo HttpSuccess.

El parámetro de tipo de entorno es un parámetro de tipo compuesto porque, a veces, un solo efecto puede requerir varios valores de diferentes tipos. Si ve que un efecto tiene un tipo de ZIO[UserSession with HttpRequest, E, A] (Scala 2.x) o ZIO[UserSession & HttpRequest, E, A] (Scala 3.x), significa que el efecto requiere múltiples valores contextuales antes de que pueda ejecutarse.

Aunque esta analogía no es precisa, se puede pensar en un efecto ZIO como una función:


R => Either[E, A]


Esta función requiere una R y produce una falla de tipo E o un valor de éxito de tipo A.

Los efectos ZIO no son en realidad funciones, por supuesto, porque modelan cálculos e interacciones complejos, que pueden ser asincrónicos, concurrentes o ingeniosos.

El tipo de datos ZIO es el único tipo de efecto en ZIO. Sin embargo, hay una familia de alias de tipo que reducen la necesidad de escribir:

  • UIO[A]: un alias de tipo para ZIO[Any, Nothing, A], que representa un efecto que no tiene requisitos, no puede fallar y puede tener éxito con una A.
  • URIO[R, A]: un alias de tipo para ZIO[R, Nothing, A], que representa un efecto que requiere una R, no puede fallar y puede tener éxito con una A.
  • Task[A]: un alias de tipo para ZIO[Any, Throwable, A], que representa un efecto que no tiene requisitos, puede fallar con un valor Throwable o tener éxito con una A.
  • RIO[R, A]: un alias de tipo para ZIO[R, Throwable, A], que representa un efecto que requiere una R, puede fallar con un valor Throwable o tener éxito con una A.
  • IO[E, A]: un alias de tipo para ZIO[Any, E, A], que representa un efecto que no tiene requisitos, puede fallar con una E o tener éxito con una A.

Si es nuevo en los efectos funcionales, le recomendamos que comience con el tipo de tarea, que tiene un solo parámetro de tipo y se corresponde más con los tipos de datos futuros integrados en las bibliotecas estándar de Scala y Java.

Si está utilizando bibliotecas Cats Effect, puede encontrar útil el tipo RIO, ya que le permite enhebrar el contexto a través de bibliotecas de terceros.

Independientemente del tipo de alias que utilice en su aplicación, UIO puede ser útil para describir efectos infalibles, incluidos los que resultan del manejo de todos los errores.

Finalmente, si es un programador funcional experimentado, se recomienda el uso directo del tipo de datos ZIO, aunque puede resultarle útil crear su propia familia de alias de tipo en diferentes partes de su aplicación.

Si se siente cómodo con el tipo de datos ZIO y su familia de alias de tipo, el siguiente paso es aprender a crear efectos ...

lunes, 29 de mayo de 2023

Primeros pasos con ZIO


Empecemos desde el principio, incluyamos ZIO en nuestro proyecto agregando la siguiente dependencia en el build.sbt:


libraryDependencies += "dev.zio" %% "zio" % "2.0.13"


Para hacer una aplicación ZIO podemos hacer que nuestra aplicación extienda de ZIOAppDefault, que permite escribir todo el programa usando ZIO:


import zio._

import zio.Console._


object MyApp extends ZIOAppDefault {


  def run = myAppLogic


  val myAppLogic =

    for {

      _    <- printLine("Hello! What is your name?")

      name <- readLine

      _    <- printLine(s"Hello, ${name}, welcome to ZIO!")

    } yield ()

}


El método run debemos devolver un valor ZIO que tiene todos sus errores manejados, que, en la jerga de ZIO, es un valor ZIO no excepcional.

Una forma de hacer esto es invocar un pliegue sobre un valor ZIO, para obtener otro valor ZIO no excepcional. Eso requiere dos funciones de controlador: de E => B (el controlador de errores) y de A => B (el controlador de éxito). Si myAppLogic falla, habrá un 1; si tiene éxito, habrá un 0.

Si la aplicación que queremos hacer es una aplicación existente, utilizando inyección de dependencia o no controla su función principal, entonces podemos crear un sistema para ejecutar sus programas ZIO:

import zio._


object IntegrationExample {

  val runtime = Runtime.default


  Unsafe.unsafe { implicit unsafe =>

    runtime.unsafe.run(ZIO.attempt(println("Hello World!"))).getOrThrowFiberFailure()

  }

}

Idealmente, la aplicación debería tener un solo runtime, porque cada runtime tiene sus propios recursos (incluido el grupo de subprocesos y el informador de errores no controlados).

ZIO proporciona un módulo para interactuar con la consola. Si necesita imprimir texto en la consola, puede usar print e printLine:

import zio._


// Print without trailing line break

Console.print("Hello World")

// Print string and include trailing line break

Console.printLine("Hello World")


Si necesita leer la entrada desde la consola, puede usar readLine:

import zio._

val echo = Console.readLine.flatMap(line => Console.printLine(line))



viernes, 12 de agosto de 2022

Creando un web service REST con ZIO


 Vamos hacer un pequeño proyecto con ZIO, como para empezar. El "hola mundo" de toda la vida pero en un servicio REST. 

Antes de empezar vamos a hacer un proyecto con scala 3 con sbt : 


sbt new scala/scala3.g8


Luego agregamos las dependencias de zio, el build.sbt debe quedar así : 


scalaVersion := "3.1.2"

organization := "dev.zio"

name         := "zio-quickstart-restful-webservice"


libraryDependencies ++= Seq(

  "dev.zio"       %% "zio"            % "2.0.0",

  "dev.zio"       %% "zio-json"       % "0.3.0-RC10",

  "io.d11"        %% "zhttp"        % "2.0.0-RC10",

  "io.getquill"   %% "quill-zio"    % "4.2.0",

  "io.getquill"   %% "quill-jdbc-zio" % "4.2.0",

  "com.h2database" % "h2"             % "2.1.214"

)


Aclaro que tengo otras dependencias como la de base de dato que las voy a usar en proximos ejemplos. 

Y ahore si, hacemos el web service para nuestro "hola mundo" : 


package dev.zio.quickstart.greet


import zhttp.http._


object GreetingApp {

  def apply(): Http[Any, Nothing, Request, Response] =

    Http.collect[Request] {

      // GET /greet?name=:name

      case req@(Method.GET -> !! / "greet") if (req.url.queryParams.nonEmpty) =>

        Response.text(s"Hello ${req.url.queryParams("name").mkString(" and ")}!")


      // GET /greet

      case Method.GET -> !! / "greet" =>

        Response.text(s"Hello World!")


      // GET /greet/:name

      case Method.GET -> !! / "greet" / name =>

        Response.text(s"Hello $name!")

    }

}



Luego programamos el Main :


package dev.zio.quickstart


import dev.zio.quickstart.counter.CounterApp

import dev.zio.quickstart.download.DownloadApp

import dev.zio.quickstart.greet.GreetingApp

import dev.zio.quickstart.users.{InmemoryUserRepo, PersistentUserRepo, UserApp}

import zhttp.service.Server

import zio._


object MainApp extends ZIOAppDefault {

  def run =

    Server.start(

      port = 8999,

      http = GreetingApp() 

    ).provide(

      // An layer responsible for storing the state of the `counterApp`

      ZLayer.fromZIO(Ref.make(0)),

      

      // To use the persistence layer, provide the `PersistentUserRepo.layer` layer instead

      InmemoryUserRepo.layer

    )

}


Y listo!! Si vamos a http://localhost:8999/greet?name=Emanuel me va a saludar. 

Para correrlo lo pueden hacer con sbt run o con intellij corriendo el Main. 


Dejo el repo : 

https://github.com/emanuelpeg/zio-quickstart-restful-webservice

lunes, 8 de agosto de 2022

Primeros pasos con ZIO


Vamos hacer un pequeño proyecto con ZIO, como para empezar. El "hola mundo" de toda la vida. 

Antes de empezar vamos a hacer un proyecto con scala 3 con sbt : 

sbt new scala/scala3.g8

Luego agregamos las dependencias de zio, en este caso solo usaremos : 


libraryDependencies += "dev.zio" %% "zio" % "2.0.0"


De esta manera el archivo build.sbt será : 


val scala3Version = "3.1.3"


lazy val root = project

  .in(file("."))

  .settings(

    name := "zioHello",

    version := "0.1.0-SNAPSHOT",


    scalaVersion := scala3Version,

    libraryDependencies += "dev.zio" %% "zio" % "2.0.0",

    libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % Test

  )


Y luego vamos a hacer nuestro "hola mundo" en el archivo Main.scala : 


import zio._
import zio.Console._

object Main extends ZIOAppDefault :

  def run = myAppLogic

  val myAppLogic =
    for {
      _    <- printLine("Hola, como te llamas guapo?")
      nombre <- readLine
      _    <- printLine(s"Hola, ${nombre}, welcome to ZIO!")
    } yield ()


Y listo!! 


domingo, 7 de agosto de 2022

Programación Funcional con Scala y ZIO 2.0

Quiero compartirles un vídeo de scalac que esta muy bueno y nos da una primera mirada a ZIO :


lunes, 24 de febrero de 2020

ZIO

ZIO es una biblioteca Scala de dependencia cero para programación asincrónica y concurrente. Y tiene un logo muuuuyyyy copado...

Su filosofía esta basada en fibras (que son como hilos de ejecución) altamente escalables y sin bloqueo que nunca desperdician ni pierden recursos, ZIO permite crear aplicaciones escalables, resistentes y reactivas que satisfagan las necesidades de su negocio. Entre las características podemos nombrar:
  • Alto rendimiento. 
  • Tipe-safe. 
  • Concurrente. 
  • Asincrónico. 
  • Seguridad en los recursos. 
  • Probable 
  • Elástico. 
  • Funcional. 
Antes de empezar debemos incluir a ZIO en el proyecto agregando la siguiente entrade en el build.sbt:

libraryDependencies += "dev.zio" %% "zio" % "1.0.0-RC17"

Si deseamos utilizar streams ZIO, también debe incluir la siguiente dependencia:

libraryDependencies += "dev.zio" %% "zio-streams" % "1.0.0-RC17"

Ahora veamos un pequeño ejemplo: 

import zio.App
import zio.console._

object MyApp extends App {

  def run(args: List[String]) =
    myAppLogic.fold(_ => 1, _ => 0)

  val myAppLogic =
    for {
      _    <- putStrLn("Hello! What is your name?")
      name <- getStrLn
      _    <- putStrLn(s"Hello, ${name}, welcome to ZIO!")
    } yield ()
}

run debería devolver un valor ZIO que tenga todos sus errores manejados,

Como se puede ver myAppLogic, se plegó de tal forma que produce un valor si falla, habrá un 1; pero si tiene éxito, habrá un 0.

Si está integrando ZIO en una aplicación existente, utilizando inyección de dependencia, o no controla su función principal main, se puede crear un sistema de tiempo de ejecución para ejecutar sus programas ZIO:

import zio._
import zio.console._

object IntegrationExample {
  val runtime = new DefaultRuntime {}

  runtime.unsafeRun(putStrLn("Hello World!"))
}

ZIO proporciona un módulo para interactuar con la consola. Puede importar las funciones en este módulo con el siguiente fragmento de código:

import zio.console._

Si necesita imprimir texto en la consola, puede usar putStr y putStrLn:

// Print without trailing line break
putStr("Hello World")
// res8: ZIO[Console, Nothing, Unit] = zio.ZIO$Read@2ba3e0d1

// Print string and include trailing line break
putStrLn("Hello World")
// res9: ZIO[Console, Nothing, Unit] = zio.ZIO$Read@75e3acfe

Si necesita leer la entrada desde la consola, puede usar getStrLn:

val echo = getStrLn flatMap putStrLn
// echo: ZIO[Console, java.io.IOException, Unit] = zio.ZIO$FlatMap@72bc78be

Es importante utilizar la consola de zio porque cada fibra tiene sus propios recursos, y la consola de Zio soluciona la interacción con la consola. 

Dejo link : https://zio.dev/