Exit e-book
Show all chapters
16
Funcionalidad para obtener el nombre del jugador por consola
16. 
Funcionalidad para obtener el nombre del jugador por consola

Sign up to our Newsletter

Signing up to our newsletter allows you to read all our ebooks.

    Introducción a la Programación con Efectos Funcionales usando ZIO
    16

    Funcionalidad para obtener el nombre del jugador por consola

    Ahora que ya tenemos el esqueleto básico de nuestra aplicación, primeramente necesitamos escribir la funcionalidad para obtener el nombre del jugador por consola, para ello escribiremos una función auxiliar, dentro del objeto Hangman, que nos permita imprimir un mensaje cualquiera y a continuación solicite un texto al jugador:

    import zio.console._
    
    def getUserInput(message: String): ZIO[Console, IOException, String] = {
      putStrLn(message)
      getStrLn
    }

    Veamos con más detalle esta función:

    • Para imprimir el mensaje provisto por consola, usamos la función putStrLn incluida en el módulo zio.console. Esta función retorna un efecto del tipo ZIO[Console, Nothing, Unit], que es equivalente a URIO[Console, Unit], lo que significa que es un efecto que para ejecutarse requiere del módulo Console (que es un módulo estándar provisto por ZIO), que no puede fallar y que retorna un valor de tipo Unit.
    • Luego, para solicitar al usuario que introduzca un texto, usamos la función getStrLn incluida también en el módulo zio.console. Esta función retorna un efecto del tipo ZIO[Console, IOException, String], lo que significa que es un efecto que para ejecutarse requiere del módulo Console, que puede fallar con un error de tipo IOException y que retorna un valor de tipo String.

    ¡Y eso es todo! Bueno…en realidad hay un pequeño problema, y es que si llamáramos esta función desde el método run, nunca se mostraría un mensaje por consola y se pasaría directamente a solicitar un texto al usuario. ¿Qué es lo que está faltando entonces, si primero llamamos a putStrLn y luego a getStrLn? El problema es que tanto putStrLn como getStrLn retornan simples descripciones de interacciones con el mundo exterior (llamadas efectos funcionales), y si nos fijamos detenidamente: ¿estamos haciendo algo realmente con el efecto funcional retornado por putStrLn? La respuesta es que no, simplemente lo estamos descartando, y el único efecto que es retornado por nuestra función getUserInput es el efecto retornado por getStrLn. Es más o menos como si tuviéramos una función así:

    def foo(): Int = {
      4
      3
    }

    ¿Estamos haciendo algo con el 4 en esa función? ¡Nada! Simplemente es como si no estuviera ahí, y lo mismo pasa en nuestra función getUserInput, es como si la llamada a putStrLn no estuviera ahí.

    Por lo tanto, tenemos que modificar la función getUserInput para que el efecto retornado por putStrLn sea realmente usado:

    def getUserInput(message: String): ZIO[Console, IOException, String] =
      putStrLn(message).flatMap(_ => getStrLn)

    Lo que hace esta nueva versión es retornar un efecto que combina en forma secuencial los efectos retornados por putStrLn y getStrLn, usando el operador ZIO#flatMap, el cual recibe una función donde:

    • La entrada es el resultado del primer efecto (en este caso putStrLn, que retorna Unit).
    • Retorna un nuevo efecto a ser ejecutado, en este caso getStrLn.
    • Algo importante del funcionamiento de ZIO#flatMap es que, si el primer efecto falla, el segundo efecto no es ejecutado.

    De esta forma, ya no estamos descartando el efecto producido por putStrLn.

    Ahora bien, gracias a que el tipo de datos ZIO también ofrece un método ZIO#map, podemos escribir getUserInput usando una comprensión for, lo cual nos ayuda a visualizar el código en una forma más parecida a como luce un típico código imperativo:

    def getUserInput(message: String): ZIO[Console, IOException, String] =
      for {
        _     <- putStrLn(message)
        input <- getStrLn
      } yield input

    Esta implementación funciona perfectamente, sin embargo podemos escribirla de otra forma:

    def getUserInput(message: String): ZIO[Console, IOException, String] =
      (putStrLn(message) <*> getStrLn).map(_._2)

    En este caso, estamos usando otro operador para combinar dos efectos de manera secuencial, y es el operador <*> (que por cierto es equivalente al método ZIO#zip). Este operador, así como ZIO#flatMap, combina los resultados de dos efectos, con la diferencia de que el segundo efecto no necesita del resultado del primero para ejecutarse. El resultado de <*> es un efecto cuyo valor de retorno es una tupla, pero en este caso sólo necesitamos el valor de getStrLn (el resultado de putStrLn no nos interesa porque simplemente es un Unit), es por ello que ejecutamos ZIO#map, el cual es un método que nos permite transformar el resultado de un efecto, en este caso estamos obteniendo el segundo componente de la tupla retornada por <*>.

    Una versión mucho más simplificada y equivalente a la anterior es la siguiente:

    def getUserInput(message: String): ZIO[Console, IOException, String] =   
      putStrLn(message) *> getStrLn

    El operador *> (equivalente al método ZIO#zipRight), hace exactamente lo que hicimos en la anterior versión, pero de una forma mucho más resumida.

    Y bueno, ahora que ya tenemos una función para obtener un texto por parte del jugador, podemos implementar un efecto funcional para obtener específicamente su nombre:

    lazy val getName: ZIO[Console, IOException, Name] =
      for {
        input <- getUserInput("What's your name?")
        name <- Name.make(input) match {
                 case Some(name) => ZIO.succeed(name)
                 case None       => putStrLn("Invalid input. Please try again...") *> getName
               }
      } yield name

    Como podemos ver:

    • Primero se solicita al jugador que ingrese su nombre.
    • Luego se intenta construir un objeto Name, usando el método Name.make que hemos definido anteriormente.
      • Si el nombre introducido es válido (es decir no es vacío) retornamos un efecto que termina exitosamente con el nombre correspondiente, usando el método ZIO.succeed.
      • Caso contrario, mostramos un mensaje de error por pantalla (usando putStrLn) y a continuación volvemos a solicitar el nombre, llamando a getName recursivamente. Esto significa que el efecto getName se ejecutará tantas veces como sea necesario, mientras el nombre del jugador sea inválido.

    ¡Y eso es todo! Sin embargo, podemos escribir una versión equivalente:

    lazy val getName: ZIO[Console, IOException, Name] =
      for {
        input <- getUserInput("What's your name?")
        name  <- ZIO.fromOption(Name.make(input)) <> (putStrLn("Invalid input. Please try again...") *> getName)
      } yield name

    En esta versión, el resultado de llamar a Name.make, que es de tipo Option, es convertido en un efecto ZIO, usando el método ZIO.fromOption, el cual retorna un efecto que termina exitosamente si el Option dado es un Some, y un efecto que falla si el Option dado es un None. Luego, estamos usando un nuevo operador <> (que es equivalente al método ZIO#orElse), que también nos permite combinar dos efectos de forma secuencial, pero de una forma algo diferente a lo que ocurre con <*>. La lógica de <> es la siguiente:

    • Si el primer efecto es exitoso (en este caso si el nombre del jugador es válido), el segundo efecto no se ejecuta.
    • Si el primer efecto falla (en este caso si el nombre del jugador es inválido), se ejecuta el segundo efecto (en este caso, se muestra un mensaje de error y a continuación se vuelve a llamar a getName).

    Como vemos esta nueva versión de getName es algo más concisa, y por tanto nos quedaremos con ella.

    PREVIOUS
    Chapter
    15
    NEXT
    Chapter
    17