Capítulo 4. Estructuras de control

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Como su nombre indica, las estructuras de control proporcionan a los programadores una forma de controlar el flujo de un programa. Son una característica fundamental de los lenguajes de programación que te permiten manejar la toma de decisiones y las tareas en bucle.

Antes de aprender Scala, allá por 2010, pensaba que las estructuras de control como las sentencias if/then, junto con los bucles for y while, eran características relativamente aburridas de los lenguajes de programación, pero eso era sólo porque no sabía que había otra forma. Hoy en día sé que son una característica definitoria de los lenguajes de programación.

Las estructuras de control de Scala son

  • for bucles y expresiones for

  • if/then/else if expresiones

  • match expresiones (concordancia de patrones)

  • try/catch/finally bloques

  • while bucles

A continuación te presentaré brevemente cada una de ellas, y luego las recetas te mostrarán detalles adicionales sobre cómo utilizar sus funciones.

Bucles for y expresiones for

En su uso más básico, los bucles for proporcionan una forma de iterar sobre una colección para operar sobre los elementos de la misma:

for i <- List(1, 2, 3) do println(i)

Pero ése es sólo un caso de uso básico. Los bucles for también pueden tenerdeclaraciones ifcon guardias:

for
    i <- 1 to 10
    if i > 3
    if i < 6
do
    println(i)

Con el uso de la palabra clave yield, los bucles for también se convierten en expresiones for: bucles que producen un resultado:

val listOfInts = for
    i <- 1 to 10
    if i > 3
    if i < 6
yield
    i * 10

Después de que se ejecute ese bucle, listOfInts es un Vector(40, 50). Las guardas del interior del bucle filtran todos los valores excepto 4 y 5, y luego esos valores se multiplican por 10 en el bloque yield.

En las recetas iniciales de este capítulo encontrarás muchos más detalles sobre los bucles y expresiones de for.

Expresiones if/then/else-if

Mientras que los bucles y expresiones for te permiten recorrer una colección, las expresiones if/then/else proporcionan una forma de tomar decisiones de ramificación. En Scala 3 la sintaxis preferida ha cambiado, y ahora tiene este aspecto:

val absValue = if a < 0 then -a else a

def compare(a: Int, b: Int): Int =
    if a < b then
        -1
    else if a == b then
        0
    else
        1
end compare

Como se muestra en esos dos ejemplos, una expresión if es realmente una expresión que devuelve un valor. (Las expresiones se tratan en la Receta 4.5.)

Expresiones coincidentes y concordancia de patrones

A continuación, las expresiones match y la concordancia de patrones son una característica definitoria de Scala, y la demostración de sus capacidades ocupa la mayor parte de este capítulo. Al igual que las expresiones if, las expresiones match devuelven valores, por lo que puedes utilizarlas como cuerpo de un método. Como ejemplo, este método es similar a la versión del lenguaje de programación Perl de las expresiones true y false:

def isTrue(a: Matchable): Boolean = a match
    case false | 0 | "" => false
    case _ => true

En ese código, si isTrue recibe un 0 o una cadena vacía, devuelve false, de lo contrario devuelve true. Diez recetas de este capítulo sirven para detallar las características de las expresiones match.

Bloques try/catch/finally

A continuación, los bloques try/catch/finally de Scala son similares a los de Java, pero la sintaxis es ligeramente diferente, principalmente en que el bloque catch es coherente con una expresión match:

try
    // some exception-throwing code here
catch
    case e1: Exception1Type => // handle that exception
    case e2: Exception2Type => // handle that exception
finally
    // close your resources and do anything else necessary here

Al igual que if y match, try es una expresión que devuelve un valor, por lo que puedes escribir código como éste para transformar un String en un Int:

def toInt(s: String): Option[Int] =
    try
        Some(s.toInt)
    catch
        case e: NumberFormatException => None

Estos ejemplos muestran cómo funciona toInt:

toInt("1")    // Option[Int] = Some(1)
toInt("Yo")   // Option[Int] = None

La receta 4.16 proporciona más información sobre los bloques try/catch.

Bucles while

En lo que respecta a los bucles while, verás que se utilizan muy poco en Scala. Esto se debe a que los bucles while se utilizan sobre todo para efectos secundarios, como actualizarvariables mutables e imprimir con println, y éstas son cosas que también puedes hacer con los bucles for y el método foreach en colecciones. Dicho esto, si alguna vez necesitas utilizar uno, su sintaxis es la siguiente:

while
    i < 10
do
    println(i)
    i += 1

while se tratan brevemente en la Receta 4.1.

Por último, gracias a una combinación de varias características de Scala, puedes crear tus propias estructuras de control, y estas capacidades se tratan en la Receta 4.17.

Las estructuras de control como característica definitoria de los lenguajes de programación

A finales de 2020 tuve la suerte de co-escribir el Libro de Scala 3 en el sitio web oficial de Documentación de Scala, incluyendo estos trescapítulos:

Cuando antes dije que las estructuras de control son una "característica definitoria de los lenguajes de programación", una de las cosas a las que me refería es que, después de escribir esos capítulos, llegué a darme cuenta de la potencia de las características de este capítulo, así como de lo consistente que es Scala en comparación con otros lenguajes de programación. Esa coherencia es una de las características que hacen que sea un placer utilizar Scala.

4.1 Bucle sobre estructuras de datos con for

Problema

Quieres iterar sobre los elementos de una colección a la manera de un bucle tradicional for.

Solución

Hay muchas formas de hacer bucles sobre colecciones en Scala, como los bucles for, los bucles while y los métodos de colección como foreach, map, flatMap, etc. Esta solución se centra principalmente en el bucle for.

Dada una lista simple:

val fruits = List("apple", "banana", "orange")

puedes hacer un bucle sobre los elementos de la lista e imprimirlos así:

scala> for f <- fruits do println(f)
apple
banana
orange

Ese mismo enfoque funciona para todas las secuencias, incluidas List, Seq, Vector, Array, ArrayBuffer, etc.

Cuando tu algoritmo requiera varias líneas, utiliza la misma sintaxis de bucle for, y realiza tu trabajo en un bloque dentro de llaves:

scala> for f <- fruits do
     |      // imagine this requires multiple lines
     |      val s = f.toUpperCase
     |      println(s)
APPLE
BANANA
ORANGE

contadores de bucle for

Si necesitas acceder a un contador dentro de un bucle for, utiliza uno de los siguientes enfoques. En primer lugar, puedes acceder a los elementos de una secuencia con contador de la siguiente manera:

for i <- 0 until fruits.length do
    println(s"$i is ${fruits(i)}")

Ese bucle produce este resultado:

0 is apple
1 is banana
2 is orange

Rara vez necesitarás acceder a los elementos de la secuencia por su índice, pero cuando lo hagas, ése es un enfoque posible. Las colecciones Scala también ofrecen un método zipWithIndex que puedes utilizar para crear un contador de bucle:

for (fruit, index) <- fruits.zipWithIndex do
    println(s"$index is $fruit")

Su salida es:

0 is apple
1 is banana
2 is orange

Generadores

En una nota relacionada, el siguiente ejemplo muestra cómo utilizar un Range para ejecutar un bucle tres veces:

scala> for i <- 1 to 3 do println(i)
1
2
3

La parte 1 to 3 del bucle crea un Range, como se muestra en el REPL:

scala> 1 to 3
res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3)

Utilizar un Range como éste se conoce como utilizar un generador. La receta 4.2 muestra cómo utilizar esta técnica para crear varios contadores de bucle.

Hacer un bucle sobre un mapa

Al iterar sobre claves y valores en Map, considero que éste es el bucle for más conciso y legible:

val names = Map(
    "firstName" -> "Robert",
    "lastName" -> "Goren"
)

for (k,v) <- names do println(s"key: $k, value: $v")

El REPL muestra su salida:

scala> for (k,v) <- names do println(s"key: $k, value: $v")
key: firstName, value: Robert
key: lastName, value: Goren

Debate

Como he cambiado a un estilo de programación funcional, no he utilizado un bucle while en varios años, pero el REPL demuestra cómo funciona:

scala> var i = 0
i: Int = 0

scala> while i < 3 do
     |     println(i)
     |     i += 1
0
1
2

while Los bucles se utilizan generalmente para efectos secundarios, como actualizar una variable mutable como i y escribir la salida al mundo exterior. A medida que mi código se acerca más a la programación funcional pura -donde no hay estado mutable- no he tenido necesidad de ellos.

Dicho esto, cuando programes en un estilo de programación orientado a objetos, los bucles while se siguen utilizando con frecuencia, y ese ejemplo demuestra su sintaxis. Un bucle while también puede escribirse en varias líneas, así:

