Listas, mapas, tuplas y sets
En esta guía (15)
En la lección anterior trabajaste con arrays, que son mutables. En esta vas a conocer las colecciones que más se usan en Scala, que son inmutables por defecto: listas, mapas, tuplas y sets. Vas a ver cómo crearlas, consultarlas y "actualizarlas" (creando versiones nuevas), y cómo transformarlas con map, filter, groupBy y compañía. Al final vas a aplicar todo sobre listas de transacciones y cuentas.
Listas, mapas y tuplas, un primer vistazo
// Una lista de Strings
val frutas: List[String] = List("manzanas", "naranjas", "peras")
// Un mapa: pares clave -> valor
val miMapa = Map("nombre" -> "Juan", "ciudad" -> "Mendoza")
val nombre = miMapa("nombre")
// Una tupla: varios valores, de tipos distintos, agrupados
val miTupla = ("Manzana", 3)
val fruta = miTupla._1 // primer elemento
- Una lista es una secuencia ordenada de elementos del mismo tipo.
- Un mapa es una colección de pares clave-valor: buscás un valor a partir de su clave.
- Una tupla agrupa unos pocos valores, que pueden ser de tipos distintos.
Listas
Una List es inmutable: una vez creada, no cambia. Las operaciones que "modifican" una lista devuelven una lista nueva.
val numeros = List(10, 20, 30)
println(numeros.head) // 10: el primer elemento
println(numeros.tail) // List(20, 30): todos menos el primero
println(numeros.isEmpty) // false
println(numeros.length) // 3
println(numeros(1)) // 20: acceso por índice
val conCero = 0 :: numeros // agrega al principio: List(0, 10, 20, 30)
val conCuarenta = numeros :+ 40 // agrega al final: List(10, 20, 30, 40)
val juntas = numeros ++ List(40, 50)
println(numeros) // List(10, 20, 30): el original no cambió
Agregar al principio con :: es inmediato; agregar al final con :+ tiene que recorrer toda la lista. Si vas a acceder mucho por índice o agregar al final, conviene un Vector, que se usa igual pero es eficiente para esas operaciones.
Ojo con head en una lista vacía: lanza una NoSuchElementException. Si la lista puede estar vacía, usá headOption, que devuelve un Option (más abajo vas a ver qué es).
Mapas
Construir un mapa
Los mapas se crean con Map(...) y la flecha ->, que arma un par clave-valor. Son inmutables por defecto.
val saldos = Map("Ana" -> 1500.0, "Bruno" -> 300.0, "Carla" -> 2200.0)
Tratá de que todos los valores sean del mismo tipo. Si mezclás tipos, como en Map("nombre" -> "Ana", "edad" -> 25), el compilador elige un tipo genérico para los valores (Any en Scala 2, Matchable en Scala 3), y perdés la ayuda de los tipos: miMapa("edad") + 1 no compila. Para datos con campos de distinto tipo, lo correcto es una case class (lección 8).
Acceder a los valores
val saldos = Map("Ana" -> 1500.0, "Bruno" -> 300.0, "Carla" -> 2200.0)
println(saldos("Ana")) // 1500.0
println(saldos.contains("Zoe")) // false
println(saldos.get("Ana")) // Some(1500.0)
println(saldos.get("Zoe")) // None
println(saldos.getOrElse("Zoe", 0.0)) // 0.0
println(saldos.keys) // Set(Ana, Bruno, Carla)
println(saldos.values) // Iterable(1500.0, 300.0, 2200.0)
println(saldos.isEmpty) // false
Si pedís una clave que no existe con saldos("Zoe"), se lanza una NoSuchElementException. Por eso existen get y getOrElse:
getdevuelve unOption:Some(valor)si la clave existe, oNonesi no.Optiones la forma de Scala de decir "puede haber un valor o no" sin usarnull.getOrElsedevuelve el valor o, si no está, el valor por defecto que le pasás.
Actualizar un mapa
Como el mapa es inmutable, "actualizarlo" significa crear uno nuevo:
val saldos = Map("Ana" -> 1500.0, "Bruno" -> 300.0)
val conCarla = saldos + ("Carla" -> 2200.0) // agrega
val anaActualizada = saldos + ("Ana" -> 1600.0) // si la clave existe, la reemplaza
val conUpdated = saldos.updated("Dario", 100.0) // lo mismo, con un método
val sinBruno = saldos - "Bruno" // quita
println(anaActualizada) // Map(Ana -> 1600.0, Bruno -> 300.0)
println(saldos) // Map(Ana -> 1500.0, Bruno -> 300.0): el original no cambió
Recorrer un mapa
val saldos = Map("Ana" -> 1500.0, "Bruno" -> 300.0)
for ((clave, valor) <- saldos) {
println(s"Clave: $clave, Valor: $valor")
}
saldos.foreach { case (cliente, saldo) => println(s"$cliente tiene $saldo") }
El for desarma cada par en (clave, valor). En foreach, el { case (cliente, saldo) => ... } hace lo mismo con pattern matching.
Mapas ordenados
Un Map común no garantiza ningún orden. Si lo necesitás, hay mapas especializados: SortedMap mantiene las claves ordenadas, y ListMap (inmutable) o LinkedHashMap (mutable) mantienen el orden de inserción.
import scala.collection.immutable.SortedMap
val mapaOrdenado = SortedMap("b" -> 2, "a" -> 1, "c" -> 3)
println(mapaOrdenado) // TreeMap(a -> 1, b -> 2, c -> 3)
Tuplas
Una tupla agrupa valores de tipos distintos sin necesidad de definir una clase. Es útil para devolver varios resultados de una función o para pares temporales.
val miTupla = ("Manzana", 3, 1.5)
println(miTupla._1) // Manzana
println(miTupla._3) // 1.5
val (fruta, cantidad, precio) = miTupla // desarmar la tupla en tres valores
println(s"$cantidad $fruta a $precio") // 3 Manzana a 1.5
def minYMax(xs: List[Int]): (Int, Int) = (xs.min, xs.max)
println(minYMax(List(5, 2, 8))) // (2,8)
Los ._1, ._2 se vuelven difíciles de leer rápido. Si una tupla empieza a viajar por todo el programa, es momento de convertirla en una case class con nombres de campo.
zip: combinar dos secuencias
zip combina dos secuencias en una secuencia de pares, emparejando elemento a elemento:
val nombres = List("Juan", "Ana", "Luis")
val edades = List(30, 25, 35)
val nombresYEdades = nombres.zip(edades)
println(nombresYEdades) // List((Juan,30), (Ana,25), (Luis,35))
val (soloNombres, soloEdades) = nombresYEdades.unzip // la operación inversa
println(nombresYEdades.toMap) // Map(Juan -> 30, Ana -> 25, Luis -> 35)
Si las listas tienen distinto largo, zip corta en la más corta.
Sets
Un Set (conjunto) es una colección sin duplicados y sin un orden garantizado. Sirve para guardar elementos únicos y para las operaciones de conjuntos: unión, intersección, diferencia. Por defecto es inmutable, pero también existe una versión mutable.
import scala.collection.mutable
// Inmutable (el Set por defecto)
val conjuntoInmutable = Set(1, 2, 3, 4, 3, 2)
println(conjuntoInmutable) // Set(1, 2, 3, 4): sin duplicados
println(conjuntoInmutable.contains(3)) // true
println(conjuntoInmutable.isEmpty) // false
// Mutable
val conjuntoMutable = mutable.Set(1, 2, 3)
conjuntoMutable += 4
println(conjuntoMutable.size) // 4
// Operaciones de conjuntos
val a = Set(1, 2, 3)
val b = Set(2, 3, 4)
println(a union b) // Set(1, 2, 3, 4)
println(a intersect b) // Set(2, 3)
println(a diff b) // Set(1)
for (elemento <- a) println(elemento)
Un detalle sobre el import: si escribís import scala.collection.mutable.Set, a partir de ahí todo Set del archivo pasa a ser mutable, incluido el que creías inmutable. Importando el paquete scala.collection.mutable y escribiendo mutable.Set, queda claro cuál es cuál.
Transformar colecciones
Las funciones de orden superior que viste con arrays funcionan igual con todas estas colecciones. Son las herramientas con las que se escribe casi cualquier transformación de datos:
val transacciones = List(("ana", 1500.0), ("bruno", -300.0), ("ana", -200.0), ("carla", 800.0))
val montos = transacciones.map(_._2) // List(1500.0, -300.0, -200.0, 800.0)
val creditos = transacciones.filter(_._2 > 0) // solo los montos positivos
val total = transacciones.map(_._2).sum // 1800.0
val total2 = transacciones.foldLeft(0.0)((acumulado, t) => acumulado + t._2) // 1800.0
val porCliente = transacciones.groupBy(_._1) // Map(cliente -> List(transacciones))
val ordenadas = transacciones.sortBy(_._2) // de menor a mayor monto
val hayDebitos = transacciones.exists(_._2 < 0) // true
println(porCliente("ana")) // List((ana,1500.0), (ana,-200.0))
maptransforma cada elemento.filterse queda con los que cumplen una condición.foldLeftrecorre la colección acumulando un resultado: arranca con un valor inicial (0.0) y, en cada paso, combina lo acumulado con el elemento actual.sum,maxocountson casos particulares de esta idea.groupByarma unMapcuya clave es el resultado de la función y cuyo valor es la lista de elementos con esa clave.sortByordena según un campo;existsyforallpreguntan si alguno o todos cumplen una condición.
Practicá
-
Listas. Partiendo de esta lista de transacciones (fecha, monto):
scalaval transacciones = List( ("2023-10-01", 100.0), ("2023-10-15", -50.0), ("2023-10-20", 200.0), ("2023-11-05", -30.0) )- Calculá la suma de todas las transacciones con
sum. - Obtené la fecha de la primera transacción con
head. - Obtené las transacciones de un mes específico con
filter.
- Calculá la suma de todas las transacciones con
-
Tuplas. Dada esta lista de cuentas (titular, cuenta, saldo), usá
unzip3para separarla en tres listas: titulares, números de cuenta y saldos.scalaval cuentas = List( ("Titular1", "Cuenta1", 1000.0), ("Titular2", "Cuenta2", 1500.0), ("Titular3", "Cuenta3", 2000.0) ) -
Mapas. Dado este mapa de titulares y números de cuenta, agregá un titular nuevo con
updatedy verificá concontainssi un titular existe.scalaval mapaCuentas = Map( "Titular1" -> "Cuenta1", "Titular2" -> "Cuenta2", "Titular3" -> "Cuenta3" ) -
Sets. Dado
val numerosDeCuenta = Set(111, 222, 333), agregá una cuenta con+, eliminá otra con-y verificá concontainssi un número existe. -
Combinación de tipos. Dada esta lista de transacciones (titular, cuenta, monto), agrupalas por titular con
groupByy calculá el saldo total de cada titular conmapysum(oreduce).scalaval transaccionesBancarias = List( ("Titular1", "Cuenta1", 100.0), ("Titular2", "Cuenta2", -50.0), ("Titular1", "Cuenta1", 200.0), ("Titular3", "Cuenta3", -30.0) )
Soluciones
// Ejercicio 1
val transacciones = List(
("2023-10-01", 100.0),
("2023-10-15", -50.0),
("2023-10-20", 200.0),
("2023-11-05", -30.0)
)
val suma = transacciones.map(_._2).sum
println(s"Suma: $suma") // Suma: 220.0
val primeraFecha = transacciones.head._1
println(s"Fecha de la primera transacción: $primeraFecha") // 2023-10-01
val mes = "2023-10"
val delMes = transacciones.filter(_._1.startsWith(mes))
println(delMes) // List((2023-10-01,100.0), (2023-10-15,-50.0), (2023-10-20,200.0))
// Ejercicio 2
val cuentas = List(
("Titular1", "Cuenta1", 1000.0),
("Titular2", "Cuenta2", 1500.0),
("Titular3", "Cuenta3", 2000.0)
)
val (titulares, numerosCuenta, saldos) = cuentas.unzip3
println("Titulares: " + titulares.mkString(", "))
println("Números de cuenta: " + numerosCuenta.mkString(", "))
println("Saldos: " + saldos.mkString(", "))
// Titulares: Titular1, Titular2, Titular3
// Números de cuenta: Cuenta1, Cuenta2, Cuenta3
// Saldos: 1000.0, 1500.0, 2000.0
unzip separa pares en dos listas; para tríos se usa unzip3.
// Ejercicio 3
val mapaCuentas = Map(
"Titular1" -> "Cuenta1",
"Titular2" -> "Cuenta2",
"Titular3" -> "Cuenta3"
)
val nuevoMapaCuentas = mapaCuentas.updated("NuevoTitular", "NuevaCuenta")
for (titular <- List("Titular2", "TitularNoExistente")) {
if (nuevoMapaCuentas.contains(titular)) println(s"$titular existe en el mapa.")
else println(s"$titular no existe en el mapa.")
}
println("Nuevo mapa de cuentas: " + nuevoMapaCuentas)
println("Mapa original: " + mapaCuentas)
Titular2 existe en el mapa.
TitularNoExistente no existe en el mapa.
Nuevo mapa de cuentas: Map(Titular1 -> Cuenta1, Titular2 -> Cuenta2, Titular3 -> Cuenta3, NuevoTitular -> NuevaCuenta)
Mapa original: Map(Titular1 -> Cuenta1, Titular2 -> Cuenta2, Titular3 -> Cuenta3)
// Ejercicio 4
val numerosDeCuenta = Set(111, 222, 333)
val conNueva = numerosDeCuenta + 444
val sin222 = numerosDeCuenta - 222
for (cuenta <- List(111, 555)) {
if (numerosDeCuenta.contains(cuenta)) println(s"La cuenta $cuenta existe.")
else println(s"La cuenta $cuenta no existe.")
}
println("Con la nueva cuenta: " + conNueva) // Set(111, 222, 333, 444)
println("Sin la 222: " + sin222) // Set(111, 333)
// Ejercicio 5
val transaccionesBancarias = List(
("Titular1", "Cuenta1", 100.0),
("Titular2", "Cuenta2", -50.0),
("Titular1", "Cuenta1", 200.0),
("Titular3", "Cuenta3", -30.0)
)
val porTitular = transaccionesBancarias.groupBy(_._1)
val saldoPorTitular = porTitular.map {
case (titular, transacciones) => (titular, transacciones.map(_._3).sum)
}
// Otra forma, sin pattern matching:
val saldoPorTitular2 = porTitular.map(e => (e._1, e._2.map(_._3).reduce(_ + _)))
saldoPorTitular.toList.sortBy(_._1).foreach { case (titular, saldo) =>
println(s"$titular: $saldo")
}
Titular1: 300.0
Titular2: -50.0
Titular3: -30.0
Como un Map no garantiza el orden, para mostrar el resultado lo convertimos en lista y lo ordenamos por titular. Fijate también la diferencia entre sum y reduce(_ + _): dan lo mismo, pero reduce falla con una lista vacía, mientras que sum devuelve 0.
Resumen
List,Map,Sety las tuplas son inmutables: cada "cambio" devuelve una colección nueva.- Listas:
head,tail,isEmpty,::para agregar al principio,:+al final. - Mapas: se arman con
->; se consultan conm(clave),get(devuelveOption),getOrElseycontains; se actualizan con+,updatedy-.SortedMapmantiene las claves ordenadas. - Tuplas:
._1,._2o desarmándolas conval (a, b) = tupla.zip,unzipyunzip3combinan y separan. - Sets: sin duplicados, con
union,intersectydiff. Para el mutable, usámutable.Set. map,filter,foldLeft,groupBy,sortByyexistsson la base de cualquier transformación de datos.