Lambdas con listas: filter, map y reglas
Al terminar: Usar lambdas con filter, filterIsInstance y map, y guardar reglas como funciones en el torneo.
Al terminar esta lección podrás preguntarle a un partido cosas como «dame solo las tarjetas», «dame los goleadores» o «cuántos eventos hubo en la primera mitad», y cada pregunta será una línea. Es lo que se consigue al juntar las lambdas de la lección anterior con las listas: filter para quedarte con unos elementos, map para transformarlos y filterIsInstance para quedarte con los de un tipo.
Temas: filter, map, filterIsInstance, el atajo :: (las referencias) y reglas guardadas en variables que se pasan a filter.
filter y map: dos preguntas a una lista
Una lista es un valor que guarda varios valores en orden (listOf(4, 1, 7)); las listas se estudian a fondo en Colecciones. Hoy solo necesitas dos funciones suyas que reciben una lambda y la aplican a cada elemento:
filter { … }se queda con los elementos para los que la lambda datruey devuelve una lista nueva con ellos.map { … }transforma cada elemento con la lambda y devuelve la lista de los resultados, en el mismo orden.
Ninguna cambia la lista original: devuelven otra. Se pueden encadenar, porque cada una devuelve una lista. Y en lugar de la lambda puedes pasar una referencia con :: si solo llama a algo que ya existe (la sección siguiente a este ejemplo lo explica).
fun main() {
val precios = listOf(4, 1, 7, 2)
// filter: se queda con los elementos para los que la lambda da true
println(precios.filter { it > 2 })
// map: transforma cada elemento con la lambda y devuelve la lista de resultados
println(precios.map { it * 10 })
// se encadenan: primero filter, luego map sobre lo que quedó
println(precios.filter { it > 2 }.map { it * 10 })
// map también recibe una referencia con :: en lugar de la lambda.
// length es el número de letras de un texto
val palabras = listOf("pera", "kiwi", "uva")
println(palabras.map(String::length)) // String::length equivale a { it.length }
// filterIsInstance<String>(): entre los <> va el tipo que quieres conservar.
// Esta lista admite cualquier cosa: mezcla números y textos
val mezcla = listOf(1, "dos", 3, "cuatro")
println(mezcla.filterIsInstance<String>())
}[4, 7]
[40, 10, 70, 20]
[40, 70]
[4, 4, 3]
[dos, cuatro]filterIsInstance<Tipo>() es lo que usarás con los eventos del partido: de una lista de EventoDePartido te quedas solo con los Gol, y el resultado ya es una lista de Gol, así que puedes usar sus propiedades sin más comprobaciones.
El atajo ::
Si la lambda solo llama a algo que ya existe, puedes nombrarlo directamente con :: y te ahorras escribir la lambda. Según lo que haya a la izquierda de :::
- Un tipo:
String::lengthequivale a{ texto -> texto.length }. Es la propiedad de cualquierString, y la lambda recibe eseString. - Nada, para una función suelta (no está dentro de ninguna clase):
::nombreDeequivale a{ producto -> nombreDe(producto) }. - Un objeto concreto:
partido::golesDeequivale a{ equipo -> partido.golesDe(equipo) }, ligada a esepartido. No la necesitas en esta lección.
El ejemplo de la cesta, justo debajo, usa las dos primeras.
Un ejemplo con una cesta de la compra
Ahora, todo junto con otros datos: una cesta con frutas y pan, que son dos tipos de Producto (una interfaz sellada, como la de Sealed: estados con datos). La clase Cesta tiene una función filtrar que recibe la condición como lambda, igual que lo hará Partido. Y esPan es una condición guardada en una variable, que se pasa a filtrar como cualquier otra función:
sealed interface Producto
data class Fruta(val nombre: String, val precio: Int) : Producto
data class Pan(val precio: Int) : Producto
// Función suelta: el nombre de cualquier producto (when exhaustivo sobre la interfaz sellada)
fun nombreDe(producto: Producto): String = when (producto) {
is Fruta -> producto.nombre
is Pan -> "Pan"
}
class Cesta(val productos: List<Producto>) {
// Función de orden superior: recibe una condición y la pasa a filter
fun filtrar(condicion: (Producto) -> Boolean): List<Producto> = productos.filter(condicion)
}
// Una regla guardada en una variable: se reutiliza donde haga falta
val esPan: (Producto) -> Boolean = { it is Pan }
fun main() {
val cesta = Cesta(listOf(Fruta("Pera", 2), Pan(1), Fruta("Mango", 4)))
println(cesta.filtrar { it is Pan }) // trailing lambda con it
println(cesta.filtrar(esPan).size) // la misma regla, guardada y pasada por su nombre
val frutas = cesta.productos.filterIsInstance<Fruta>() // devuelve List<Fruta>
println(frutas.map(Fruta::nombre)) // Tipo::propiedad = { f -> f.nombre }
println(frutas.filter { it.precio <= 2 }) // aquí it es una Fruta: tiene precio
println(cesta.productos.map(::nombreDe)) // ::funcion = { p -> nombreDe(p) }
}[Pan(precio=1)]
1
[Pera, Mango]
[Fruta(nombre=Pera, precio=2)]
[Pera, Pan, Mango]Reto · Ejercicio: palabras y mezclas
En el Kotlin Playground, fuera de tu proyecto. Con la lista de palabras sol, luna, estrella y mar, imprime, en este orden: las palabras de más de 3 letras; las cuatro palabras en mayúsculas (uppercase() convierte un texto a mayúsculas); y la longitud de las palabras de más de 3 letras (length es el número de letras de un texto). Después, con la lista mezclada 3, «casa», 8, «tren» y 5, imprime los números multiplicados por 2 (solo los números).
La consola debe mostrar:
[luna, estrella]
[SOL, LUNA, ESTRELLA, MAR]
[4, 8]
[6, 16, 10]Ver solución
fun main() {
val palabras = listOf("sol", "luna", "estrella", "mar")
println(palabras.filter { it.length > 3 }) // las de más de 3 letras
println(palabras.map { it.uppercase() }) // todas, en mayúsculas
println(palabras.filter { it.length > 3 }.map { it.length }) // primero filtra, luego mide
val mezcla = listOf(3, "casa", 8, "tren", 5)
// filterIsInstance<Int>() deja solo los números; map multiplica cada uno
println(mezcla.filterIsInstance<Int>().map { it * 2 })
}Paso · Preguntarle cosas al partido
Objetivo. Añadir a Partido las funciones eventos(filtro) y goleadores(), completar Main con tres líneas más y copiar LambdasTest.kt. Al terminar, ./gradlew :torneo:run imprime seis líneas de lambdas y los tests de la lección pasan.
Partido.kt y Main.kt ya existen: ábrelos con doble clic. Una rareza de Partido: ya tiene una propiedad llamada eventos (la lista) y ahora tendrá también una función eventos(filtro). Kotlin las distingue por los paréntesis: partido.eventos es la lista y partido.eventos { … } llama a la función. Es la misma idea que la filtrar de la cesta, solo que con el mismo nombre.
Dónde va cada cosa. En Partido.kt, pega las dos funciones nuevas justo encima del comentario // Retorno con etiqueta (la función minutosDeGolDe de la lección anterior queda debajo, sin cambios); el … del bloque solo marca el código que ya tienes. En Main.kt, pega el bloque nuevo en lugar del comentario // Lambdas: minutos de gol y repetir.: trae un comentario nuevo y las tres líneas, y debajo siguen Minutos de gol de Ana y repetir, sin cambios.
Intenta escribirlo tú, en este orden:
- Dos preguntas a
Partido.eventos(filtro)recibe una condición sobre un evento y devuelve los eventos que la cumplen;goleadores()devuelve quién marcó cada gol, en orden (si prefieres, escríbela con un bucleforymutableListOf<Jugador>()en vez defilterIsInstanceymap: el resultado es el mismo). main. Con el partido de la lección anterior: describe las tarjetas (condescribir), cuenta los eventos de la primera mitad (antes del minuto 45) y lista los goleadores por nombre.
Tras las líneas de la lección anterior, debe verse:
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 2Cuando termines, abre el bloque y compara con tu código.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
…
// Función de orden superior: recibe una función como parámetro.
// `(EventoDePartido) -> Boolean` es un tipo función: toma un evento y
// devuelve si cumple. Al llamarla, la lambda va fuera de los paréntesis
// (trailing lambda): partido.eventos { it is Tarjeta }
fun eventos(filtro: (EventoDePartido) -> Boolean): List<EventoDePartido> = eventos.filter(filtro)
// `Gol::jugador` es una referencia a propiedad: equivale a { gol -> gol.jugador }.
fun goleadores(): List<Jugador> = eventos.filterIsInstance<Gol>().map(Gol::jugador)
// Retorno con etiqueta: `return@forEach` sale solo de la lambda actual
… // Lambdas: filtrar eventos y sacar goleadores.
println("Tarjetas: ${alFinal.eventos { it is Tarjeta }.map(::describir)}")
println("Primera mitad: ${alFinal.eventos(antesDelMinuto(45)).size} eventos")
println("Goleadores: ${alFinal.goleadores().map { it.nombre }}")
…::describir es la referencia a una función suelta: apunta a la función describir de Sealed: estados con datos. Y map { it.nombre } es una lambda con it.
Ejecuta ./gradlew :torneo:run en la Terminal de Android Studio (View → Tool Windows → Terminal). Las seis últimas líneas son las del punto de control de abajo.
El test
LambdasTest.kt son seis tests de las funciones que has escrito en esta lección y en la anterior; no enseñan nada nuevo de kotlin.test, así que no los escribes: los copias. Primero crea el fichero: en torneo → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) (el que lleva (test) detrás) → New → Kotlin Class/File → File → LambdasTest → Intro. Después pulsa Copiar fichero, que copia el contenido al portapapeles. En el editor, selecciona todo (Cmd+A) y pega (Cmd+V): el fichero copiado trae su propia línea package, y sustituye a la que escribió Android Studio.
Del fichero que copiaste, este es el test que enseña algo: usa la función que devuelve función. (Se muestra solo el test, sin la clase que lo rodea.)
@Test
fun funcionQueDevuelveFuncion() {
assertEquals(1, partido.eventos(antesDelMinuto(20)).size)
assertEquals(3, partido.eventos(antesDelMinuto(60)).size)
}Ejecuta ./gradlew :torneo:test. Debe terminar en BUILD SUCCESSFUL. Para ver el recuento, abre el informe como en Bucles y tu primer test: open torneo/build/reports/tests/test/index.html en la Terminal; arriba debe poner 36 tests.
Punto de control
./gradlew :torneo:runtermina enBUILD SUCCESSFULy las últimas líneas son:
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./gradlew :torneo:testpasa. El informe (open torneo/build/reports/tests/test/index.html) muestra 36 tests en total: los 30 de antes y los 6 deLambdasTest.Partidotiene a la vez la propiedadeventosy la funcióneventos(filtro)y compila.- En
minutosDeGolDeusasreturn@forEach, noreturn. Cámbialo porreturn minutos, ejecuta./gradlew :torneo:testy comprueba queminutosDeGolSaltaLosEventosQueNoSonGolesfalla. Devuélvelo.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Qué devuelve
listOf(4, 1, 7, 2).filter { it > 2 }?2.Tienes
val precios = listOf(4, 1, 7, 2). ¿Qué devuelveprecios.map { it * 10 }?3.Con una lista de
EventoDePartido, ¿para qué sirvefilterIsInstance<Gol>()?4.En
Partidoconviven la propiedadeventosy la funcióneventos(filtro). ¿Cómo sabe Kotlin a cuál te refieres?5.Escribes
partido.eventos(antesDelMinuto(45)). ¿Qué esantesDelMinuto(45)?