while
    i < 10
do
    println(i)
    i += 1

Métodos de recogida como foreach

En cierto modo, Scala me recuerda el eslogan de Perl: "Hay más de una forma de hacerlo", e iterar sobre una colección proporciona magníficos ejemplos de ello. Con la gran cantidad de métodos disponibles sobre colecciones, es importante tener en cuenta que un bucle for puede incluso no ser el mejor enfoque para un problema concreto; los métodos foreach, map, flatMap, collect, reduce, etc., pueden utilizarse a menudo para resolver tu problema sin necesidad de un bucle for explícito.

Por ejemplo, cuando trabajas con una colección, también puedes iterar sobre cada elemento llamando al método foreach de la colección:

scala> fruits.foreach(println)
apple
banana
orange

Cuando tengas un algoritmo que quieras ejecutar en cada elemento de la colección, sólo tienes que pasar la función anónima a foreach:

scala> fruits.foreach(e => println(e.toUpperCase))
APPLE
BANANA
ORANGE

Al igual que con el bucle for, si tu algoritmo requiere varias líneas, realiza tu trabajo en un bloque:

scala> fruits.foreach { e =>
     |     val s = e.toUpperCase
     |     println(s)
     | }
APPLE
BANANA
ORANGE

Ver también

La teoría que subyace al funcionamiento de los bucles for es muy interesante, y conocerla puede ser útil a medida que progresas. Escribí sobre ello extensamente en estos artículos:

4.2 Utilizar bucles for con varios contadores

Problema

Quieres crear un bucle con varios contadores, como cuando iteras sobre una matriz multidimensional.

Solución

Puedes crear un bucle for con dos contadores así:

scala> for i <- 1 to 2; j <- 1 to 2 do println(s"i = $i, j = $j")
i = 1, j = 1
i = 1, j = 2
i = 2, j = 1
i = 2, j = 2

Observa que establece i en 1, recorre los elementos de j, luego establece i en 2 y repite el proceso.

Utilizar ese enfoque funciona bien con ejemplos pequeños, pero cuando tu código se hace más grande, éste es el estilo preferido:

for
    i <- 1 to 3
    j <- 1 to 5
    k <- 1 to 10 by 2
do
    println(s"i = $i, j = $j, k = $k")

Este enfoque es útil cuando se realiza un bucle sobre una matriz multidimensional. Suponiendo que creas y rellenas una pequeña matriz bidimensional como ésta:

val a = Array.ofDim[Int](2,2)
a(0)(0) = 0
a(0)(1) = 1
a(1)(0) = 2
a(1)(1) = 3

puedes imprimir cada elemento de la matriz así

scala> for
     |     i <- 0 to 1
     |     j <- 0 to 1
     | do
     |     println(s"($i)($j) = ${a(i)(j)}")
(0)(0) = 0
(0)(1) = 1
(1)(0) = 2
(1)(1) = 3

Debate

Como se muestra en la Receta 15.2, "Crear rangos", la sintaxis 1 to 5 crea un Range:

scala> 1 to 5
val res0: scala.collection.immutable.Range.Inclusive = Range 1 to 5

Los rangos son estupendos para muchos propósitos, y los rangos creados con el símbolo <- en los bucles for se denominan generadores. Como se muestra, puedes utilizar fácilmente varios generadores en un bucle.

4.3 Utilizar un bucle for con sentencias if incrustadas (Guardias)

Problema

Quieres añadir una o varias cláusulas condicionales a un bucle for, normalmente para filtrar algunos elementos de una colección mientras trabajas con los demás.

Solución

Añade una o más frases if después de tu generador, así:

for
    i <- 1 to 10
    if i % 2 == 0
do
    print(s"$i ")

// output: 2 4 6 8 10

Estas sentencias if se denominan filtros, expresiones de filtro o guardas, y puedes utilizar tantas guardas como sean necesarias para el problema en cuestión. Este bucle muestra una forma difícil de imprimir el número 4:

for
    i <- 1 to 10
    if i > 3
    if i < 6
    if i % 2 == 0
do
    println(i)

Debate

Todavía es posible escribir bucles for con expresiones if al estilo antiguo. Por ejemplo, dado este código

import java.io.File
val dir = File(".")
val files: Array[java.io.File] = dir.listFiles()

podrías, en teoría, escribir un bucle for con un estilo como éste, que recuerda a C y Java:

// a C/Java style of writing a 'for' loop
for (file <- files) {
    if (file.isFile && file.getName.endsWith(".scala")) {
        println(s"Scala file: $file")
    }
}

Sin embargo, una vez que te sientas cómodo con la sintaxis de bucle for de Scala, creo que descubrirás que hace que el código sea más legible, porque separa las preocupaciones de bucle y filtrado de la lógica empresarial:

for
    // loop and filter
    file <- files
    if file.isFile
    if file.getName.endsWith(".scala")
do
    // as much business logic here as needed
    println(s"Scala file: $file")

Ten en cuenta que, como los guardianes suelen estar pensados para filtrar colecciones, puede que quieras utilizar uno de los muchos métodos de filtrado de que disponen las colecciones -filter, take, drop, etc.- en lugar de un bucle for, dependiendo de tus necesidades. Consulta el Capítulo 11 para ver ejemplos de esos métodos.

4.4 Crear una nueva colección a partir de una colección existente con for/yield

Problema

Quieres crear una nueva colección a partir de una colección existente, aplicando un algoritmo (y potencialmente una o varias guardas) a cada elemento de la colección original.

Solución

Utiliza una sentencia yield con un bucle for para crear una nueva colección a partir de una colección existente. Por ejemplo, dada una matriz de cadenas en minúsculas:

scala> val names = List("chris", "ed", "maurice")
val names: List[String] = List(chris, ed, maurice)

puedes crear una nueva matriz de cadenas en mayúsculas combinando yield con un bucle for y un algoritmo sencillo:

scala> val capNames = for name <- names yield name.capitalize
val capNames: List[String] = List(Chris, Ed, Maurice)

El uso de un bucle for con una sentencia yield se conoce como for-comprensión.

Si tu algoritmo requiere varias líneas de código, realiza el trabajo en un bloque después de la palabra clave yield, especificando manualmente el tipo de la variable resultante, o no:

// [1] declare the type of `lengths`
val lengths: List[Int] = for name <- names yield
    // imagine that this body requires multiple lines of code
    name.length

// [2] don’t declare the type of `lengths`
val lengths = for name <- names yield
    // imagine that this body requires multiple lines of code
    name.length

Ambos enfoques dan el mismo resultado:

List[Int] = List(5, 2, 7)

Las dos partes de tu comprensión for (también conocida como expresión for ) pueden ser tan complicadas como sea necesario. He aquí un ejemplo más amplio:

val xs = List(1,2,3)
val ys = List(4,5,6)
val zs = List(7,8,9)

val a = for
    x <- xs
    if x > 2
    y <- ys
    z <- zs
    if y * z < 45
yield
    val b = x + y
    val c = b * z
    c

Esa comprensión de for arroja el siguiente resultado:

a: List[Int] = List(49, 56, 63, 56, 64, 63)

Una comprensión de for puede ser incluso el cuerpo completo de un método:

def between3and10(xs: List[Int]): List[Int] =
    for
        x <- xs
        if x >= 3
        if x <= 10
    yield x

between3and10(List(1,3,7,11))   // List(3, 7)

Debate

Si eres nuevo en el uso de yield con un bucle for, puede ayudarte pensar en el bucle así:

  1. Cuando comienza a ejecutarse, el bucle for/yield crea inmediatamente una nueva colección vacía que es del mismo tipo que la colección de entrada. Por ejemplo, si el tipo de entrada es un Vector, el tipo de salida también será un Vector. Puedes pensar en esta nueva colección como si fuera un cubo vacío.

  2. En cada iteración del bucle for, se puede crear un nuevo elemento de salida a partir del elemento actual de la colección de entrada. Cuando se crea el elemento de salida, se coloca en el cubo.

  3. Cuando el bucle termina de ejecutarse, se devuelve todo el contenido del cubo.

Es una simplificación, pero me parece útil para explicar el proceso.

Ten en cuenta que escribir una expresión for sin una guarda es como llamar al método map en una colección.

Por ejemplo, la siguiente comprensión de for convierte todas las cadenas de la colección fruits a mayúsculas:

scala> val namesUpper = for n <- names yield n.toUpperCase
val namesUpper: List[String] = List(CHRIS, ED, MAURICE)

Llamar al método map sobre la colección hace lo mismo:

scala> val namesUpper = names.map(_.toUpperCase)
val namesUpper: List[String] = List(CHRIS, ED, MAURICE)

