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 expresionesfor
-
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 if
con 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
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
-
Para ver más ejemplos de cómo utilizar
zipWithIndex
, consulta la Receta 13.4, "Utilizar zipConIndex o zip para crear contadores de bucle". -
Para ver más ejemplos de cómo iterar sobre los elementos de un
Map
, consulta la Receta 14.9, "Recorrer un mapa".
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
(
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í:
-
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 unVector
, el tipo de salida también será unVector
. Puedes pensar en esta nueva colección como si fuera un cubo vacío. -
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. -
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
-
Las comparaciones entre las comprensiones
for
ymap
se muestran con más detalle en la Receta 13.5, "Transformar una colección en otra con mapa".
4.5 Utilizar la construcción if como un operador ternario
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
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
-
Para más información sobre cómo funcionan los conmutadores de la JVM, consulta la especificación de la JVM sobre los conmutadores de compilación.
-
En cuanto a la diferencia entre un
lookupswitch
y untableswitch
, esta página de Stack Overflow afirma: "La diferencia es que un conmutador de búsqueda utiliza una tabla con claves y etiquetas, mientras que un conmutador de tablas utiliza una tabla sólo con etiquetas". De nuevo, consulta la sección "Interruptores de compilación" de la especificación de la Máquina Virtual Java (JVM ) para obtener más detalles. -
Consulta la Receta 10.7, "Crear funciones parciales ", para más información sobre las funciones parciales.
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
-
Consulta la Receta 4.12 para un enfoque relacionado.
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
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 deInt
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óncase
. Por ejemplo, al final de una expresiónmatch
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, ystr
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 avariableName
.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 es1
, pero hasta ahora no se ha accedido alList
en el lado derecho de la expresión. Al acceder a unList
, 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ónmatch
, la línea de código comentada no compilará, pero la segunda línea muestra cómo hacer coincidir el objetoList
deseado y luego vincular esa lista a la variablex
. Cuando esta línea de código coincide con una lista comoList(1,2,3)
, da como resultado la salidaList(1, 2, 3)
, como se muestra en la salida de la primera expresiónprintln
.El primer ejemplo de
Some
muestra que puedes hacer coincidir unSome
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 deSome
, y el tercer ejemplo va un paso más allá, dándote acceso al propio objetoSome
. Cuando coincide con la segunda llamada aprintln
, imprimeSome(foo)
, demostrando que ahora tienes acceso al objetoSome
.Por último, este enfoque se utiliza para hacer coincidir un
Person
cuyo apellido esDoe
. Esta sintaxis te permite asignar el resultado de la coincidencia de patrones a la variablep
, 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
-
Una discusión sobre cómo evitar el borrado de tipos al utilizar expresiones
match
en Stack Overflow
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
-
La receta 4.10 muestra muchas más técnicas de
match
.
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:
-
"Recursivo: Cómo funcionan las llamadas a funciones recursivas"
-
En "Recursión: Pensar recursivamente", escribo sobre los elementos de identidad, incluyendo cómo
0
es un elemento de identidad para la operación suma,1
es un elemento de identidad para la operación producto y""
(una cadena en blanco) es un elemento de identidad para trabajar con cadenas.
4.15 Emparejar una o más excepciones con 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
-
Consulta la Receta 8.7, "Declarar que un método puede lanzar una excepción", para ver más ejemplos de cómo declarar que un método puede lanzar una excepción.
-
Consulta la Receta 24.6, "Uso de los tipos de gestión de errores de Scala (Option, Tryy Either)", para obtener más información sobre el uso de
Option
/Some
/None
yTry
/Success
/Failure
. -
Consulta la página
scala.util.control.Exception
Scaladoc para obtener másinformaciónallCatch
.
4.16 Declarar una variable antes de utilizarla en un bloquetry/catch/finally
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:
-
Receta 24.6, "Uso de los tipos de tratamiento de errores de Scala (Option, Tryy Either)"
-
Receta 24.8, "Manejar valores de opción con funciones de orden superior"
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
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
-
Uno de mis usos favoritos de esta técnica se muestra en el libro Beginning Scala de David Pollak (Apress). Aunque ha quedado obsoleta por el objeto
scala.util.Using
, describo cómo funciona la técnica en esta entrada del blog, "La estructura de control de uso en Beginning Scala". -
La clase
Breaks
de Scala se utiliza para implementar la funcionalidad de romper y continuar en los buclesfor
, y escribí sobre ella: "Scala: Cómo utilizar break y continue en los bucles for y while". El código fuente de la claseBreaks
es bastante sencillo y proporciona otro ejemplo de cómo implementar una estructura de control. Puedes encontrar su código fuente como enlace en su página Scaladoc.
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.