Closures con listas
Al terminar: Usar closures con listas: map, filter y ordenar.
Al terminar esta lección podrás preguntarle a un partido en Swift cosas como «dame solo las tarjetas», «dame los goleadores» o «dame los minutos de gol de Ana», y cada pregunta será una línea, igual que en tu versión de Kotlin. Es lo que se consigue al juntar las closures de la lección anterior con las listas: filter para quedarte con unos elementos, compactMap para transformar y descartar, forEach para recorrer y map para transformar.
Temas: filter, compactMap, forEach con guard case, map con el nombre de una función o un key path, y una propiedad y una función con el mismo nombre.
filter, compactMap, forEach y map: preguntas a una lista
Una lista (un array) guarda varios valores en orden. Cuatro de sus funciones reciben una closure y la aplican a cada elemento:
filter { … }se queda con los elementos para los que la closure datruey devuelve una lista nueva con ellos.map { … }transforma cada elemento con la closure y devuelve la lista de los resultados, en el mismo orden. En lugar de la closure puedes pasar el nombre de una función (map(describir)) o un key path (map(\.nombre)): una forma corta de decir «de cada elemento, sunombre», equivale a{ $0.nombre }; ya viste uno,\.rawValue, en Enums con valores asociados.compactMap { … }transforma cada elemento, pero la closure puede devolvernil(«nada») y esos resultados se descartan. Sirve para «quédate con esto de los elementos que lo tengan».forEach { … }ejecuta la closure una vez por elemento y no devuelve nada.
Ninguna cambia la lista original: devuelven otra. Una nota sobre cómo se escriben las closures de abajo: if case .cafe = $0 { true } else { false } es un if usado como expresión, así que cada rama es el valor que devuelve la closure, sin return (lo mismo que hiciste en Enums: el partido de eventos). En Kotlin no había compactMap: se escribía filterIsInstance y después map. En Swift no existe is para saber qué caso es un enum; se usa if case, y compactMap recoge lo que sí encaja.
Dentro de un forEach, un return sale solo de la closure, es decir, de la vuelta actual, y forEach sigue con el elemento siguiente: es la forma de decir «salta este». Con guard case let .gol(…) = evento else { return } se saltan los eventos que no son goles; si es un gol, sus datos valen para el resto de la closure. Un detalle: Partido ya tiene una propiedad eventos y ahora también tendrá una función eventos(_:). Son cosas distintas con el mismo nombre; con paréntesis o una closure final (alFinal.eventos { … }) Swift elige la función, y sin ellos la propiedad.
Dónde se prueba. En tu Playground de Xcode, el de Tu primer paquete de Swift: reutilízalo, borra lo anterior, pega el código, ejecútalo y mira la consola.
enum Pedido { // como EventoDePartido: casos con datos
case cafe(mesa: Int)
case tostada(mesa: Int, extra: String)
}
// Una constante cuyo valor es una closure: ¿es un café?
let esCafe: (Pedido) -> Bool = {
if case .cafe = $0 { true } else { false }
}
// La función DEVUELVE una closure; `{ $0 > minimo }` recuerda `minimo`.
func masDe(_ minimo: Int) -> (Int) -> Bool {
{ $0 > minimo }
}
let pedidos: [Pedido] = [.cafe(mesa: 1), .tostada(mesa: 2, extra: "tomate"), .cafe(mesa: 2)]
let cafes = pedidos.filter(esCafe) // filter se queda con los que dan true
let extras = pedidos.compactMap { pedido in
if case let .tostada(_, extra) = pedido { extra } else { nil } // nil se descarta
}
var mesasConTostada: [Int] = []
pedidos.forEach { pedido in // una vuelta por pedido
guard case let .tostada(mesa, _) = pedido else { return } // no es tostada: sale de esta vuelta
mesasConTostada.append(mesa) // aquí `mesa` ya vale
}
func nombre(_ pedido: Pedido) -> String { // una función normal, para pasársela a map
switch pedido {
case .cafe: "café"
case .tostada: "tostada"
}
}
print(cafes.count, extras, mesasConTostada) // 2 ["tomate"] [2]
print(pedidos.map(nombre)) // map con el nombre de una función
print([5, 20, 8].filter(masDe(6))) // [20, 8]: filter con una closure devuelta por una funciónImprime 2 ["tomate"] [2], ["café", "tostada", "café"] y [20, 8].
Tu Kotlin y este Swift, línea a línea
Kotlin (tus Partido.kt y Main.kt) | Swift (Partido.swift) | Diferencia |
|---|---|---|
eventos.filter(filtro) | eventos.filter(filtro) | Igual; también acepta el nombre de una función |
filterIsInstance<Gol>().map(Gol::jugador) | compactMap { if case let .gol(_, jugador, _) = evento { jugador } else { nil } } | No hay is para los casos de un enum: if case liga el dato y compactMap descarta los nil |
map(::describir) | map(describir) | Sin :: |
return@forEach | return | En Swift, return dentro de una closure sale solo de la closure |
Reto · Ejercicio: los préstamos de la biblioteca
En tu Playground (borra antes lo anterior), un préstamo puede ser un libro (con su título y los días que lleva prestado) o una revista (con su título). Escribe una función que dé el título de cualquier préstamo. Con tres préstamos —el libro «Dune» con 20 días, la revista «Hola» y el libro «Momo» con 5 días—, imprime: los títulos de los préstamos que son libros, los días de cada libro y los títulos de los libros que llevan más de 14 días (recorriendo los préstamos uno a uno y saltando los que no son libros).
["Dune", "Momo"]
[20, 5]
["Dune"]Ver solución
enum Prestamo {
case libro(titulo: String, dias: Int)
case revista(titulo: String)
}
func titulo(_ prestamo: Prestamo) -> String {
switch prestamo {
case let .libro(titulo, _): titulo
case let .revista(titulo): titulo
}
}
let prestamos: [Prestamo] = [
.libro(titulo: "Dune", dias: 20),
.revista(titulo: "Hola"),
.libro(titulo: "Momo", dias: 5),
]
let soloLibros = prestamos.filter { prestamo in
if case .libro = prestamo { true } else { false }
}
let diasDeCadaLibro = prestamos.compactMap { prestamo in
if case let .libro(_, dias) = prestamo { dias } else { nil }
}
var atrasados: [String] = []
prestamos.forEach { prestamo in
guard case let .libro(titulo, dias) = prestamo else { return }
if dias > 14 { atrasados.append(titulo) }
}
print(soloLibros.map(titulo))
print(diasDeCadaLibro)
print(atrasados)Imprime ["Dune", "Momo"], [20, 5] y ["Dune"]. filter se queda con los libros, compactMap saca los días y descarta la revista (que da nil), y el return del guard salta la revista sin salir del forEach.
Paso · Preguntarle cosas al partido
Objetivo. Añadir a Partido tres funciones (eventos(_:), goleadores() y minutosDeGolDe(_:)) y ver el resultado en main.swift. Al terminar, xcrun swift run torneo-cli imprime cuatro líneas nuevas antes de las de Vuelta.
El título de cada bloque es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.
Lo que tiene que ofrecer Partido. Los tests que copiarás usan estos nombres:
| Nombre | Qué recibe y qué hace |
|---|---|
eventos (una función, sin etiqueta) | recibe un filtro y devuelve los eventos que lo cumplen |
goleadores | el jugador de cada gol, en orden |
minutosDeGolDe | recibe un jugador (sin etiqueta) y devuelve los minutos de sus goles |
Intenta escribirlo tú: las tres funciones van en Partido.swift, dentro del struct, tras resultado() y antes de la llave que lo cierra (el resto del fichero no cambia). Después, en main.swift, justo debajo del comentario // Closures: filtrar eventos y sacar goleadores. y encima de la línea repetir(2), escribe unas líneas que, con el partido alFinal, impriman sus tarjetas descritas, cuántos eventos hay antes del minuto 45, los nombres de los goleadores y los minutos de gol de Ana. Guarda cada resultado en una constante y luego imprímela. Lo que debes ver:
Tarjetas: ["30\' Tarjeta amarilla para Iván"]
Primera mitad: 2 eventos
Goleadores: ["Ana", "Iván", "Ana"]
Minutos de gol de Ana: [12, 80]Cuando termines, abre el bloque y compara con la solución.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
public func eventos(_ filtro: (EventoDePartido) -> Bool) -> [EventoDePartido] {
eventos.filter(filtro)
}
public func goleadores() -> [Jugador] {
eventos.compactMap { evento in
if case let .gol(_, jugador, _) = evento { jugador } else { nil }
}
}
public func minutosDeGolDe(_ jugador: Jugador) -> [Int] {
var minutos: [Int] = []
eventos.forEach { evento in
guard case let .gol(minuto, autor, _) = evento else { return }
if autor == jugador { minutos.append(minuto) }
}
return minutos
}Las funciones de Partido, comentadas línea a línea:
// `filtro` es una closure: recibe un evento y dice si entra (`true`) o no (`false`).
public func eventos(_ filtro: (EventoDePartido) -> Bool) -> [EventoDePartido] {
eventos.filter(filtro) // `filter` se queda con los eventos para los que `filtro` da `true`
}
public func goleadores() -> [Jugador] {
// `compactMap` aplica la closure a cada evento y descarta los `nil`.
eventos.compactMap { evento in
// Si es un gol, devuelve el jugador; si no, `nil`.
if case let .gol(_, jugador, _) = evento { jugador } else { nil }
}
}
public func minutosDeGolDe(_ jugador: Jugador) -> [Int] {
var minutos: [Int] = [] // aquí se van acumulando los minutos
eventos.forEach { evento in // la closure se ejecuta una vez por evento
// Si el evento NO es un gol, sale de esta vuelta (el `return` sale solo de la closure).
guard case let .gol(minuto, autor, _) = evento else { return }
// Si el gol es de ese jugador, apunta el minuto (la closure captura `minutos`).
if autor == jugador { minutos.append(minuto) }
}
return minutos
}Y las cuatro líneas nuevas de main.swift, en pasos. Van debajo del comentario // Closures: …, encima de repetir(2); lo demás queda como estaba:
// Closures: filtrar eventos y sacar goleadores.
// La closure final decide qué eventos pasan; `.map(describir)` pasa cada uno a texto.
let lasTarjetas = alFinal.eventos { if case .tarjeta = $0 { true } else { false } }
print("Tarjetas: \(lasTarjetas.map(describir))")
// `antesDelMinuto(45)` devuelve la closure que hace de filtro.
let primeraMitad = alFinal.eventos(antesDelMinuto(45))
print("Primera mitad: \(primeraMitad.count) eventos")
// `goleadores()` da un jugador por gol; `\.nombre` se queda con el nombre de cada uno.
let quienesMarcaron = alFinal.goleadores()
print("Goleadores: \(quienesMarcaron.map(\.nombre))")
let minutosDeAna = alFinal.minutosDeGolDe(ana)
print("Minutos de gol de Ana: \(minutosDeAna)")
// (aquí siguen `repetir(2)`, `contador()` y `alResolver`, sin cambios)En la salida, Tarjetas: ["30\' Tarjeta amarilla para Iván"] lleva comillas y el apóstrofo escapado: así imprime Swift un [String]. Para el aspecto de Kotlin, usa .joined(separator: ", ").
Los tests
ClosuresTests.swift no lo escribes: son ocho comprobaciones de las funciones de esta lección y de la anterior, del mismo tipo que las demás. Se rellena con el botón Copiar fichero, que copia el contenido al portapapeles y no escribe nada en tu proyecto (se explica en Clases y objetos). El orden es siempre el mismo: crear el fichero, pulsar el botón, seleccionar todo, pegar.
- En el Project navigator, clic derecho sobre
Tests → TorneoTests→ New File from Template… → Swift File → Next →ClosuresTests→ Create. (Plan B:touch Tests/TorneoTests/ClosuresTests.swiftdesde~/Marcador-Swift/Torneo;touchcrea un fichero vacío con ese nombre.) - Pulsa el botón de abajo. Aparecerá un aviso de que se ha copiado.
- Haz clic dentro de
ClosuresTests.swift, selecciona todo con Cmd+A y pega con Cmd+V.
Punto de control
- Build. Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina enBuild complete!. - Run.
xcrun swift run torneo-clitermina con estas líneas:
Tarjetas: ["30\' Tarjeta amarilla para Iván"]
Primera mitad: 2 eventos
Goleadores: ["Ana", "Iván", "Ana"]
Minutos de gol de Ana: [12, 80]
Vuelta 1
Vuelta 2
Contador: 1, 2, 3
Ganó Rayo FC
Partidos vistos desde `torneo`: 1- Test.
xcrun swift testtermina conTest run with 42 tests in 6 suites passed: los 34 anteriores y los 8 deClosuresTests.swift. - Provocado. En
minutosDeGolDecambiaautor == jugadorporautor != jugador:xcrun swift testfalla (enClosuresTests, el test de los minutos conreturnen la closure). Devuélvelo a como estaba. - Sabes decir qué descarta
compactMap, y por qué unreturndentro deforEachno sale deminutosDeGolDe.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.Dentro de
eventos.forEach { evento in … }escribesreturnpara saltar un evento. ¿Qué hace en Swift?2.Con
[.cafe, .tostada, .cafe]y una closure que devuelve el extra de las tostadas ynilen los cafés, ¿qué hacecompactMapcon losnil?3.
esGoles una constante cuyo valor es una closure de tipo(EventoDePartido) -> Bool. ¿Qué haceeventos.filter(esGol)?4.
Partidotiene una propiedadeventosy una funcióneventos(_:). ¿Cómo elige Swift entre las dos?5.
quienesMarcaron.map(\.nombre)recibe una lista deJugador. ¿Qué devuelve?