Cuando empecé a aprender Scala, escribía todo mi código utilizando expresiones for/yield, hasta que un día me di cuenta de que utilizar for/yield sin una guarda era lo mismo que utilizar map.

Ver también

4.5 Utilizar la construcción if como un operador ternario

Problema

Ya conoces la sintaxis especial de los operadores ternarios de Java:

int absValue = (a < 0) ? -a : a;

y te gustaría saber cuál es el equivalente en Scala.

Solución

Se trata de un problema un poco tramposo, porque a diferencia de Java, en Scala no existe un operador ternario especial; basta con utilizar una expresión if/else/then:

val a = 1
val absValue = if a < 0 then -a else a

Como una expresión if devuelve un valor, puedes incrustarla en una sentencia print:

println(if a == 0 then "a" else "b")

También puedes utilizarla en otra expresión, como esta parte de un método hashCode:

hash = hash * prime + (if name == null then 0 else name.hashCode)

El hecho de que las expresiones if/else devuelvan un valor también te permite escribir métodos concisos:

// Version 1: one-line style
def abs(x: Int) = if x >= 0 then x else -x
def max(a: Int, b: Int) = if a > b then a else b

// Version 2: the method body on a separate line, if you prefer
def abs(x: Int) =
    if x >= 0 then x else -x

def max(a: Int, b: Int) =
    if a > b then a else b

Debate

La página de documentación de Java "Operadores de igualdad, relacionales y condicionales" afirma que el operador condicional de Java ?: "se conoce como operador ternario porque utiliza tres operandos".

Java requiere aquí una sintaxis separada porque la construcción if/else de Java es una expresión; no tiene valor de retorno y sólo se utiliza para efectos secundarios, como la actualización de campos mutables. En cambio, como if/else/then de Scala es realmente una expresión, no se necesita un operador especial. Consulta la Receta 24.3, "Escribir expresiones (en lugar de sentencias)", para más detalles sobre sentencias y expresiones.

Arity

La palabra ternario tiene que ver con la aridad de las funciones. La página "Aridad" de Wikipedia dice: "En lógica, matemáticas e informática, la aridad de una función u operación es el número de argumentos u operandos que toma la función". Un operador unario toma un operando, un operador binario toma dos operandos y un operador ternario toma tres operandos.

4.6 Utilizar una expresión coincidente como una sentencia switch

Problema

Te encuentras en una situación en la que quieres crear algo parecido a una simple sentencia Java switch basada en números enteros, como por ejemplo hacer coincidir los días de una semana, los meses de un año y otras situaciones en las que un número entero se asigna a un resultado.

Solución

Para utilizar una expresión match de Scala como una simple sentencia switch basada en enteros, utiliza este enfoque:

import scala.annotation.switch

// `i` is an integer
(i: @switch) match
    case 0 => println("Sunday")
    case 1 => println("Monday")
    case 2 => println("Tuesday")
    case 3 => println("Wednesday")
    case 4 => println("Thursday")
    case 5 => println("Friday")
    case 6 => println("Saturday")
    // catch the default with a variable so you can print it
    case whoa  => println(s"Unexpected case: ${whoa.toString}")

Ese ejemplo muestra cómo producir una acción de efecto secundario (println) basada en una coincidencia. Un enfoque más funcional es devolver un valor de una expresión match:

import scala.annotation.switch

// `i` is an integer
val day = (i: @switch) match
    case 0 => "Sunday"
    case 1 => "Monday"
    case 2 => "Tuesday"
    case 3 => "Wednesday"
    case 4 => "Thursday"
    case 5 => "Friday"
    case 6 => "Saturday"
    case _ => "invalid day"   // the default, catch-all

La anotación @switch

Al escribir expresiones match sencillas como ésta, se recomienda utilizar la anotación @switch, como se muestra. Esta anotación proporciona una advertencia en tiempo de compilación si el conmutador no se puede compilar a un tableswitch o lookupswitch. Compilar tu expresión coincidente a un tableswitch o lookupswitch es mejor para el rendimiento porque da como resultado una tabla de ramas en lugar de un árbol de decisión. Cuando se da un valor a la expresión, ésta puede saltar directamente al resultado en lugar de trabajar a través del árbol de decisión.

Enla documentación de la anotación Scala @switch se indica:

Si [esta anotación está] presente, el compilador verificará que la coincidencia se ha compilado en un conmutador de tablas o un conmutador de búsqueda, y emitirá un error si en su lugar se compila en una serie de expresiones condicionales

El efecto de la anotación @switch se demuestra con un sencillo ejemplo. Primero, coloca el siguiente código en un archivo llamado SwitchDemo.scala:

// Version 1 - compiles to a tableswitch
import scala.annotation.switch

class SwitchDemo:
    val i = 1
    val x = (i: @switch) match
        case 1 => "One"
        case 2 => "Two"
        case 3 => "Three"
        case _ => "Other"

Luego compila el código como de costumbre:

$ scalac SwitchDemo.scala

La compilación de esta clase no produce advertencias y crea el archivo de salida SwitchDemo.class. A continuación, desensambla ese archivo con este comando javap:

$ javap -c SwitchDemo

La salida de este comando muestra un tableswitch, como éste:

16: tableswitch   { // 1 to 3
             1: 44
             2: 52
             3: 60
       default: 68
  }

Esto demuestra que Scala fue capaz de optimizar tu expresión match a una tableswitch. (Esto es algo bueno.)

A continuación, realiza un pequeño cambio en el código, sustituyendo el literal entero 1 por un valor:

import scala.annotation.switch

// Version 2 - leads to a compiler warning
class SwitchDemo:
    val i = 1
    val one = 1               // added
    val x = (i: @switch) match
        case one  => "One"    // replaced the '1'
        case 2    => "Two"
        case 3    => "Three"
        case _    => "Other"

De nuevo, compila el código con scalac, pero enseguida verás un mensaje de advertencia:

