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:
¡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:
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:
¡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:
Como vemos esta nueva versión de getName es algo más concisa, y por tanto nos quedaremos con ella.