$ scalac SwitchDemo.scala
SwitchDemo.scala:7: warning: could not emit switch for @switch annotated match
  val x = (i: @switch) match {
               ^
one warning found

Este mensaje de advertencia significa que no se ha podido generar ni un tableswitch ni un lookupswitch para la expresión match. Puedes confirmarlo ejecutando el comando javap en el archivo SwitchDemo.class que se generó. Cuando mires esa salida, verás que el tableswitch que aparecía en el ejemplo anterior ha desaparecido.

En su libro, Scala in Depth (Manning), Joshua Suereth afirma que deben cumplirse las siguientes condiciones para que Scala aplique la optimización tableswitch:

  • El valor coincidente debe ser un número entero conocido.

  • La expresión coincidente debe ser "simple". No puede contener comprobaciones de tipo, declaraciones if ni extractores.

  • La expresión debe tener su valor disponible en tiempo de compilación.

  • Debe haber más de dos declaraciones case.

Debate

Los ejemplos de la Solución mostraban las dos formas en que puedes tratar el caso "atrapar todo" por defecto. En primer lugar, si no te preocupa el valor de la coincidencia por defecto, puedes atraparlo con el comodín _:

case _ => println("Got a default match")

Por el contrario, si te interesa lo que cayó en la coincidencia por defecto, asígnale un nombre de variable. Luego puedes utilizar esa variable en el lado derecho de la expresión:

case default => println(default)

Utilizar un nombre como default suele ser lo más lógico, pero puedes utilizar cualquier nombre legal para la variable:

case oops => println(oops)

Es importante saber que puedes generar un MatchError si no manejas el caso por defecto. Dada esta expresión match:

i match
    case 0 => println("0 received")
    case 1 => println("1 is good, too")

si i es un valor distinto de 0 o 1, la expresión lanza un MatchError:

scala.MatchError: 42 (of class java.lang.Integer)
  at .<init>(<console>:9)
  at .<clinit>(<console>)
    much more error output here ...

Así que, a menos que escribas intencionadamente una función parcial, querrás manejar el caso por defecto.

¿Realmente necesitas una expresión de coincidencia?

Ten en cuenta que puede que no necesites una expresión de correspondencia para ejemplos como éste. Por ejemplo, siempre que sólo estés asignando un valor a otro, puede ser preferible utilizar Map:

val days = Map(
    0 -> "Sunday",
    1 -> "Monday",
    2 -> "Tuesday",
    3 -> "Wednesday",
    4 -> "Thursday",
    5 -> "Friday",
    6 -> "Saturday"
)

println(days(0))   // prints "Sunday"

Ver también

4.7 Hacer coincidir varias condiciones conuna sentencia Case

Problema

Tienes una situación en la que varias condiciones de match requieren que se ejecute la misma lógica empresarial, y en lugar de repetir tu lógica empresarial para cada caso, te gustaría utilizar una copia de la lógica empresarial para las condiciones coincidentes.

Solución

Coloca las condiciones de coincidencia que invocan la misma lógica empresarial en una línea, separadas por el carácter | (pipa):

// `i` is an Int
i match
    case 1 | 3 | 5 | 7 | 9 => println("odd")
    case 2 | 4 | 6 | 8 | 10 => println("even")
    case _ => println("too big")

Esta misma sintaxis funciona con cadenas y otros tipos. Aquí tienes un ejemplo basado en una coincidencia de String:

val cmd = "stop"
cmd match
    case "start" | "go" => println("starting")
    case "stop" | "quit" | "exit" => println("stopping")
    case _ => println("doing nothing")

Este ejemplo muestra cómo hacer coincidir varios objetos en cada sentencia case:

enum Command:
    case Start, Go, Stop, Whoa

import Command.*
def executeCommand(cmd: Command): Unit = cmd match
    case Start | Go => println("start")
    case Stop | Whoa => println("stop")

Como se ha demostrado, la posibilidad de definir varias coincidencias posibles para cada sentencia case puede simplificar tu código.

Ver también

4.8 Asignar el resultado de una expresión coincidentea una variable

Problema

Quieres devolver un valor de una expresión match y asignarlo a una variable, o utilizar una expresión match como cuerpo de un método.

Solución

Para asignar el resultado de una expresión match a una variable, inserta la asignación de variable antes de la expresión, como con la variable evenOrOdd en este ejemplo:

val someNumber = scala.util.Random.nextInt()
val evenOrOdd = someNumber match
    case 1 | 3 | 5 | 7 | 9 => "odd"
    case 2 | 4 | 6 | 8 | 10 => "even"
    case _ => "other"

Este enfoque se utiliza habitualmente para crear métodos o funciones cortos. Por ejemplo, el siguiente método implementa las definiciones Perl de true y false:

def isTrue(a: Matchable): Boolean = a match
    case false | 0 | "" => false
    case _ => true

Debate

Puede que hayas oído que Scala es un lenguaje de programación orientado a expresiones (EOP). EOP significa que cada construcción es una expresión, devuelve un valor y no tiene efectos secundarios. A diferencia de otros lenguajes, en Scala cada construcción como if, match, for, y try devuelve un valor. Consulta la Receta 24.3, "Escribir expresiones (en lugar de sentencias)", para más detalles.

4.9 Acceder al valor del caso por defecto en unaexpresión coincidente

Problema

Quieres acceder al valor del caso "catch all" por defecto cuando utilizas una expresión coincidente, pero no puedes acceder al valor cuando lo haces coincidir con la sintaxis de comodín _.

Solución

En lugar de utilizar el carácter comodín _, asigna un nombre de variable al caso por defecto:

i match
    case 0 => println("1")
    case 1 => println("2")
    case default => println(s"You gave me: $default")

Si asignas un nombre de variable a la coincidencia por defecto, podrás acceder a la variable en el lado derecho de la expresión.

Debate

La clave de esta receta está en utilizar un nombre de variable para la coincidencia por defecto en lugar del habitual carácter comodín _. El nombre que asignes puede ser cualquier nombre de variable legal, así que en lugar de llamarla default, puedes llamarla de otra forma, como what:

i match
    case 0 => println("1")
    case 1 => println("2")
    case what => println(s"You gave me: $what" )

Es importante proporcionar una coincidencia por defecto. No hacerlo puede provocar un MatchError:

scala> 3 match
     |     case 1 => println("one")
     |     case 2 => println("two")
     |     // no default match
scala.MatchError: 3 (of class java.lang.Integer)
many more lines of output ...

Consulta la Discusión de la Receta 4.6 para más detalles MatchError.

4.10 Utilizar la concordancia de patrones en las expresiones coincidentes

Problema

Tienes que hacer coincidir uno o varios patrones en una expresión match, y el patrón puede ser un patrón constante, un patrón variable, un patrón constructor, un patrón secuencia, un patrón tupla o un patrón tipo.

Solución

Define una expresión case para cada patrón que quieras que coincida. El método siguiente muestra ejemplos de los distintos tipos de patrones que puedes utilizar en las expresiones match:

def test(x: Matchable): String = x match

    // constant patterns
    case 0 => "zero"
    case true => "true"
    case "hello" => "you said 'hello'"
    case Nil => "an empty List"

    // sequence patterns
    case List(0, _, _) => "a 3-element list with 0 as the first element"
    case List(1, _*) => "list, starts with 1, has any number of elements"

    // tuples
    case (a, b) => s"got $a and $b"
    case (a, b, c) => s"got $a, $b, and $c"

    // constructor patterns
    case Person(first, "Alexander") => s"Alexander, first name = $first"
    case Dog("Zeus") => "found a dog named Zeus"

    // typed patterns
    case s: String => s"got a string: $s"
    case i: Int => s"got an int: $i"
    case f: Float => s"got a float: $f"
    case a: Array[Int] => s"array of int: ${a.mkString(",")}"
    case as: Array[String] => s"string array: ${as.mkString(",")}"
    case d: Dog => s"dog: ${d.name}"
    case list: List[_] => s"got a List: $list"
    case m: Map[_, _] => m.toString

    // the default wildcard pattern
    case _ => "Unknown"

end test

La gran expresión match de este método muestra las distintas categorías de patrones descritas en el libro Programar en Scala, incluidos los patrones de constantes, patrones de secuencias, patrones de tuplas, patrones de constructores y patrones de tipos.

El código siguiente muestra todos los casos de la expresión match, con la salida de cada expresión mostrada en los comentarios. Observa que el método println se renombra en la importación para que los ejemplos sean más concisos:

import System.out.{println => p}

case class Person(firstName: String, lastName: String)
case class Dog(name: String)

// trigger the constant patterns
p(test(0))               // zero
p(test(true))            // true
p(test("hello"))         // you said 'hello'
p(test(Nil))             // an empty List

// trigger the sequence patterns
p(test(List(0,1,2)))     // a 3-element list with 0 as the first element
p(test(List(1,2)))       // list, starts with 1, has any number of elements
p(test(List(1,2,3)))     // list, starts with 1, has any number of elements
p(test(Vector(1,2,3)))   // vector, starts w/ 1, has any number of elements

// trigger the tuple patterns
p(test((1,2)))                            // got 1 and 2
p(test((1,2,3)))                          // got 1, 2, and 3

// trigger the constructor patterns
p(test(Person("Melissa", "Alexander")))   // Alexander, first name = Melissa
p(test(Dog("Zeus")))                      // found a dog named Zeus

// trigger the typed patterns
p(test("Hello, world"))                   // got a string: Hello, world
p(test(42))                               // got an int: 42
p(test(42F))                              // got a float: 42.0
p(test(Array(1,2,3)))                     // array of int: 1,2,3
p(test(Array("coffee", "apple pie")))     // string array: coffee,apple pie
p(test(Dog("Fido")))                      // dog: Fido
p(test(List("apple", "banana")))          // got a List: List(apple, banana)
p(test(Map(1->"Al", 2->"Alexander")))     // Map(1 -> Al, 2 -> Alexander)

// trigger the wildcard pattern
p(test("33d"))                            // you gave me this string: 33d

Observa que en la expresión match, las expresiones List y Map que se escribieron así:

case m: Map[_, _] => m.toString
case list: List[_] => s"thanks for the List: $list"

podría haberse escrito así:

case m: Map[A, B] => m.toString
case list: List[X] => s"thanks for the List: $list"

Prefiero la sintaxis del guión bajo porque deja claro que no me interesa lo que se almacena en List o Map. En realidad, hay veces que me puede interesar lo que se almacena en List o Map, pero debido al borrado de tipos en la JVM, eso se convierte en un problema difícil.

Tipo Borrado

Cuando escribí por primera vez este ejemplo, escribí la expresión List dela siguiente manera:

case l: List[Int] => "List"

Si estás familiarizado con el borrado de tipos en la plataforma Java, sabrás que esto no funcionará. El compilador de Scala te avisa amablemente de este problema con este mensaje de advertencia:

Test1.scala:7: warning: non-variable type argument Int in
type pattern List[Int] is unchecked since it is eliminated
by erasure case l: List[Int] => "List[Int]"
                   ^

Si no estás familiarizado con el borrado de tipos, he incluido un enlace en la sección Ver también de esta receta a una página que describe cómo funciona en la JVM.

Debate

Normalmente, al utilizar esta técnica, tu método esperará una instancia que herede de una clase base o rasgo, y entonces tus sentencias case harán referencia a subtipos de ese tipo base. Esto se dedujo en el método test, donde cada tipo Scala es un subtipo de Matchable. El código siguiente muestra un ejemplo más obvio.

En mi aplicación Blue Parrot, que reproduce un archivo de sonido o "habla" el texto que se le da a intervalos de tiempo aleatorios, tengo un método parecido a éste:

import java.io.File

sealed trait RandomThing

case class RandomFile(f: File) extends RandomThing
case class RandomString(s: String) extends RandomThing

class RandomNoiseMaker:
    def makeRandomNoise(thing: RandomThing) = thing match
        case RandomFile(f) => playSoundFile(f)
        case RandomString(s) => speakText(s)

Se declara que el método makeRandomNoise toma un tipo RandomThing, y luego la expresión match maneja sus dos subtipos, RandomFile y RandomString.

Patrones

La gran expresión match de la Solución muestra una serie de patrones definidos en el libro Programming in Scala (coescrito por Martin Odersky, creador del lenguaje Scala). Los patrones incluyen:

  • Patrones constantes

  • Patrones variables

  • Patrones constructores

  • Patrones de secuencia

  • Patrones de tupla

  • Patrones mecanografiados

  • Patrones de enlace variable

Estas pautas se describen brevemente en los párrafos siguientes.

Patrones constantes

Un patrón constante sólo puede coincidir consigo mismo. Se puede utilizar cualquier literal como constante. Si especificas un 0 como literal, sólo coincidirá un valor de Int 0 . Algunos ejemplos son:

case 0 => "zero"
case true => "true"
Patrones variables

Esto no se mostró en el ejemplo de coincidencia grande de la Solución, pero un patrón de variable coincide con cualquier objeto, igual que el carácter comodín _. Scala vincula la variable al objeto que sea, lo que te permite utilizar la variable a la derecha de la expresión case. Por ejemplo, al final de una expresión match puedes utilizar el carácter comodín _ de este modo para atrapar cualquier otra cosa:

case _ => s"Hmm, you gave me something ..."

Pero con un patrón variable puedes escribir esto en su lugar:

case foo => s"Hmm, you gave me a $foo"

Para más información, consulta la Receta 4.9.

Patrones constructores

El patrón construc tor te permite hacer coincidir un constructor en una sentencia case. Como se muestra en los ejemplos, puedes especificar constantes o patrones de variables según sea necesario en el patrón constructor:

case Person(first, "Alexander") => s"found an Alexander, first name = $first"
case Dog("Zeus") => "found a dog named Zeus"
Patrones de secuencia

Puedes comparar secuencias como List, Array, Vector, etc. Utiliza el carácter _ para representar un elemento de la secuencia, y utiliza _* para representar cero o más elementos, como se muestra en los ejemplos:

case List(0, _, _) => "a 3-element list with 0 as the first element"
case List(1, _*) => "list, starts with 1, has any number of elements"
case Vector(1, _*) => "vector, starts with 1, has any number of elements"
Patrones de tupla

Como se muestra en los ejemplos, puedes hacer coincidir patrones de tupla y acceder al valor de cada elemento de la tupla. También puedes utilizar el comodín _ si no te interesa el valor de un elemento:

case (a, b, c) => s"3-elem tuple, with values $a, $b, and $c"
case (a, b, c, _) => s"4-elem tuple: got $a, $b, and $c"
Patrones mecanografiados

En el siguiente ejemplo, str: String es un patrón tipado, y str es una variable de patrón:

case str: String => s"you gave me this string: $str"

Como se muestra en los ejemplos, puedes acceder a la variable patrón en el lado derecho de la expresión después de declararla.

Patrones de enlace variable

A veces puedes querer añadir una variable a un patrón. Puedes hacerlo con la siguiente sintaxis general:

case variableName @ pattern => ...

Esto se denomina patrón de vinculación de variables. Cuando se utiliza, la variable de entrada a la expresión match se compara con el patrón y, si coincide, la variable de entrada se vincula a variableName.

La mejor forma de demostrar su utilidad es mostrando el problema que resuelve. Supón que tienes el patrón List que se ha mostrado antes:

case List(1, _*) => "a list beginning with 1, having any number of elements"

Como se ha demostrado, esto te permite hacer coincidir un List cuyo primer elemento es 1, pero hasta ahora no se ha accedido al List en el lado derecho de la expresión. Al acceder a un List, sabes que puedes hacerlo:

case list: List[_] => s"thanks for the List: $list"

por lo que parece que deberías intentarlo con un patrón secuencial:

case list: List(1, _*) => s"thanks for the List: $list"

Desgraciadamente, esto falla con el siguiente error del compilador:

Test2.scala:22: error: '=>' expected but '(' found.
    case list: List(1, _*) => s"thanks for the List: $list"
                   ^
one error found

La solución a este problema es añadir un patrón de enlace variable al patrón de secuencia:

case list @ List(1, _*) => s"$list"

Este código se compila, y funciona como se espera, dándote acceso a la List de la parte derecha de la declaración.

El código siguiente demuestra este ejemplo y la utilidad de este enfoque:

case class Person(firstName: String, lastName: String)

def matchType(x: Matchable): String = x match
    //case x: List(1, _*) => s"$x"   // doesn’t compile
    case x @ List(1, _*) => s"$x"    // prints the list

    //case Some(_) => "got a Some"   // works, but can’t access the Some
    //case Some(x) => s"$x"          // returns "foo"
    case x @ Some(_) => s"$x"        // returns "Some(foo)"

    case p @ Person(first, "Doe") => s"$p" // returns "Person(John,Doe)"
end matchType

@main def test2 =
    println(matchType(List(1,2,3)))             // prints "List(1, 2, 3)"
    println(matchType(Some("foo")))             // prints "Some(foo)"
    println(matchType(Person("John", "Doe")))   // prints "Person(John,Doe)"

En los dos ejemplos List dentro de la expresión match, la línea de código comentada no compilará, pero la segunda línea muestra cómo hacer coincidir el objeto List deseado y luego vincular esa lista a la variable x. Cuando esta línea de código coincide con una lista como List(1,2,3), da como resultado la salida List(1, 2, 3), como se muestra en la salida de la primera expresión println.

El primer ejemplo de Some muestra que puedes hacer coincidir un Some con el enfoque mostrado, pero no puedes acceder a su información en el lado derecho de la expresión. El segundo ejemplo muestra cómo puedes acceder al valor dentro de Some, y el tercer ejemplo va un paso más allá, dándote acceso al propio objeto Some. Cuando coincide con la segunda llamada a println, imprime Some(foo), demostrando que ahora tienes acceso al objeto Some.

Por último, este enfoque se utiliza para hacer coincidir un Person cuyo apellido es Doe. Esta sintaxis te permite asignar el resultado de la coincidencia de patrones a la variable p, y luego acceder a esa variable en el lado derecho de la expresión.

Uso de Some y None en expresiones coincidentes

Para completar estos ejemplos, a menudo utilizarás Some y None con expresiones match. Por ejemplo, cuando intentas crear un número a partir de una cadena con un método como toIntOption, puedes manejar el resultado en una expresión match:

val s = "42"

// later in the code ...
s.toIntOption match
    case Some(i) => println(i)
    case None => println("That wasn't an Int")

Dentro de la expresión match sólo tienes que especificar los casos Some y None como se muestra para manejar las condiciones de éxito y fracaso. Consulta la Receta 24.6, "Uso de los tipos de gestión de errores de Scala (Option, Tryy Either)", para ver más ejemplos de uso de Option, Some y None.

Ver también

4.11 Utilizar Enums y Clases Case en expresiones coincidentes

Problema

Quieres hacer coincidir enums, clases case u objetos case en una expresión match.

Solución

El siguiente ejemplo demuestra cómo utilizar patrones para hacer coincidir enums de distintas formas, dependiendo de la información que necesites a la derecha de cadasentencia case. En primer lugar, aquí tienes un enum llamado Animal que tiene tres instancias, Dog, Cat, yWoodpecker:

enum Animal:
    case Dog(name: String)
    case Cat(name: String)
    case Woodpecker

Dado que enum, este método getInfo muestra las distintas formas en que puedes hacer coincidir los tipos enum en una expresión match:

import Animal.*

def getInfo(a: Animal): String = a match
    case Dog(moniker) => s"Got a Dog, name = $moniker"
    case _: Cat       => "Got a Cat (ignoring the name)"
    case Woodpecker   => "That was a Woodpecker"

Estos ejemplos muestran cómo funciona getInfo cuando se le da un Dog, Cat, y Woodpecker:

println(getInfo(Dog("Fido")))     // Got a Dog, name = Fido
println(getInfo(Cat("Morris")))   // Got a Cat (ignoring the name)
println(getInfo(Woodpecker))      // That was a Woodpecker

En getInfo, si coincide la clase Dog, se extrae su nombre y se utiliza para crear la cadena de la parte derecha de la expresión. Para mostrar que el nombre de variable utilizado al extraer el nombre puede ser cualquier nombre de variable legal, utilizo el nombre moniker.

Al buscar un Cat quiero ignorar el nombre, así que utilizo la sintaxis mostrada para buscar cualquier instancia de Cat. Como Woodpecker no se crea con un parámetro, también se empareja como se muestra.

Debate

En Scala 2, los rasgos sellados se utilizaron con clases caso y objetos caso para conseguir el mismo efecto que el enum:

sealed trait Animal
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal
case object Woodpecker extends Animal

Como se describe en la Receta 6.12, "Cómo crear conjuntos de valores nombrados con Enums", un enum es un atajo para definir (a) una clase sellada o trait junto con (b) valores definidos como miembros del objeto compañero de la clase. Ambos enfoques pueden utilizarse en la expresión match en getInfo porque las clases case tienen un método unapply incorporado, que les permite trabajar en expresiones match. Describo cómo funciona esto en la Receta 7.8, "Implementación de la concordancia de patrones con unapply".

4.12 Añadir expresiones if (guardias) a las sentencias Case

Problema

Quieres añadir lógica de calificación a una sentencia case en una expresión match, como permitir un rango de números o coincidir con un patrón, pero sólo si ese patrón coincide con algún criterio adicional.

Solución

Añade una protección if a tu sentencia case. Utilízala para hacer coincidir un rango de números:

i match
    case a if 0 to 9 contains a => println("0-9 range: " + a)
    case b if 10 to 19 contains b => println("10-19 range: " + b)
    case c if 20 to 29 contains c => println("20-29 range: " + c)
    case _ => println("Hmmm...")

Utilízalo para hacer coincidir diferentes valores de un objeto:

i match
    case x if x == 1 => println("one, a lonely number")
    case x if (x == 2 || x == 3) => println(x)
    case _ => println("some other value")

Siempre que tu clase tenga un método unapply, puedes hacer referencia a los campos de la clase en tus guardias if. Por ejemplo, como una clase caso tiene un método unapply generado automáticamente, dada esta clase e instancia Stock:

case class Stock(symbol: String, price: BigDecimal)
val stock = Stock("AAPL", BigDecimal(132.50))

puedes utilizar la concordancia de patrones y las condiciones de guardia con los campos de clase:

stock match
    case s if s.symbol == "AAPL" && s.price < 140 => buy(s)
    case s if s.symbol == "AAPL" && s.price > 160 => sell(s)
    case _ => // do nothing

También puedes extraer campos de clases case -y clases que tengan métodos unapply correctamente implementados- y utilizarlos en tus condiciones de guardia. Por ejemplo, las declaraciones case de esta expresión match:

// extract the 'name' in the 'case' and then use that value
def speak(p: Person): Unit = p match
    case Person(name) if name == "Fred" =>
        println("Yabba dabba doo")
    case Person(name) if name == "Bam Bam" =>
        println("Bam bam!")
    case _ =>
        println("Watch the Flintstones!")

funcionará si Person se define como una clase de caso:

case class Person(aName: String)

o como una clase con un método unapply debidamente implementado:

class Person(val aName: String)
object Person:
    // 'unapply' deconstructs a Person. it’s also known as an
    // extractor, and Person is an “extractor object.”
    def unapply(p: Person): Option[String] = Some(p.aName)

Consulta la Receta 7.8, "Implementación de la concordancia de patrones con unapply", para obtener más detalles sobre cómo escribir métodos unapply.

Debate

Puedes utilizar expresiones if como ésta siempre que quieras añadir pruebas booleanas a la izquierda de las sentencias case (es decir, antes del símbolo => ).

Ten en cuenta que todos estos ejemplos podrían escribirse poniendo las pruebas if a la derecha de las expresiones, así:

case Person(name) =>
    if name == "Fred" then println("Yabba dabba doo")
    else if name == "Bam Bam" then println("Bam bam!")

Sin embargo, para muchas situaciones, tu código será más sencillo y fácil de leer uniendo la guardia if directamente con la declaración case; ayuda a separar la guardia de la lógica de negocio posterior.

Ten en cuenta también que este ejemplo de Person es un poco artificioso, porque las capacidades de concordancia de patrones de Scala te permiten escribir los casos así:

def speak(p: Person): Unit = p match
    case Person("Fred") => println("Yabba dabba doo")
    case Person("Bam Bam") => println("Bam bam!")
    case _ => println("Watch the Flintstones!")

En este caso, una guarda sería realmente necesaria cuando Person es más complejo y necesitas hacer algo más que coincidir con sus parámetros.

Además, como se demuestra en la Receta 4.10, en lugar de utilizar este código que se muestra en la Solución:

case x if (x == 2 || x == 3) => println(x)

Otra posible solución es utilizar un patrón de enlace de variables:

case x @ (2|3) => println(x)

Este código puede leerse como: "Si el valor de la expresión match (i) es 2 o 3, asigna ese valor a la variable x, luego imprime x utilizando println."

4.13 Utilizar una expresión coincidente en lugar de isInstanceOf

Problema

Quieres escribir un bloque de código que coincida con un tipo, o con varios tipos diferentes.

Solución

Puedes utilizar el método isInstanceOf para comprobar el tipo de un objeto:

if x.isInstanceOf[Foo] then ...

Sin embargo, la "manera Scala" es preferir las expresiones match para este tipo de trabajo, porque generalmente es mucho más potente y cómodo utilizar match que isInstanceOf.

Por ejemplo, en un caso de uso básico se te puede dar un objeto de tipo desconocido y querer determinar si el objeto es una instancia de un Person. Este código muestra cómo escribir una expresión match que devuelva true si el tipo es Person, y false en caso contrario:

def isPerson(m: Matchable): Boolean = m match
    case p: Person => true
    case _ => false

Un escenario más común es que tengas un modelo como éste:

enum Shape:
    case Circle(radius: Double)
    case Square(length: Double)

y luego querrás escribir un método para calcular el área de un Shape. Una solución a este problema es escribir area utilizando la concordancia de patrones:

import Shape.*

def area(s: Shape): Double = s match
    case Circle(r) => Math.PI * r * r
    case Square(l) => l * l

// examples
area(Circle(2.0))   // 12.566370614359172
area(Square(2.0))   // 4.0

Este es un uso común, en el que area toma un parámetro cuyo tipo es un padre inmediato de los tipos que deconstruyes dentro de match.

Ten en cuenta que si Circle y Square tuvieran parámetros constructores adicionales, y sólo necesitaras acceder a sus radius y length, respectivamente, la solución completa sería así:

enum Shape:
    case Circle(x0: Double, y0: Double, radius: Double)
    case Square(x0: Double, y0: Double, length: Double)

import Shape.*

def area(s: Shape): Double = s match
    case Circle(_, _, r) => Math.PI * r * r
    case Square(_, _, l) => l * l

// examples
area(Circle(0, 0, 2.0))   // 12.566370614359172
area(Square(0, 0, 2.0))   // 4.0

Como se muestra en las declaraciones case dentro de la expresión match, simplemente ignora los parámetros que no necesites refiriéndote a ellos con el carácter _.

Debate

Como se muestra, una expresión match te permite emparejar varios tipos, por lo que utilizarla para sustituir al método isInstanceOf no es más que un uso natural de la sintaxis match/case y del enfoque general de emparejamiento de patrones utilizado en las aplicaciones Scala.

Para los casos de uso más básicos, el método isInstanceOf puede ser un enfoque más sencillo para determinar si un objeto coincide con un tipo:

if (o.isInstanceOf[Person]) { // handle this ...

Sin embargo, para algo más complejo que esto, una expresión match es más legible que una larga declaración if/then/else if.

Ver también

4.14 Trabajar con una lista en una expresión coincidente

Problema

Sabes que una estructura de datos List es un poco diferente de otras estructuras de datos secuenciales: se construye a partir de celdas contras y termina en un elemento Nil. Querrás utilizar esto en tu beneficio cuando trabajes con una expresión match, por ejemplo al escribir una función recursiva.

Solución

Puedes crear un List que contenga los enteros 1, 2, y 3 de la siguiente manera:

val xs = List(1, 2, 3)

o así:

val ys = 1 :: 2 :: 3 :: Nil

Como se muestra en el segundo ejemplo, un List termina con un elemento Nil, y puedes aprovecharte de ello al escribir expresiones match para trabajar con listas, especialmente al escribir algoritmos recursivos. Por ejemplo, en el siguiente método listToString, si el elemento actual no es Nil, se llama recursivamente al método con el resto de List, pero si el elemento actual es Nil, se detienen las llamadas recursivas y se devuelve un String vacío, momento en el que se desenrollan las llamadas recursivas:

def listToString(list: List[String]): String = list match
    case s :: rest => s + " " + listToString(rest)
    case Nil => ""

El REPL muestra cómo funciona este método:

scala> val fruits = "Apples" :: "Bananas" :: "Oranges" :: Nil
fruits: List[java.lang.String] = List(Apples, Bananas, Oranges)

scala> listToString(fruits)
res0: String = "Apples Bananas Oranges "

Se puede utilizar el mismo enfoque cuando se trata de listas de otros tipos y algoritmos diferentes. Por ejemplo, aunque podrías escribir simplemente List(1,2,3).sum, este ejemplo muestra cómo escribir tu propio método de suma utilizando match y la recursividad:

def sum(list: List[Int]): Int = list match
    case Nil => 0
    case n :: rest => n + sum(rest)

Del mismo modo, se trata de un algoritmo de producto:

def product(list: List[Int]): Int = list match
    case Nil => 1
    case n :: rest => n * product(rest)

El REPL muestra cómo funcionan estos métodos:

scala> val nums = List(1,2,3,4,5)
nums: List[Int] = List(1, 2, 3, 4, 5)

scala> sum(nums)
res0: Int = 15

scala> product(nums)
res1: Int = 120

No olvides reducir y plegar

Aunque la recursividad es genial, los distintos métodos de reducción y pliegue de Scala en las clases de colecciones están diseñados para permitirte recorrer una colección mientras aplicas un algoritmo, y a menudo eliminan la necesidad de recursividad. Por ejemplo, puedes escribir un algoritmo de suma utilizando reduce en cualquiera de estas dos formas:

// long form
def sum(list: List[Int]): Int = list.reduce((x,y) => x + y)

// short form
def sum(list: List[Int]): Int = list.reduce(_ + _)

Consulta la Receta 13.10, "Recorrer una colección con los métodos reducir y plegar", para más detalles.

Debate

Como se ha visto, la recursión es una técnica en la que un método se llama a sí mismo para resolver un problema. En programación funcional -donde todas las variables son inmutables- la recursión proporciona una forma de iterar sobre los elementos de un List para resolver un problema, como calcular la suma o el producto de todos los elementos de un List.

Lo bueno de trabajar con la clase List en particular es que un List termina con el elemento Nil, por lo que tus algoritmos recursivos suelen tener este patrón:

def myTraversalMethod[A](xs: List[A]): B = xs match
    case head :: tail =>
        // do something with the head
        // pass the tail of the list back to your method, i.e.,
        // `myTraversalMethod(tail)`
    case Nil =>
        // end condition here (0 for sum, 1 for product, etc.)
        // end the traversal

Las variables en la programación funcional

En FP, utilizamos el término variables, pero como sólo utilizamos variables inmutables, puede parecer que esta palabra no tiene sentido, es decir, que tenemos una variable que no puede variar.

Lo que ocurre aquí es que realmente queremos decir "variable" en el sentido algebraico, no en el sentido de programación informática. Por ejemplo, en álgebra decimos que a, b, y c son variables cuando escribimos esta ecuación algebraica:

a = b * c

Sin embargo, una vez asignadas, no pueden variar. El término variable tiene el mismo significado en la programación funcional.

Ver también

Al principio me pareció que la recursividad era un tema innecesariamente difícil de entender, por lo que he escrito bastantes entradas en el blog sobre ella:

4.15 Emparejar una o más excepciones con try/catch

Problema

Quieres atrapar una o varias excepciones en un bloque try/catch.

Solución

La sintaxis de Scala try/catch/finally es similar a la de Java, pero utiliza el enfoque de expresión match en el bloque catch:

try
   doSomething()
catch
   case e: SomeException => e.printStackTrace
finally
   // do your cleanup work

Cuando necesites atrapar y manejar varias excepciones, sólo tienes que añadir los tipos de excepción como diferentes sentencias case:

try
   openAndReadAFile(filename)
catch
   case e: FileNotFoundException =>
       println(s"Couldn’t find $filename.")
   case e: IOException =>
       println(s"Had an IOException trying to read $filename.")

También puedes escribir ese código así, si lo prefieres:

try
    openAndReadAFile(filename)
catch
    case e: (FileNotFoundException | IOException) =>
        println(s"Had an IOException trying to read $filename")

Debate

Como se muestra, la sintaxis de Scala case se utiliza para emparejar diferentes excepciones posibles. Si no te preocupa qué excepciones concretas pueden lanzarse y quieres atraparlas todas y hacer algo con ellas -como registrarlas-, utiliza esta sintaxis:

try
   openAndReadAFile(filename)
catch
   case t: Throwable => logger.log(t)

Si por alguna razón no te importa el valor de la excepción, también puedes atraparlas todas e ignorarlas así:

try
   openAndReadAFile(filename)
catch
   case _: Throwable => println("Nothing to worry about, just an exception")

Métodos basados en try/catch

Como se ha mostrado en la introducción de este capítulo, un bloque try/catch/finally puede devolver un valor y, por tanto, utilizarse como cuerpo de un método. El siguiente método devuelve un Option[String]. Devuelve un Some que contiene un String si se encuentra el archivo, y un None si hay algún problema al leer el archivo:

import scala.io.Source
import java.io.{FileNotFoundException, IOException}

def readFile(filename: String): Option[String] =
    try
        Some(Source.fromFile(filename).getLines.mkString)
    catch
        case _: (FileNotFoundException|IOException) => None

Esto muestra una forma de devolver un valor de una expresión try.

Hoy en día, rara vez escribo métodos que lancen excepciones, pero, al igual que en Java, puedes lanzar una excepción desde una cláusula catch. Sin embargo, como Scala no tiene excepciones comprobadas, no necesitas especificar que un método lanza la excepción. Esto se demuestra en el siguiente ejemplo, en el que el método no está anotado de ninguna forma:

// danger: this method doesn’t warn you that an exception can be thrown
def readFile(filename: String): String =
    try
        Source.fromFile(filename).getLines.mkString
    catch
        case t: Throwable => throw t

En realidad, es un método terriblemente peligroso: ¡no escribas código así!

Para declarar que un método lanza una excepción, añade la anotación @throws a la definición de tu método:

// better: this method warns others that an exception can be thrown
@throws(classOf[NumberFormatException])
def readFile(filename: String): String =
    try
        Source.fromFile(filename).getLines.mkString
    catch
        case t: Throwable => throw t

Aunque este último método es mejor que el anterior, no es preferible ninguno de los dos. La "manera Scala" es no lanzar nunca excepciones. En su lugar, debes utilizar Option, como se ha mostrado anteriormente, o utilizar las clases Try/Success/Failure o Either/Right/Left cuando quieras devolver información sobre lo que ha fallado. Este ejemplo muestra cómo utilizar Try:

import scala.io.Source
import java.io.{FileNotFoundException, IOException}
import scala.util.{Try,Success,Failure}

def readFile(filename: String): Try[String] =
    try
        Success(Source.fromFile(filename).getLines.mkString)
    catch
        case t: Throwable => Failure(t)

Siempre que se trate de un mensaje de excepción, prefiero utilizar Try o Either en lugar de Option, porque te dan acceso al mensaje en Failure o Left, mientras que Option sólo devuelve None.

Una forma concisa de cogerlo todo

Otra forma concisa de atrapar todas las excepciones es con el método allCatch del objeto scala.util.control.Exception. Los siguientes ejemplos demuestran cómo utilizar allCatch, mostrando primero el caso de éxito y luego el caso de fallo. La salida de cada expresión se muestra después del comentario de cada línea:

import scala.util.control.Exception.allCatch

// OPTION
allCatch.opt("42".toInt)      // Option[Int] = Some(42)
allCatch.opt("foo".toInt)     // Option[Int] = None

// TRY
allCatch.toTry("42".toInt)    // Matchable = 42
allCatch.toTry("foo".toInt)
   // Matchable = Failure(NumberFormatException: For input string: "foo")

// EITHER
allCatch.either("42".toInt)   // Either[Throwable, Int] = Right(42)
allCatch.either("foo".toInt)
   // Either[Throwable, Int] =
   // Left(NumberFormatException: For input string: "foo")

Ver también

4.16 Declarar una variable antes de utilizarla en un bloquetry/catch/finally

Problema

Quieres utilizar un objeto en un bloque try, y necesitas acceder a él en la parte finally del bloque, como cuando necesitas llamar a un método close de un objeto.

Solución

En general, declara tu campo como un Option antes del bloque try/catch, y luego vincula la variable a un Some dentro de la cláusula try. Esto se muestra en el siguiente ejemplo, donde el campo sourceOption se declara antes del bloque try/catch, y se asigna dentro de la cláusula try:

import scala.io.Source
import java.io.*

var sourceOption: Option[Source] = None
try
    sourceOption = Some(Source.fromFile("/etc/passwd"))
    sourceOption.foreach { source =>
        // do whatever you need to do with 'source' here ...
        for line <- source.getLines do println(line.toUpperCase)
    }
catch
    case ioe: IOException => ioe.printStackTrace
    case fnf: FileNotFoundException => fnf.printStackTrace
finally
    sourceOption match
        case None =>
            println("bufferedSource == None")
        case Some(s) =>
            println("closing the bufferedSource ...")
            s.close

Éste es un ejemplo artificioso -y la Receta 16.1, "Lectura de archivos de texto", muestra una forma mucho mejor de leer archivos-, pero muestra el planteamiento. En primer lugar, define un campo var como Option antes del bloque try:

var sourceOption: Option[Source] = None

A continuación, dentro de la cláusula try, asigna a la variable un valor Some:

sourceOption = Some(Source.fromFile("/etc/passwd"))

Cuando tengas un recurso que cerrar, utiliza una técnica como la que se muestra (aunque la Receta 16.1, "Lectura de archivos de texto", también muestra una forma mucho mejor de cerrar recursos). Ten en cuenta que si se lanza una excepción en este código, sourceOption dentro de finally será un valor None. Si no se lanza ninguna excepción, se evaluará la rama Some de la expresión match.

Debate

Una clave de esta receta es conocer la sintaxis para declarar campos Option que no se rellenan inicialmente:

var in: Option[FileInputStream] = None
var out: Option[FileOutputStream] = None

También se puede utilizar esta segunda forma, pero se prefiere la primera:

var in = None: Option[FileInputStream]
var out = None: Option[FileOutputStream]

No utilices null

Cuando empecé a trabajar con Scala, la única forma que se me ocurrió de escribir este código fue utilizando valores de null. El siguiente código demuestra el enfoque que utilicé en una aplicación que comprueba mis cuentas de correo electrónico. Los campos store y inbox de este código se declaran como campos null que tienen los tipos Store y Folder (del paquete javax.mail ):

// (1) declare the null variables (don’t use null; this is just an example)
var store: Store = null
var inbox: Folder = null

try
    // (2) use the variables/fields in the try block
    store = session.getStore("imaps")
    inbox = getFolder(store, "INBOX")
    // rest of the code here ...
catch
    case e: NoSuchProviderException => e.printStackTrace
    case me: MessagingException => me.printStackTrace
finally
    // (3) call close() on the objects in the finally clause
    if (inbox != null) inbox.close
    if (store != null) store.close

Sin embargo, trabajar en Scala te permite olvidarte de que existen los valores null, por lo que no es un enfoque recomendable.

Ver también

Consulta estas recetas para obtener más detalles sobre (a) cómo no utilizar los valores null, y (b) cómo utilizar Option, Try, y Either en su lugar:

Siempre que escribas código que necesite abrir un recurso al empezar y cerrarlo al terminar, puede ser útil utilizar el objetoscala.util.Using . Consulta la Receta 16.1, "Lectura de archivos de texto", para ver un ejemplo de cómo utilizar este objeto y una forma mucho mejor de leer un archivo de texto.

Además, la Receta 24.8, "Manejar valores de opción con funciones de orden superior", muestra otras formas de trabajar con valores Option además de utilizar una expresión match.

4.17 Crear tus propias estructuras de control

Problema

Quieres definir tus propias estructuras de control para personalizar el lenguaje Scala, simplificar tu código o crear un lenguaje específico de dominio (DSL).

Solución

Gracias a funciones como listas de parámetros múltiples, parámetros por nombre, métodos de extensión, funciones de orden superior, etc., puedes crear tu propio código que funcione igual que una estructura de control.

Por ejemplo, imagina que Scala no tiene su propio bucle while incorporado, y quieres crear tu propio bucle whileTrue personalizado, que puedes utilizar así:

var i = 0
whileTrue (i < 5) {
    println(i)
    i += 1
}

Para crear esta estructura de control whileTrue, define un método llamado whileTrue que tome dos listas de parámetros. La primera lista de parámetros se encarga de la condición de prueba -en este caso, i < 5- y la segunda lista de parámetros es el bloque de código que el usuario quiere ejecutar, es decir, el código que hay entre las llaves. Define ambos parámetros como parámetros by-name. Como whileTrue sólo se utiliza para efectos secundarios, como actualizar variables mutables o imprimir en la consola, declara que devuelve Unit. Un primer esbozo de la firma del método tiene este aspecto:

def whileTrue(testCondition: => Boolean)(codeBlock: => Unit): Unit = ???

Una forma de implementar el cuerpo del método es escribir un algoritmo recursivo. Este código muestra una solución completa:

import scala.annotation.tailrec

object WhileTrue:
    @tailrec
    def whileTrue(testCondition: => Boolean)(codeBlock: => Unit): Unit =
        if (testCondition) then
            codeBlock
            whileTrue(testCondition)(codeBlock)
        end if
    end whileTrue

En este código, se evalúa testCondition, y si la condición es verdadera, se ejecuta codeBlock, y luego se llama recursivamente a whileTrue. Se sigue llamando a sí mismo hasta que testCondition devuelve false.

Para probar este código, primero impórtalo:

import WhileTrue.whileTrue

A continuación, ejecuta el bucle whileTrue mostrado anteriormente, y verás que funciona como deseabas.

Debate

Los creadores del lenguaje Scala tomaron la decisión consciente de no implementar algunas palabras clave en Scala, y en su lugar implementaron la funcionalidad a través de bibliotecas Scala. Por ejemplo, Scala no tiene incorporadas las palabras clave break y continue. En su lugar, las implementa a través de una biblioteca, como describo en mi entrada del blog "Scala: Cómo utilizar break y continue en los bucles for y while".

Como se muestra en la Solución, la capacidad de crear tus propias estructuras de control proviene de funciones como éstas:

  • Las listas de parámetros múltiples te permiten hacer lo que yo hice con whileTrue: crear un grupo de parámetros para la condición de prueba, y un segundo grupo para el bloque de código.

  • Los parámetros by-name también te permiten hacer lo que yo hice con whileTrue: aceptar parámetros que no se evalúan hasta que se accede a ellos dentro de tu método.

Del mismo modo, otras características como la notación infija, las funciones de orden superior, los métodos de extensión y las interfaces fluidas te permiten crear otras estructuras de control y DSL personalizadas.

Parámetros por nombre

Los parámetros by-name son una parte importante de la estructura de control whileTrue. En Scala es importante saber que cuando defines parámetros de método utilizando la sintaxis =>:

def whileTrue(testCondition: => Boolean)(codeBlock: => Unit) =
                           -----                  -----

estás creando lo que se conoce como parámetro de llamada por nombre o parámetro por nombre. Un parámetro by-name sólo se evalúa cuando se accede a él dentro de tu método, así que, como escribo en mis entradas de blog "Cómo utilizar parámetros by-name en Scala" y "Scala y los parámetros call-by-name", un nombre más preciso para estos parámetros es evaluar cuando se accede a ellos. Porque así es exactamente como funcionan: sólo se evalúan cuando se accede a ellos dentro de tu método. Como señalo en esa segunda entrada del blog, Rob Norris hace la comparación de que un parámetro by-name es como recibir un método def.

Otro ejemplo

En el ejemplo de whileTrue, utilicé una llamada recursiva para mantener el bucle en ejecución, pero para estructuras de control más sencillas no necesitas recursividad. Por ejemplo, supongamos que quieres una estructura de control que tome dos condiciones de prueba, y si ambas se evalúan como true, ejecutará un bloque de código que se le proporcione. Una expresión que utilice esa estructura de control tiene este aspecto

doubleIf(age > 18)(numAccidents == 0) { println("Discount!") }

En este caso, define doubleIf como un método que toma tres listas de parámetros, donde, de nuevo, cada parámetro es un parámetro by-name:

// two 'if' condition tests
def doubleIf(test1: => Boolean)(test2: => Boolean)(codeBlock: => Unit) =
    if test1 && test2 then codeBlock

Como doubleIf sólo necesita realizar una prueba y no necesita hacer un bucle indefinido, no hay necesidad de una llamada recursiva en el cuerpo de su método. Simplemente comprueba las dos condiciones de la prueba, y si se evalúan como true, se ejecuta codeBlock.

Ver también

Get Scala Cookbook, 2ª Edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.